第一章 JVM 概述
第二章 JVM 编译
第三章 JVM 类结构
第四章 JVM 类加载机制
JVM 编译
1. javac 编译 和 javap 查看 class 文件
本章描述的编译是指使用 JDK 自带的 java 工具把源代码编译成 class 文件,重点关注如何把 java 语言编译成 jvm 指令。
用户可以使用 javap 工具把二进制的 class 文件转换成用户可以看懂的 jvm 指令文本,来方便我们理解编译后的 class 文件,-p 参数会展示私有的方法的属性,-v 参数会展示尽可能多的信息。示例:javap -v -p HelloWorld。如果你使用了编译器 idea,可以使用 jclasslib 插件来查看 class 文件信息。
2. 指令格式
指令的格式:<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]
index 是指从方法开始的位置到当前指令的偏移量
opcode 是指操作码
operand 是指操作数,操作码的参数
comment 由编译器或者用户生成的注释
以下代码对常用的 Java 语法做了示例,可以通过方法名去找对应的字节码实现。字节码实现基本可以和方法一一对应。
3. 如何学习和理解编译后字节码
3.1 对象创建的例子
3.1.1 Java 代码
// 新建类实例、操作数栈相关指令
JavaCompiler compiler = new JavaCompiler();
3.1.2 编译后的使用 javap 查看的字节码
0: new #2 // class org/compiler/JavaCompiler
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
3.1.3 指令的执行
3.1.3.1 第一条指令 new
参照指令格式 0: new #2 // class org/compiler/JavaCompiler
,0 代表当前指令在方法中的偏移量是0,代表第一条指令,new
是操作符也就是是指令名称,表示当前指令用来创建对象实例,#2
是指令的操作数即参数,# 代表内容需要从字节码的常量池 Constant poll
中获取,常量池中 #2 对应的内容为 #76 对应的内容 org/compiler/JavaCompiler
代表类的全限定名,也就是要创建对象的类型。最后一列的 comment 也帮我们标明了实际的值。执行完这条指令,JVM 就会创建一个 JavaCompiler
实例,并把它放入操作数栈中。
Constant pool:
#1 = Methodref #24.#75 // java/lang/Object."<init>":()V
#2 = Class #76 // org/compiler/JavaCompiler
#76 = Utf8 org/compiler/JavaCompiler
3.1.3.2 第二条指令 dup
执行完第二条指令 dup
,在操作数栈中拷贝栈顶元素并推入栈内,此时本内有两个 this。
3.1.3.3 第三条指令 invokespecial
执行第三条指令 invokespecial #3
会调用对象的 init
方法并做初始化,需要从栈内弹出一个 this 参数,但是此方法无返回值,此时栈内还剩一个 this。
3.1.3.4 第四条指令 astore_1
执行第四条指令 astore_1
把弹出一个操作数栈元素,并保存在本地变量 1 中。
3.1.3.5 第五条指令 aload_1
执行第五条指令 aload_1
又把本地变量 1 的值推入操作数栈中供后面的方法使用。
4. 关键知识点
基础的指令知识请参考Compiling for the Java Virtual Machine
- 新建类实例的指令比构造方法中的指令多了一个
dup
指令,dup 是操作数栈的指令,它不需要关心数据的类型,作用是拷贝一份和当前栈顶元素一模一样的数据并推入栈内,作用是在对象初始化的时候为初始化后的 aload_n 提供 this 操作数,栈中 this 被 invokeSpecial init() 拿去执行,初始化方法没有返回值,如果不 dup 一个栈中就没有 this 了。 - 方法调用和静态方法调用的参数不一样,方法调用需要带上 this 作为第一个参数,参数个数和本地变量的个数都比静态方法多一个
- 同步关键字加在方法上,JVM 会在执行方法的 invoke 和 return 指令的时候判断方法上是否有标志 ACC_SYNCHRONIZED 并做相应的处理,确保方法不管是正常还是异常退出都能保证线程安全;同步代码块通过 moniterenter 和 monitorexist 指令实现,同时方法中会生成异常表,当方法中任务异常发生时都会执行成对的 monitorexit 指令来保证当前线程能从监视器中正常退出。
- switch 指令有两种实现方式,JVM 会对 switch 值稀疏的采用 lookuptable 实现方式(本例中使用此方式),对 switch 值紧凑的采用 tableswitch 实现方式。tableswitch 方式更高效,但是如果值稀疏字节码文件会很大。可以参考 Difference between JVM’s LookupSwitch and TableSwitch?。
5. 代表性的 Java 代码,覆盖了常用的语法
public class JavaCompiler {
private static final Integer LIMIT_100 = 100;
private static final String FINALLY = "finally";
public static void main(String[] args) {
try {
// 访问运行时常量池
// 新建类实例、操作数栈相关指令
JavaCompiler compiler = new JavaCompiler();
// 调用方法、接收参数
compiler.setCount(0);
// 同步代码块
int size = compiler.synchronizedBlockAddTwoInteger(compiler.addToLimit(), LIMIT_100);
// 数组
int[] array = new int[size];
final int mod = size / LIMIT_100;
int switchResult;
// switch
switch (mod) {
case 0: switchResult = 0; break;
case 1: switchResult = 1; break;
case 2: switchResult = 2; break;
case 5: switchResult = 5; break;
case 10: switchResult = 10; break;
case 20: switchResult = 20; break;
case 30: switchResult = 30; break;
default: switchResult = 99;
}
// 调用静态方法, 接收参数,同步方法
System.out.println("result:" + compiler.synchronizedAddTwoInteger(JavaCompiler.staticAddTwoInteger(switchResult,mod),LIMIT_100));
} catch (Exception e) {
// 异常处理指令
System.out.println(e.getMessage());
} finally {