java 文件经过javac编译后,变成了存储了一系列指令的.class文件。本文从指令层面分析Java 方法从解析、调用到执行的过程。
1 指令
一般格式:操作码 [操作数1] [操作数2] ...
操作码 | 1个字节的无符号整数(范围:0x00 ~ 0xFF)。 特点:1)每个操作码对应一个助记符(如iconst_1,iload,iadd等)。2)操作码决定了操作数类型及个数。 |
操作数 | 操作码所需的参数,紧跟在操作码后面。 |
表 指令的组成
1.1 未对齐的操作数
操作数的基本单位是字节。为了让.class文件更紧凑,jvm没有让操作数对齐。这意味着JVM需要逐个字节读取。
例如指令:0x11 0x03 0xE8 (sipush 1000)
0x11 是操作码,它的助记符是sipush。这个操作码的参数是1个2字节长度的操作数。
jvm 先读取第1个字节0x03,然后读取第2个字节 0xE8,最后将这两个字节组合:
(0x03 << 8) | 0xE8 => 0x03E8。
1.2 指令的执行模型
不考虑异常处理的话,JVM的解释器解析.class文件中的指令伪代码如下:
do {PC 寄存器值++;根据PC寄存器指示的位置,读取操作码;if (操作码需要操作数) 读取操作数;执行操作码所定义的操作;
} while(字节流长度 > 0);
1.3 方法相关指令
invokestatic | 静态方法。 |
invokespecial | 需要特殊处理的实例方法,包括实例初始化、私有方法、和super调用。 |
invokevirtual | 虚方法分派(可被重写的方法及final方法)。 |
invokeinterface | 接口方法,在运行期间再确定一个实现该接口的对象。 |
invokedynamic | 先在运行时动态解析调用点限定符所引用的方法,然后再执行该方法。Java 7 引入,用于支持动态语言特性,比如Lambda |
表 调用方法的指令
方法调用指令和数据类型无关,而方法返回指令根据返回值类型区分,包括ireturn、lreturn、freturn、dreturn、areturn(返回值为对象、数组等引用类型)及return(返回值为void)。
1.3.1 类与实例的初始化
<clinit> | 类(或接口)的静态初始化方法。用于执行静态变量的赋值和静态代码块。 如果父类未初始化,会先触发父类的clinit方法,但接口的clinit不会因为实现类的初始化而触发(需要直接使用接口的静态变量才触发)。 |
<init> | 对象的初始化方法,用于实例变量的赋值、实例代码块及构造器。 每个构造器对应一个init方法;子类构造器会隐式调用父类的init方法。 |
表 类与实例初始化方法
<clinit> 与<init>方法都是由编译器自动生成,用户无法调用。
2 方法调用
java 是一门静态多分派(和接收者及参数有关)、动态单分派(只能接收者有关)的语言。
静态分派:方法的静态类型在编译阶段是可知的(如方法重载,取决于参数类型、数量及位置)。
动态分派:运行时类型要在运行期才可知(方法重写,取决于执行对象的实际类型)。
2.1 虚方法表
JVM 在类初始化过程中,会为这个类维护一个虚拟方法表,存储该类所有可被重写的方法的入口地址。
虚方法表可理解为一个数组,每个数组元素(槽位)存储的是方法的入口地址。
虚方法表创建步骤如下:
1)父类的方法按声明顺序占据虚方法表的固定槽位。
2)子类继承父类的虚方法表,并保留父类方法的地址。
3)如果子类重写了父类的方法,子类的虚方法中对应的槽位会被替换为子类方法的地址。
4)子类新增的方法追加到虚方法表的末尾。
例如 Animal类又两个可重写的方法sound和eat,Dog类继承Animal类,并重写了eat方法,又新增了一个方法wagTail。则这两个类的虚方法表如下。
Animal类的虚方法表 | Dog类的虚方法表 | ||
索引 | 方法地址 | 索引 | 方法地址 |
0 | Animal.sound() | 0 | Animal.sound() |
1 | Animal.eat() | 1 | Dog.eat() |
2 | Dog.wagTail() |
表 Animal 与 Dog类的虚方法表
2.1.1 动态分派过程
当父类引用调用方法时,JVM执行步骤如下:
- 获取对象的实际类型。
- 根据对象的实际类型查找对应的虚方法表。
- 根据方法调用指令的操作数(虚方法表的索引),找到方法入口地址。
- 执行对应方法。