方法内联
上篇文章中,和大家聊过JVM会用编译的方式为代码提速,那在这个基础上,即时编译器还做了很多的优化措施,从而进一步的提升性能,我们这次聊的方法内联就是即时编译器优化措施之一。
来看下这段代码:
private static int add1(int x1, int x2, int x3, int x4){
return add2(x1, x2) + add2(x3, x4);
}
private static int add2(int x1, int x2) {
return x1 + x2;
}
这段代码很简单,add1里面调用了两次add2方法,那么大家想一想,编译器怎样优化这段代码呢?
我们都知道,调用方法需要经过压栈出栈的操作,压栈出栈都是有开销的,比如说,压栈的话会向栈里存数据,所以存在内存的开销,同时呢,压栈和出栈都是需要消耗时间的,所以同时还有时间的开销,那么如果这段代码调用次数不多,那么压栈和出栈的开销倒也无所谓,但是假设,这段代码调用的非常频繁,比方说,每秒要调用两万次,那么累计下来的开销还是比较可观的,那么有什么办法去优化呢?
当然是有的,JVM会自动识别热点方法,并自动的进行方法的内联
所谓方法内联,就是把目标方法复制到发起调用的方法之中,避免发生真实的方法调用
比如上面那段代码,进行内联后就会变成这个样子:
private static int add1(int x1, int x2, int x3, int x4){
return add2(x1, x2) + add2(x3, x4);
}
private static int add2(int x1, int x2) {
return x1 + x2;
}
//内联后
private static int add1(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
也就是说,他会自动的把add1方法,和add2方法合并到一起去,从而减少压栈和出栈的操作。
那么需要注意的是,使用方法内联是有条件的:
- 方法体要足够小,如果方法体太大,JVM是不会内联的
- 热点方法: 如果方法体小于325字节会尝试内联,可用-XX:FreqInlineSize修改大小
- 非热点方法:如果方法体小于35字节,会尝试内联,可用-XX:MaxInlineSize修改大小
- 被调用方法运行时的实现可以被唯一确定
- static方法、private 方法及final方法,JIT可以唯一确定具体的实现代码。
- pubic的实例方法,指向的实现可能是自身、父类、子类的代码,当且仅当JIT能够唯一确定方法的具体实现时,才有可能完成内联。
那么我们根据这两点条件,可以总结出使用方法内联所注意的点:
- 写代码的时候,尽量让方法体小一些,避免在一个方法里面编写大量的代码。
- 尽量使用final、private、static关键字修饰方法,避免因为多态,需要对方法做额外的检查。
- 在某些场景下,可通过JVM参数修改阈值,从而让更多的方法内联。
下面我们探讨一下,使用方法内联可能带来的问题:
方法内联不是万能的,他本是也有缺点,方法内联本事就是一种用空间换时间的玩法,用即时编译器在编译期间,把方法调用连接起来,从而减少进栈和出栈的开销,但是呢,经过内联之后的代码会变多,而增加的代码量又取决于方法的调用次数,以及方法本身的大小,所以在一些极端的场景下,内联会出现很多问题:
比如:CodeCache 溢出,导致JVM退化成解释执行模式
内联相关JVM参数
参数名 | 默认 | 说明 |
---|---|---|
-XX:+PrintInlining | - | 打印内联详情,该参数需要和-XX:+UnlockDiagnosticVMOptions,且放在该参数之后 |
-XX:+UnlockDiagnosticVMOptions | - | 打印JVM诊断相关的信息 |
-XX:MaxInlineSize=n | 35 | 如果非热点方法的字节码超过该值,则无法内联,单位字节 |
-XX:FreqInlineSize=n | 325 | 如果热点方法的字节码超过该值,则无法内联,单位字节 |
-XXInlineSmallCode=n | 1000 | 目标编译后生成的机器码代销大于该值,则无法内联,单位字节 |
-XX:MaxInlineLevel=n | 9 | 内联方法的最大调用帧数(嵌套调用的最大内联深度) |
-XX:MaxTrivialSize=n | 6 | 如果方法的字节码小于该值,则直接内联,单位字节 |
-XX:MinInliningThreshold=n | 250 | 如果目标方法的调用次数低于该值,则不去内联 |
-XX:LiveNodeCountInliningCutoff=n | 40000 | 编译过程中最大活动节点数(IR节点)的上限,仅对C2编译器有效 |
-XX:InlineFrequencyCount=n | 100 | 如果方法的调用点(call site)的执行次数超过该值,则触发内联 |
-XX:MaxRecursiveInlineLevel=n | 1 | 递归调用大于那么多次就不内联 |
-XX:+InlineSynchronizedMethods | 开启 | 是否允许内联同步方法 |