Java中的方法内联
Java中的方法内联
1. 什么是方法内联
例如有下面的原始代码:
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.get();
// ...do stuff...
z = b.get();
sum = y + z;
}
我们首先要进行的就是方法内联,主要有下面两个目的:
- 去除方法调用的成本,如查找方法版本、建立栈帧。
- 为其他优化建立良好基础。
内联后代码如下:
public void foo() {
y = b.value;
// ...do stuff...
z = b.value;
sum = y + z;
}
后续,还可以进行冗余访问消除、复写传播、无用代码消除等优化操作。
2. 方法内联的重要性
方法内联是编译器最重要的优化手段,如果没有内联,多数其他优化都无法有效进行。例如下面这个例子:
public static void foo(Object obj){
if (obj != null) {
System.out.println("do something");
}
}
public static void testInline(String[] args) {
Object obj = null;
foo(obj);
}
testInline()
方法里其实全都是无用的代码,但是如果不做方法内联,就无法发现任何 Dead Code 的存在,因为分开看的话两个方法里面的操作可能都有意义。
3. Java中方法内联的困难
在 JVM 中,只有非虚方法,也就是使用invokespecial
指令调用的私有方法、实例构造器、父类方法和使用invokestatic
指令调用的静态方法才会在编译器进行解析。
而其他虚方法被invokevirtual
指令调用,在调用时必须进行方法接收者的多态选择。对于一个虚方法,编译器静态地去做内联的时候很难确定应该使用哪个方法版本,这就造成了方法内联的困难。
继承类型关系分析 CHA
首先,JVM 引入了一种名为类型继承关系分析 CHA 的技术,这种技术用于在已加载的类中,确定某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息。
编译器在进行内联时会分不同情况采取不同处理:
-
如果是非虚方法,那么就直接进行内联。
-
如果是虚方法,那么向 CHA 查询是否有多个目标版本可供选择。
-
如果只有一个版本,就直接内联,称为守护内联。但由于 Java 程序动态连接,不知道什么时候就会加载到新的类型而改变 CHA 的结论,所以要留好逃生门,假如程序后续执行中加载了导致继承关系发生变化的新类,那么必须抛弃已经编译的代码,退回到解释状态进行执行,或者重新编译。
-
如果有多个版本可供选择,那即时编译器使用内联缓存来缩减方法调用的开销。内联缓存是一个建立在目标方法正常入口之前的缓存。在未发生方法调用时,内联缓存为空。第一次调用发生后,缓存记录下方法接收者的版本信息,并且在每次进行调用前都检查版本。
- 如果每次调用的方法接收者版本是一样的,那称为单态内联缓存,通过缓存来调用,相比不内联只多了一次类型判断的开销。
- 如果出现方法接收者不一致的情况,就退化为超多态内联缓存,开销相当于真正查找虚方法表来进行方法分派。
- 当缓存未命中的时候,大多数JVM的实现时退化成超多态内联缓存,也有一些JVM选择重写单态内联缓存,就是更新缓存为新的版本。这样做的好处是以后还可能会命中,坏处是可能白白浪费一个写的开销。
-