在编译过程中遇到方法调用时,就是将目标方法的方法体 / 代码块纳入编译范围之中,并取代原方法调用的优化手段,是JVM 最重要的编译器优化,没有之一
optimize before
static class A{
int value;
final int get(){
return value;
}
}
public void foo(){
y = b.get();
// do stuff..
z = b.get();
sum = y + z;
}
optimize after
static class A{
int value;
final int get(){
return value;
}
}
public void foo(){
y = b.value;
// do stuff..
z = b.value;
sum = y + z;
}
可以看到代码中函数的调用转换成了变量访问,去除了方法调用的成本
方法调用:查找函数的位置 -> 压入栈帧 -> 访问变量 -> 恢复执行
类型继承分析 / Class Hierarchy Analysis(CHA)
解决 虚方法 无法轻易在静态编译阶段寻找到到具体的调用函数
通过整个应用程序范围查的类型分析技术,实现判断目前已经加载类中的方法,某个接口是否包含多种实现;某个子类是否覆盖父类的函数;
激进优化 / 完全去虚化
如果不是虚方法,那么可以直接内联就好了,不用有任何担心,它绝对是安全的。如果是虚方法那么向 CHA 查询有多少个实现,如果只有一个,那么可以直接内联该实现函数,我们称之为守护内联(Guarded Inlining)
条件去虚化
退路 / 去优化
当 CHA 发现虚方法只有一种实现时,直接关联实现函数,是过于激进的。我们需要解决 JAVA 程序在运行时,加载了新类型时,虚方法有多种实现的问题
退路就是,假设程序加载了导致继承关系发生变化的新类,那么就必须抛弃已经变异的守护内联,退回到解释执行,或者重新进行即时编译。
方法内联的条件
方法内联能够触发更多的优化。通常而言,内联越多,生成代码的执行效率越高。然而,对于即时编译器来说,内联越多,编译时间也就越长,而程序达到峰值性能的时刻也将被推迟。
此外,内联越多也将导致生成的机器码越长。在 Java 虚拟机里,编译生成的机器码会被部署到 Code Cache 之中。这个 Code Cache 是有大小限制的(由 Java 虚拟机参数 -XX:ReservedCodeCacheSize 控制)。
因此,即时编译器不会无限制地进行方法内联。下面我便列举即时编译器的部分内联规则。(其他的特殊规则,如自动拆箱总会被内联、Throwable 类的方法不能被其他类中的方法所内联,你可以直接参考JDK 的源代码。)
-
-XX:CompileCommand 中的 inline 指令指定的方法,以及由 @ForceInline 注解的方法(仅限于 JDK 内部方法),会被强制内联
-
-XX:CompileCommand 中的 dontinline 指令或 exclude 指令(表示不编译)指定的方法,以及由 @DontInline 注解的方法(仅限于 JDK 内部方法),始终不会被内联
-
如果调用字节码对应的符号引用未被解析、目标方法所在的类未被初始化,或者目标方法是 native 方法,都将导致方法调用无法内联。
-
C2 不支持内联超过 9 层的调用(可以通过虚拟机参数 -XX:MaxInlineLevel 调整),以及 1 层的直接递归调用(可以通过虚拟机参数 -XX:MaxRecursiveInlineLevel 调整)。
如果方法 a 调用了方法 b,而方法 b 调用了方法 c,那么我们称 b 为 a 的 1 层调用,而 c 为 a 的 2 层调用。
- 即时编译器将根据方法调用指令所在的程序路径的热度,目标方法的调用次数及大小,以及当前 IR 图的大小来决定方法调用能否被内联。