一、概述
java源码编译后都会生成字节码,字节码是JVM执行代码的中间形态,到运行时jvm会将字节码文件读取到内存中并翻译为机器可识别执行的机器码。JVM字节码是一种基于栈的指令集架构(Stack-based Instruction Set Architecture)。每个字节码指令都会在JVM上执行一系列的操作,如加载、存储、运算、跳转等。
二、字节码生成
对于源码的编译,jdk提供了javac 工具,对于单文件的字节码生成,先使用javac 生成,然后可以使用 javap 进行查看。
$ javap -c PropertyValue
警告: 文件 .\PropertyValue.class 不包含类 PropertyValue
Compiled from "PropertyValue.java"
public class com.resean.spring.beans.factory.config.PropertyValue {
public com.resean.spring.beans.factory.config.PropertyValue(java.lang.String, java.lang.Object, java.lang.String, boolean);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #7 // Field name:Ljava/lang/String;
9: aload_0
10: aload_2
11: putfield #13 // Field value:Ljava/lang/Object;
14: aload_0
15: aload_3
16: putfield #17 // Field type:Ljava/lang/String;
19: aload_0
20: iload 4
22: putfield #20 // Field isRef:Z
25: return
public com.resean.spring.beans.factory.config.PropertyValue(java.lang.String, java.lang.Object);
Code:
0: aload_0
1: ldc #24 // String
3: aload_2
4: aload_1
5: iconst_0
6: invokespecial #26 // Method "<init>":(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Z)V
9: return
public java.lang.String getName();
Code:
0: aload_0
1: getfield #7 // Field name:Ljava/lang/String;
4: areturn
public java.lang.Object getValue();
Code:
0: aload_0
1: getfield #13 // Field value:Ljava/lang/Object;
4: areturn
public java.lang.String getType();
Code:
0: aload_0
1: getfield #17 // Field type:Ljava/lang/String;
4: areturn
public boolean isRef();
Code:
0: aload_0
1: getfield #20 // Field isRef:Z
4: ireturn
}
在实际的项目中,对于代码的打包,由maven或者gradel 这些打包工具完成。后续将仔细讲讲 maven的构建原理。
三、字节码指令
字节码在jvm中运行实际上是根据指令集一行行读取并进行操作的,了解字节码指令集可以帮助我们了解jvm的运行机制及编写高效的代码。
字节码指令集主要包括操作码和操作数两部份,指令集主要分为7大类:
- 加载与存储指令
加载与存储指令是用得最频繁的命令,其格式为- 加载:xload_n:表示局部变量表的槽位; x表示操作码助记符,表明是哪种数据类型。
- 存储:xstore_n:表示局部变量表的槽位; x表示操作码助记符,表明是哪种数据类型。
- 算术指令
算术指令是用于两个操作栈中值的特定运算,分为两种类型,整型运算和浮点型运算。
需要注意的是,数据运算可能会导致溢出,比如两个很大的正整数相加,很可能会得到一个负数。但 Java 虚拟机规范中并没有对这种情况给出具体结果,因此程序是不会显式报错的。所以,大家在开发过程中,如果涉及到较大的数据进行加法、乘法运算的时候,一定要注意!
当发生溢出时,将会使用有符号的无穷大 Infinity 来表示;如果某个操作结果没有明确的数学定义的话,将会使用 NaN 值来表示。而且所有使用 NaN 作为操作数的算术操作,结果都会返回 NaN。
以下是算术指令点类型:- 加法指令:iadd、ladd、fadd、dadd
- 减法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 求余指令:irem、lrem、frem、drem
- 自增指令:iinc
- 类型转换指令
类型转换指令可以分为两种:-
1)宽化
小类型向大类型转换,比如 :
int–>long–>float–>double,对应的指令有:i2l、i2f、i2d、l2f、l2d、f2d。- 从 int 到 long,或者从 int 到 double,是不会有精度丢失的;
- 从 int、long 到 float,或者 long 到 double 时,可能会发生精度丢失;
- 从 byte、char 和 short 到 int 的宽化类型转换实际上是隐式发生的,这样可以减少字节码指令,毕竟字节码指令只有 256个,占一个字节。
-
2)窄化
大类型向小类型转换,比如:- 从 int 类型到 byte、short 或者 char,对应的指令有:i2b、i2s、i2c;
- 从 long 到 int,对应的指令有:l2i;
- 从 float 到 int 或者 long,对应的指令有:f2i、f2l;
- 从 double 到 int、long 或者 float,对应的指令有:d2i、d2l、d2f。
-
- 对象的访问与创建指令
-
访问指令
字段可以分为两类,一类是成员变量,一类是静态变量(也就是类变量),所以字段访问指令可以分为两类:
访问静态变量:getstatic、putstatic。
访问成员变量:getfield、putfield,需要创建对象后才能访问。 -
创建指令
-
new #13 <java/lang/String>,创建一个 String 对象。
new #15 <java/io/File>,创建一个 File 对象。
newarray 10 (int),创建一个 int 类型的数组。
-
方法调用与返回指令
方法调用指令有 5 个,分别用于不同的场景:- invokevirtual:用于调用对象的成员方法,根据对象的实际类型进行分派,支持多态。
- invokeinterface:用于调用接口方法,会在运行时搜索由特定对象实现的接口方法进行调用。
- invokespecial:用于调用一些需要特殊处理的方法,包括构造方法、私有方法和父类方法。
- invokestatic:用于调用静态方法。
- invokedynamic:用于在运行时动态解析出调用点限定符所引用的方法,并执行。
方法返回指令根据不同类型返回:
-
操作数栈指令
常见的操作数栈管理指令有 pop、dup 和 swap。- 将一个或两个元素从栈顶弹出,并且直接废弃,比如 pop,pop2;
- 复制栈顶的一个或两个数值并将其重新压入栈顶,比如 dup,dup2,dup×1,dup2×1,dup×2,dup2×2;
- 将栈最顶端的两个槽中的数值交换位置,比如 swap。
-
控制转移指令
- 比较指令,比较栈顶的两个元素的大小,并将比较结果入栈。
- 条件跳转指令,通常和比较指令一块使用,在条件跳转指令执行前,一般先用比较指令进行栈顶元素的比较,然后进行条件跳转。
- 比较条件转指令,类似于比较指令和条件跳转指令的结合体,它将比较和跳转两个步骤合二为一。
- 多条件分支跳转指令,专为 switch-case 语句设计的。
- 无条件跳转指令,目前主要是 goto 指令。