Java字节码是java代码编译后的中间代码格式,JVM需要读取并解析字节码才能执行相应的任务
获取字节码简介:由单字节(byte
)的指令组成
指令
), 主要由类型前缀
和操作名称
两部分组成。获取字节码清单
用 javap
工具来获取 class 文件中的指令清单,专门用于反编译 class 文件。
Compiled from "HelloByteCode.java"
public class demo.jvm0104.HelloByteCode {
public demo.jvm0104.HelloByteCode();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class demo/jvm0104/HelloByteCode
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: return
}
解读字节码清单
public demo.jvm0104.HelloByteCode(); // 如果不定义任何构造函数,就会有一个默认的无参构造函数.这是 Java 编译器生成的, 而不是运行时JVM自动生成的。
//每个构造函数中会先调用super类的构造函数,默认构造函数中有些字节码指令来干这个事情
//解析的java/lang/Object 默认继承了Object类
public demo.jvm0104.HelloByteCode();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
查看class中的常量池
运行时常量池
。运行时常量池里面的常量主要是由 class 文件中的 常量池结构体
组成的。#1 = Methodref #4.#13 // java/lang/Object."":()V
,#1
:常量编号,该文件中其他地方可以引用=
:分隔符Methodref
:表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类指向的 #4
, 方法签名指向的 #13
;查看方法信息
//main方法编译结果
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
方法描述: ([Ljava/lang/String;)V:
L
表示对象;java/lang/String
就是类名称;V
则表示这个方法的返回值是 void
;flags: ACC_PUBLIC, ACC_STATIC
,表示 public 和 static。还可以看到执行该方法时需要的栈(stack)深度是多少,需要在局部变量表中保留多少个槽位, 还有方法的参数个数: stack=2, locals=2, args_size=1
。
线程栈与字节码执行模型
栈帧
(Frame)。每一次方法调用,JVM都会自动创建一个栈帧。
栈帧
由 操作数栈
, 局部变量数组
以及一个class 引用
组成。class 引用
指向当前方法在运行时常量池中对应的 class)。局部变量数组
也称为 局部变量表
(LocalVariableTable), 其中包含了方法的参数,以及局部变量。 局部变量数组的大小在编译时就已经确定: 和局部变量+形参的个数有关,还要看每个变量/参数占用多少个字节。操作数栈是一个 LIFO 结构的栈, 用于压入和弹出值。 它的大小也在编译时确定。方法体中的字节码解读
0: new #2 // class demo/jvm0104/HelloByteCode
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: return
前面的数字:间隔不相等的原因是, 有一部分操作码会附带有操作数, 也会占用字节码数组中的空间。
例如:new
就会占用三个槽位: 一个用于存放操作码指令自身,两个用于存放操作数。因此,下一条指令 dup
的索引从 3
开始。
对象初始化指令:new 指令, init 以及 clinit 简介
创建类实例生成操作码
0: new #2 // class demo/jvm0104/HelloByteCode 创建对象,但没有调用构造函数
3: dup // 用来调用某些特殊方法的,即构造函数
4: invokespecial #3 // Method "":()V 用于复制栈顶的值。构造函数调用不会返回值,所以如果没有 dup 指令, 在对象上调用方法并初始化之后,操作数栈就会是空的,在初始化之后就会出问题。所以在构造函数返回之后,可以将对象实例赋值给局部变量或某个字段
接下来指令
astore {N} or astore_{N} – 赋值给局部变量,其中 {N} 是局部变量表中的位置。
putfield – 将值赋给实例字段
putstatic – 将值赋给静态字段
在调用构造函数的时候,还会执行另一个类似的方法
,甚至在执行构造函数之前就执行了。
还有一个可能执行的方法是该类的静态初始化方法
, 但
并不能被直接调用,而是由这些指令触发的: new
, getstatic
, putstatic
or invokestatic
。
栈内存操作指令
dup
和 pop
指令。
dup
指令复制栈顶元素的值。pop
指令则从栈中删除最顶部的值。swap
, dup_x1
和 dup2_x1
。
swap
指令可交换栈顶两个元素的值,例如A和B交换位置(图中示例4);dup_x1
将复制栈顶元素的值,并在栈顶插入两次(图中示例5);dup2_x1
则复制栈顶两个元素的值,并插入第三个值(图中示例6)。局部变量表
stack
主要用于执行指令,而局部变量则用来保存中间结果,两者之间可以直接交互。
javac -g demo/jvm0104/*.java(生成调试信息的 -g
参数)
javap -c -verbose demo/jvm0104/LocalVariableTest (反编译)
代码
//移动平均数
public class MovingAverage {
private int count = 0;
private double sum = 0.0D;
public void submit(double value){
this.count ++;
this.sum += value;
}
public double getAvg(){
if(0 == this.count){ return sum;}
return this.sum/this.count;
}
}
public class LocalVariableTest {
public static void main(String[] args) {
MovingAverage ma = new MovingAverage();
int num1 = 1;
int num2 = 2;
ma.submit(num1);
ma.submit(num2);
double avg = ma.getAvg();
}
}
反编译
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=6, args_size=1
0: new #2 // class demo/jvm0104/MovingAverage new, 创建 MovingAverage 类的对象;
3: dup // 复制栈顶引用值。
4: invokespecial #3 // Method demo/jvm0104/MovingAverage."":()V invokespecial 执行对象初始化。
7: astore_1 //使用 astore_1 指令将引用地址值(addr.)存储(store)到编号为1的局部变量中: astore_1 中的 1 指代 LocalVariableTable 中ma对应的槽位编号,
8: iconst_1 // iconst_1 和 iconst_2 用来将常量值1和2加载到栈里面, 并分别由指令 istore_2 和 istore_3 将它们存储到在 LocalVariableTable 的槽位 2 和槽位 3 中。store 之类的指令调用实际上从栈顶删除了一个值。 这就是为什么再次使用相同值时,必须再加载(load)一次的原因。
9: istore_2
10: iconst_2
11: istore_3
12: aload_1
13: iload_2
14: i2d
15: invokevirtual #4 // Method demo/jvm0104/MovingAverage.submit:(D)V
18: aload_1
19: iload_3
20: i2d
21: invokevirtual #4 // Method demo/jvm0104/MovingAverage.submit:(D)V
24: aload_1 //调用 getAvg() 方法后,返回的结果位于栈顶,然后使用 dstore 将 double 值保存到本地变量4号槽位,这里的d表示目标变量的类型为double。
25: invokevirtual #5 // Method demo/jvm0104/MovingAverage.getAvg:()D
28: dstore 4
30: return
LineNumberTable:
line 5: 0
line 6: 8
line 7: 10
line 8: 12
line 9: 18
line 10: 24
line 11: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 ma Ldemo/jvm0104/MovingAverage;
10 21 2 num1 I
12 19 3 num2 I
30 1 4 avg D
给局部变量赋值时,需要使用相应的指令来进行 store
,如 astore_1
。store
类的指令都会删除栈顶值。 相应的 load
指令则会将值从局部变量表压入操作数栈,但并不会删除局部变量中的值。
流程控制指令
主要是分支和循环在用, 根据检查条件来控制程序的执行流程。
代码
public class ForLoopTest {
private static int[] numbers = {1, 6, 8};
public static void main(String[] args) {
MovingAverage ma = new MovingAverage();
for (int number : numbers) {
ma.submit(number);
}
double avg = ma.getAvg();
}
}
编译反编译
javac -g demo/jvm0104/*.java
javap -c -verbose demo/jvm0104/ForLoopTest
字节码
0: new #2 // class demo/jvm0104/MovingAverage
3: dup
4: invokespecial #3 // Method demo/jvm0104/MovingAverage."":()V
7: astore_1
8: getstatic #4 // Field numbers:[I
11: astore_2
12: aload_2
13: arraylength
14: istore_3
15: iconst_0
16: istore 4
18: iload 4 //循环体 用于执行循环计数器与数组长度的比较
20: iload_3
21: if_icmpge 43 //if, integer, compare, great equal, 如果一个数的值大于或等于另一个值,则程序执行流程跳转到pc=43的地方继续执行。
24: aload_2
25: iload 4
27: iaload
28: istore 5
30: aload_1
31: iload 5
33: i2d
34: invokevirtual #5 // Method demo/jvm0104/MovingAverage.submit:(D)V
37: iinc 4, 1 // 4号槽位的值加1
40: goto 18 //跳到循环开始的地方
43: aload_1
44: invokevirtual #6 // Method demo/jvm0104/MovingAverage.getAvg:()D
47: dstore_2
48: return
LocalVariableTable:
Start Length Slot Name Signature
30 7 5 number I //5 号槽位被 number 占用了。
0 49 0 args [Ljava/lang/String; //0槽位被 main 方法的参数 args 占据了
8 41 1 ma Ldemo/jvm0104/MovingAverage; //1 号槽位被 ma 占用了。
48 1 2 avg D //2 号槽位是for循环之后才被 avg 占用的。
2号槽位的变量保存了 numbers 的引用值,占据了 2号槽位。
3号槽位的变量, 由 arraylength 指令使用, 得出循环的长度。
4号槽位的变量, 是循环计数器, 每次迭代后使用 iinc 指令来递增。
算术运算指令与类型转换指令
将 int
值作为参数传递给实际上接收 double
的 submit()
方法时, 在实际调用该方法之前,使用了类型转换的操作码
31: iload 5
33: i2d
34: invokevirtual #5 // Method demo/jvm0104/MovingAverage.submit:(D)V
将一个 int 类型局部变量的值, 作为整数加载到栈中,然后用 i2d
指令将其转换为 double
值,以便将其作为参数传给submit
方法。
唯一不需要将数值load到操作数栈的指令是 iinc
,它可以直接对 LocalVariableTable
中的值进行运算。 其他的所有操作均使用栈来执行。
方法调用指令和参数传递
用于方法调用的指令
invokestatic
,用于调用某个类的静态方法,这也是方法调用指令中最快的一个。
invokespecial
, 用来调用构造函数,也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。
invokevirtual
,如果是具体类型的目标对象,用于调用公共,受保护和打包私有方法。
invokeinterface
,调用的方法属于某个接口。运行时受到更多限制
区别:
使用 invokestatic
指令,JVM 就确切地知道要调用的是哪个方法:因为调用的是静态方法,只能属于一个类。
使用 invokespecial
时, 查找的数量也很少, 解析也更加容易, 那么运行时就能更快地找到所需的方法。
JDK7 新增的方法调用指令 invokedynamic
A a=new A(); a.m()
,拿到一个 A 类型的实例,然后直接调用方法;Method.invoke
反射调用;