Overview
了解字节码文件结构、运行时栈帧结构、JVM 怎么匹配多态方法。
简记一下字节码文件的结构的知识,为后面铺垫:
-
数据类型
无符号数:用来描述数字、引用、数量值或以 UTF-8 编码的字符串。表:由多个无符号数或其他表组成的复合数据类型。
Class 文件严格按照大端存储,且任何编码的 .java 源文件,编译时字符串都会转化为 utf-8 编码,但加载到 JVM 内部时,使用 utf-16 编码。参考
下面开始文件包含的信息:
- 控制信息:魔数、版本号等。
- 常量池:包括字面量和符号引用。作用主要是供 Class 文件中的其他结构所引用,字段表、方法表、属性表大量引用常量。
- 字面量:文本字符串、final 常量值。
- 符号引用:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符……
- 访问标志:常量池紧接着 2 Byte,用于描述类或接口的访问信息,如是否 public、final、abstract、注解、枚举……
- 类索引、父类索引、接口集合:用于确定类或接口的继承关系。
- 字段表集合:描述类或接口中声明的字段,包括类变量和实例变量,不包括方法内部声明的局部变量。除了字段的修饰符,如 public、static 等是固定的,其他像是字段的名称、数据类型等信息都是无法固定的,所以引用自常量池中的常量。
- 字段表可能会携带属性表集合,如记录类常量的初值,放在属性表集合的 ConstantValue 字段中。
- Class 文件中修饰符不同,名称相同也可以,但源码中不可。
- 方法表集合:用于记录类或接口中的方法信息。同字段表一样,包含访问标志、引用自常量池的名称索引、描述符索引、属性表集合等。
- 方法中的代码经编译后形成字节码指令,存放在属性表集合中一个名为 Code 的属性中。
- 属性表集合:用于记录一些其他附加的专有信息,供 Class 文件、字段表、方法表使用。他有很多字段属性,如前面提到的 ConstantValue 、Code 属性。
Class 文件中的常量池也称为静态常量池,类加载时会装入运行时常量池(在方法区/堆中);常量池后面的几个部分,组成了类结构信息,加载进方法区/元空间中。
栈帧
栈帧是虚拟机栈中的元素,一个方法的调用和返回对应一个栈帧的入栈和出栈。每个栈帧中包括:局部变量表、操作数栈、动态链接、方法返回地址、附加信息。其中局部变量表和操作数栈的大小编译时即可确定。

局部变量表
局部变量表 是一组变量值的存储空间,存放方法参数和局部变量。编译时即可确定局部变量表的大小,单位是变量槽,变量槽的大小取决于具体的虚拟机实现。
-
变量槽的复用可以影响垃圾回收。
当 PC 指针已经超出方法内某个变量的作用域时,即这个变量不会再被使用了,这时该变量所占用的变量槽就可以被复用。原先变量槽内可能是一个引用,被复用后,就会断裂,从而可以让 GC 早点回收,这也是为什么建议将方法内不用的大对象手工置 null 的原因。
-
为什么局部变量必须初始化。
因为局部变量不像类变量那样在类加载的准备阶段赋 ‘0’ 值,没人知道那块内存曾经放的是什么,所以局部变量必须初始化。
操作数栈
操作数栈 可以存放任意的 Java 数据类型,编译时即可确定大小,作用是传递参数。
动态连接
静态常量池中一部分符号引用在类加载的解析阶段,会被直接解析转换为直接引用,即静态解析。而还有一部分只能在运行期间转化为直接引用,即动态连接。
在字节码文件中,方法的调用体现在调用指令的参数就是静态常量池中的符号引用。转化为直接引用就是指转化为实际的运行时可知的某一个方法的入口地址。
静态解析就是编译时就能确定调用哪一个方法,从而在解析阶段可以直接把符号引用转化为直接引用。(非虚方法 + 重载)
动态连接就是只有运行时才能确定调用哪一个方法,故而不能在解析阶段直接转化为直接引用。(重写)
而栈帧中的动态连接字段,存放的是一个指向运行时常量池中当前方法的引用,为动态连接功能服务。
方法返回地址
返回地址就是方法正常返回时要执行的下一条字节码指令地址。方法返回的过程如下:
方法返回就是当前栈帧出栈,需要恢复调用者的局部变量表和操作数栈,如果有返回值,需要把返回值压入调用者的操作数栈,然后调整 PC 的值为返回地址。
如果发生异常,则转去执行异常处理程序,即给 PC 赋相应的异常处理程序的入口地址,若没有找到相应的异常处理程序,则方法会直接退出,不返回给调用者任何返回值信息。
方法调用
- 方法分类
Java 方法可以分为两种:非虚方法、虚方法。非虚方法包括:静态方法、私有方法、实例构造器、父类方法、final 方法。类加载的解析阶段就会把符号引用转化为直接引用。除此之外,其他方法都是虚方法。
其实判断依据很简单,可重写的方法就是虚方法,就得运行时动态绑定,即找到匹配的方法,得到它的直接引用。
不同方法的调用使用不同的字节码指令:
| 字节码指令 | 说明 |
|---|---|
| invokestatic | 调用静态方法 |
| invokespecial | 调用实例构造器方法、私有方法和父类中的方法 |
| invokevirtual | 调用所有的虚方法 |
| invokeinterface | 调用接口方法 |
| invokedynamic | 运行时动态解析出调用点限定符所引用的方法,然后再执行该方法 |
- 变量的类型
静态类型:创建对象时声明的类型,也叫外观类型,编译期可知,不可变。
实际类型:实际创建的类型,也叫运行时类型,编译期不可知,可变。
- 方法调用
非虚方法编译期即可确定,解析阶段即可找到方法的入口地址(直接引用),所以非虚方法的调用是直接执行即可。
虚方法的调用,说直白点就是重写方法怎么选,如 invokevirtual 字节码指令逻辑如下:
方法调用之前,会把调用者(引用)入栈。
- 根据栈顶引用找到方法的调用者,确定它的实际类型;
- 判断有没有与 invokevirtual 的参数所指向的常量池中的常量(方法名)匹配的方法,即判断有没有重写;
- 若找到则返回这个方法的引用,即运行时确定了直接引用;
- 否则,按照继承关系,逐层向上寻找;
即多态的本质就是,运行时根据调用者(接收者)的实际类型去选择合适的方法,返回其直接引用。达到不同的对象调用相同的方法,可以执行不同的逻辑的目的。实现在于相应字节码指令的执行逻辑。