Java虚拟机 系列文章
Java虚拟机1 内存管理、GC,包括 Shenandoah ZGC
Java虚拟机2 G1垃圾回收详解, 参数, 日志
Java虚拟机3 Class文件及类加载
Java虚拟机4 方法调用原理、动态类型支持 (本文)
Java虚拟机5 编译与优化
Java虚拟机6 内存模型、线程、锁
总结 Java 不支持的语法特性
Java 协程:Loom Project 实战
其他JVM语言
栈帧
栈帧是方法执行的基本数据结构
栈帧结构包括以下几部分:
-
局部变量表(Local Variables Table)
存放方法参数和方法内部定义的局部变量,长度在编译时确定 -
操作栈(Operand Stack)
用于算数运算、或调用其它方法时传递参数(下图8-2)和返回值 -
动态连接
指向运行时常量池[插图]中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接 -
方法返回地址
保存调用者的程序计数器,用于方法正常退出后恢复调用者的状态 -
附加信息
如调式、性能收集相关信息
方法调用
Java 虚拟机支持以下5条方法调用指令:
- invokestatic 用于调用静态方法
- invokespecial 用于调用实例构造器方法、私有方法和父类中的方法
- invokevirtual 用于调用所有的虚方法
- invokeinterface 用于调用接口方法,会在运行时再确定一个实现该接口的对象
- invokedynamic 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法
解析(Resolution)调用:指调用非虚方法,包括静态方法、私有方法、构造器、父类方法、final 方法5中,会在类加载时将符号引用解析为直接引用。
分派(Dispatch)调用:分为静态分派、动态分派
-
静态分派
依赖静态类型来决定方法执行版本,用于方法重载(Overload) -
动态分派
运行时根据动态类型决定方法执行版本,用于方法重写(Override)以及实现多态。
动态分派实现方式
为每个类在方法区建立一个虚方法表。虚方法表一般在类加载的连接阶段进行初始化。
除了虚方法表,虚拟机还使用类型继承关系分析(Class Hierarchy Analysis,CHA)、守护内联(Guarded Inlining)、内联缓存(Inline Cache)等方式来优化性能。
反射(reflect)调用
位于 java.lang.reflect 包下,getDeclaredMethod() 为获取类声明的方法。
import java.lang.reflect.Method;
public class Calculator {
public static void main(String[] args) throws Throwable {
Calculator calculator = new Calculator();
// 后两个参数为 sum 方法参数类型
Method method = Calculator.class.getDeclaredMethod("sum", int.class, int.class);
int total = (int) method.invoke(calculator, 3, 5);
System.out.println(total); // 8
}
public int sum(int a, int b) {
return a + b;
}
}
invoke调用
位于 java.lang.invoke 包下,提供一种方法句柄(MethodHandle)的调用方式,类似于函数指针。
和反射的区别是,反射是模拟 Java 语言层面的方法调用,拥有方法的完整信息,包括方法名、注解等。而 MethodHandle 是模拟字节码层面的方法调用,仅包含执行方法的信息,且性能更高一些。
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class Calculator {
public static void main(String[] args) throws Throwable {
Calculator calculator = new Calculator();
// 第一个参数为返回值类型,之后为参数类型
MethodType mt = MethodType.methodType(int.class, int.class, int.class);
// findVirtual 对应 invokevirtual 指令
MethodHandle sum = MethodHandles.lookup().findVirtual(Calculator.class, "sum", mt).bindTo(calculator);
int total = (int) sum.invokeExact(3, 5);
System.out.println(total); // 8
}
public int sum(int a, int b) {
return a + b;
}
}
动态类型语言
动态类型语言的类型检查在运行期而不是编译期,如 JavaScript、Python、PHP。
为了使Java虚拟机更好的支持动态类型语言,JDK 7 引入了 invokedynamic 指令。
在 Java 语言中,invokedynamic 用于实现 lambda 表达式,详见:
http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html
比如
List<Integer> list = ...
list.sort((a, b) -> a - b);
生成字节码为:
INVOKEDYNAMIC compare()Ljava/util/Comparator; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
(Ljava/lang/Object;Ljava/lang/Object;)I,
// handle kind 0x6 : INVOKESTATIC
Obj2.lambda$sort$0(Ljava/lang/Integer;Ljava/lang/Integer;)I,
(Ljava/lang/Integer;Ljava/lang/Integer;)I
]
invokedynamic 指令的第一个参数不再是方法符号引用,而是 CONSTANT_InvokeDynamic_info 常量,从这个新常量中可以得到3项信息:
- 引导方法(Bootstrap Method),返回值是java.lang.invoke.CallSite对象,这个对象代表了真正要执行的目标方法调用
- 方法类型(MethodType)
- 名称
根据 CONSTANT_InvokeDynamic_info 常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个 CallSite 对象,最终调用到要执行的目标方法上。
字节码执行
执行Java代码一般有解释执行和编译执行两种方式。
中间一行代表解释执行过程,下边一行代表编译执行过程。
Javac 编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。
指令集
Java字节码是基于栈的指令集架构(不完全是),而与之对应的是基于寄存器的指令集架构,二者区别如下:
- 基于栈的指令集
iconst_1
iconst_1
iadd
istore_0
两条iconst_1指令连续把两个常量1压入栈后,iadd指令把栈顶的两个值出栈、相加,然后把结果放回栈顶,最后istore_0把栈顶的值放到局部变量表的第0个变量槽中。这种指令通常不带参数。
- 基于寄存器的指令集
mov eax, 1
add eax, 1
mov指令把EAX寄存器的值设为1,然后add指令再把这个值加1,结果就保存在EAX寄存器里面。这种二地址指令是x86指令集中的主流。