1. Java字节码简介
Java bytecode 由单字节( byte )的指令组成, 理论上最多支持 256 个操作码(opcode)。实际上Java只使用了200左右的操作码, 还有一些操作码则保留给调试操作。
操作码, 主要由 类型前缀 和 操作名称 两部分组成。
例如,’ i ’ 前缀代表 ‘ integer ’,所以,’ iadd ’ 很容易理解, 表示对整-- 数执行加 法运算。
根据指令的性质,主要分为四个大类:
- 栈操作指令,包括与局部变量交互的指令
- 程序流程控制指令
- 对象操作指令,包括方法调用指令
- 算术运算以及类型转换指令
此外还有一些执行专门任务的指令,比如同步(synchronization)指令,以及抛出异常相关的指令等等。下文会对这些指令进行详细的讲解。
2. 获取字节码清单
javac
编译,生成字节码- 可以用
javap
工具来获取 class 文件中的指令清单
javac
不指定 ‐d 参数编译后生成的 .class 文件默认和源代码在同一个目录。
注意
: javac 工具默认开启了优化功能, 生成的字节码中没有局部变量表 (LocalVariableTable),相当于局部变量名称被擦除。如果需要这些调试信息, 在编 译时请加上 ‐g 选项。
JDK自带工具的详细用法, 请使用:javac ‐help
或者 javap ‐help 来查看; 其他类似。
3. 解读字节码
java类中不定义任何构造函数,就会有一个默认的午餐早函数。在编译后,我们可以在对应的class文件中看到这个默认的无参构造函数。
Compiled from "HelloByteCode.java"
public class com.megetood.geek.week01.HelloByteCode {
public com.megetood.geek.week01.HelloByteCode();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/megetood/geek/week01/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
}
4. 查看class文件中的常量池信息
常量池
: Constant pool, 大多数时候 指的是 运行时常量池 。运行时的常量池主要就是由 class 文件中的 常量池结构体 组成的
在反编译 class 时,指定 ‐verbose 选项, 则会 输出附加信息 ,可一看到更详细的字节码信息。
Classfile /D:/worksapce/ideaspace/enhance/megetood-geek/src/main/java/com/megetood/geek/week01/HelloByteCode.class
Last modified 2020-10-17; size 313 bytes
MD5 checksum a2c625e7a3caa197a59067d4a287f092
Compiled from "HelloByteCode.java"
public class com.megetood.geek.week01.HelloByteCode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // com/megetood/geek/week01/HelloByteCode
#3 = Methodref #2.#13 // com/megetood/geek/week01/HelloByteCode."<init>":()V
#4 = Class #15 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 HelloByteCode.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 com/megetood/geek/week01/HelloByteCode
#15 = Utf8 java/lang/Object
{
public com.megetood.geek.week01.HelloByteCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
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
0: new #2 // class com/megetood/geek/week01/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
LineNumberTable:
line 9: 0
line 10: 8
}
SourceFile: "HelloByteCode.java"
其中显示了很多关于class文件信息: 编译时间, MD5校验和, 从哪个 .java 源文 件编译得来,符合哪个版本的Java语言规范。
到 ACC_PUBLIC(public) 和 ACC_SUPER (super)访问标志符
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = Class #14 // com/megetood/geek/week01/HelloByteCode
#3 = Methodref #2.#13 // com/megetood/geek/week01/HelloByteCode."<init>":()V
#4 = Class #15 // java/lang/Object
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#1
常量编号, 该文件中其他地方可以引用。=
等号就是分隔符.Methodref
表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类 指向的 #4 , 方法签名指向的 #13 ; 当然双斜线注释后面已经解析出来可读性比 较好的说明了。
5. 查看方法信息
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=2, locals=2, args_size=1
分别表示栈(stack)深度是多少,需要在局部变量表中保留多少 个槽位, 还有方法的参数个数
public com.megetood.geek.week01.HelloByteCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
这里无参构造函数的参数个数不是0: stack=1, locals=1, args_size=1
;这是因为在 Java 中, 如果是静态方法则没有 this 引用。 对于非静态方法, this 将被分配到局部变量表的第0号槽位中,
6. 线程栈与字节码执行模型
JVM是一台基于栈的计算机器。每个线程都有一个独属于自己的线程栈(JVM stack), 用于存储 栈帧
(Frame)。每一次方法调用,JVM都会自动创建一个栈帧。 栈帧
由 操作数栈
,局部变量数组
以及一个 class引用
组成。
class引用
指向当前方法 在运行时常量池中对应的class)。局部变量表
(LocalVariableTable), 其中包含了方法的参数, 以及局部变量。操作数栈
是一个LIFO结构的栈, 用于压 入和弹出值。 它的大小也在编译时确定。 有一些操作码/指令可以将值压入操作数栈
; 还有一些操作码/指令则是从栈中获取操作数,并进行处理,再将结果压入栈。操作数栈还用于接收调用其他方法时返回的结 果值
7. 方法体中的字节码
下面这个是上面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
0: new #2 // class com/megetood/geek/week01/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
LineNumberTable:
line 9: 0
line 10: 8
指令前面的数字代表的是操作数栈中所占的槽位,new
就会占用三个槽位: 一个用于存放操作码指令自身,两个用于存放操作数。可以看到new是在0号槽位,而他的下一条指令dup从3号位开始。
8. 对象初始化指令:new指令, init 以及 clinit
0: new #2 // class com/megetood/geek/week01/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
new, dup 和 invokespecial 指令在一起时,那么一定是在创建类 的实例对象!
- new 指令只是创建对象,但没有调用构造函数。
- invokespecial 指令用来调用某些特殊方法的, 当然这里调用的是构造函数。
- dup 指令用于复制栈顶的值。 由于构造函数调用不会返回值,所以如果没有dup指令, 在对象上调用方法并初始 化之后,操作数栈就会是空的,在初始化之后就会出问题, 接下来的代码就无法 对其进行处理
9. 栈内存操作指令
- dup 指令复制栈顶元素的值。
- pop 指令则从栈中删除最顶部的值
10. 局部变量表
stack
主要用于执行指令,而局部变量表
则用来保存中间结果,两者之间可以直接交互
/**
* 计算平均数
* /
public class MovingAverage {
private int count = 0;
private double sum = 0.0D;
/**
* 增加一个数
*
* @param val
*/
public void submit(double val) {
this.count++;
this.sum += val;
}
/**
* 获得平均值
*/
public double getAvg() {
if (0 == count) {
return sum;
}
return this.sum / this.count;
}
}
调用类
public class LocalVariableTest {
/*
计算了num1与num2的平均数
*/
public static void main(String[] args) {
MovingAverage movingAverage = new MovingAverage();
int num1 = 1;
int num2 = 2;
movingAverage.submit(num1);
movingAverage.submit(num2);
double avg = movingAverage.getAvg();
}
}
对这两个类同时编译,然后反编译得到对应字节码,其中mian方法的字节码如下:
public static void main(java.lang.String[]);
Code:
// 创建对象
0: new #2 // class com/megetood/geek/week01/codebyte/MovingAverage
// 赋值栈顶引用值
3: dup
// 调用构造函数,初始化
4: invokespecial #3 // Method com/megetood/geek/week01/codebyte/MovingAverage."<init>":()V
// 将新建的对象引用地址值存储到局部变量表索引为1的位置
7: astore_1
// 将常量值1压栈
8: iconst_1
// 将常量1存储到局部变量表索引为2的位置
9: istore_2
// 将常量值2压栈
10: iconst_2
// 将常量1存储到局部变量表索引为3的位置
11: istore_3
// 将局部变量表索引为1、2的值压栈,分别对应前面新建的MovingAverage对象和int值1
12: aload_1
13: iload_2
// 对int值1进行类型转换,转换成double类型
14: i2d
// 调用movingAverage对象的submit方法
15: invokevirtual #4 // Method com/megetood/geek/week01/codebyte/MovingAverage.submit:(D)V
// 这三行跟上面是同样的操作
18: aload_1
19: iload_3
20: i2d
21: invokevirtual #4 // Method com/megetood/geek/week01/codebyte/MovingAverage.submit:(D)V
24: aload_1
25: invokevirtual #5 // Method com/megetood/geek/week01/codebyte/MovingAverage.getAvg:()D
// 将25行函数调用结果存储到局部变量表索引为4的位置
28: dstore 4
// 返回栈顶值
30: return
}
11. 流程控制指令
在上面基础上新建下面这个类
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 *.java
javap -c -verbose ForLoopTest.class
得到下面字节码(这里只展示了main方法中的)
0: new #2 // class com/megetood/geek/week01/codebyte/MovingAverage
3: dup
4: invokespecial #3 // Method com/megetood/geek/week01/codebyte/MovingAverage."<init>":()V
7: astore_1
// 获取数组
8: getstatic #4 // Field numbers:[I
// 保存局部对象
11: astore_2
// 加载刚刚保存的
12: aload_2
// 将数组长度压栈
13: arraylength
// 保存数组长度值到局部变量表3号位置
14: istore_3
// 循环计数器,i
15: iconst_0
16: istore 4
// 获取计数器初始值压栈
18: iload 4
// 将数组长度压栈
20: iload_3
// 比较计数器值与数组长度,如果计数器值>=数组长度,则跳到中间的指令,到43行开始执行
21: if_icmpge 43
// 下面是ma.submit(number);这条指令的字节码
24: aload_2
25: iload 4
27: iaload
28: istore 5
30: aload_1
31: iload 5
33: i2d
34: invokevirtual #5 // Method com/megetood/geek/week01/codebyte/MovingAverage.submit:(D)V
// 计数器自增
37: iinc 4, 1
// 跳到18行继续执行
40: goto 18
// 下面是double avg = ma.getAvg();这条指令的字节码
43: aload_1
44: invokevirtual #6 // Method com/megetood/geek/week01/codebyte/MovingAverage.getAvg:()D
47: dstore_2
48: return
// 这里就是局部变量表了
LocalVariableTable:
Start Length Slot Name Signature
30 7 5 number I
0 49 0 args [Ljava/lang/String;
8 41 1 ma Lcom/megetood/geek/week01/codebyte/MovingAverage;
48 1 2 avg D
12. 算术运算指令与类型转换指令
数据类型转换
算术操作码和类型
类型转换操作码
13. 方法调用指令和参数传递
invokestatic
,顾名思义,这个指令用于调用某个类的静态方法,这也是方法 调用指令中最快的一个。invokespecial
, 我们已经学过了, invokespecial 指令用来调用构造函数, 但也可以用于调用同一个类中的 private 方法, 以及可见的超类方法。invokevirtual
,如果是具体类型的目标对象, invokevirtual 用于调用公 共,受保护和打包私有方法。invokeinterface
,当要调用的方法属于某个接口时,将使用 invokeinterface 指令
与invokinterface
相比,invokevirtual
针对具体的类型方法表是固定的,所 以每次都可以精确查找,效率更高