上一节我们讲过JVM会用编译的形式为代码提速,在这个基础上,即时编译器还做了很多的优化措施,从而进一步的提升性能;这一节探讨的方法内联就是即时编译器的优化措施之一;
来看一下下面的代码
public class InlineTest1 {
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方法,大家想编译器会怎么优化这段代码呢;
我们知道,调用方法需要经过压栈和出栈的操作,进入方法的时候会向栈里面压入一个元素,return的时候就向栈里面弹出这个元素;
而压栈和出栈操作都是有开销的,压栈的时候会往栈里面存入数据,存在内存的开销,同时压栈和出栈也是都需要时间的,所以还有时间的开销;
如果上述代码调用的次数不多,那么压栈和出栈的开销就无所谓了,但是假设这段代码调用的非常频繁,比方说每秒要调用2万次,累计下来的开销还是比较可观的;
那么是不是有什么办法去优化呢?
JVM会自定识别热点方法,并自动的去进行方法内联;
编译器优化-方法内联
- 把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用
上述代码经过内联后的代码如下:
# 内联后
private static int addInline(int x1, int x2, int x3, int x4){
return x1 + x2 + x3 + x4;
}
JVM会自动把add1方法和add2方法合并到一起,从而减少压栈和出栈的操作
使用方法内联的条件
-
方法体足够小:如果方法体太大,JVM是不会内联的
- 热点方法:如果方法体小于325字节会尝试内联,可用-XX:FreqInlineSize修改大小
有关JVM如何寻找热点方法,上一节已经探讨过了 - 非热点方法:如果方法体小于35字节,会尝试内联,可用-XX:MaxInlineSize修改大小
- 热点方法:如果方法体小于325字节会尝试内联,可用-XX:FreqInlineSize修改大小
-
被调用方法运行时的实现可以被确定唯一
- static方法、private方法及final方法,JIT可以唯一确定具体的实现代码
- public的实例方法,指向的实现可能是自身、父类、子类的代码(因为有多态的存在),当且仅当JIT能够唯一确定方法的具体实现时,才有可能完成内联
根据上述两点内联条件,我们可以做出如下总结
方法内联注意点
- 尽量让方法体小一些
在写代码的时候,应当尽量避免在一个方法里面编写大量的代码,让方法体尽量小一点 - 尽量使用final、private、static关键字修饰方法,避免因为多态,而对方法做额外的类型检查
很可能在检查之后,方法还没有可能内联,因为没有办法唯一确定方法的实现 - 一些场景下,可通过修改JVM的参数,去减少热点的阈值,或者修改方法体的阈值,从而让更多方法内联
比如说:-XX:FreqInlineSize 和 -XX:MaxInlineSize
内联可能带来的问题
- CodeCache的溢出,导致JVM退化成解释执行模式
首先要强调的是,方法内联不是万能药,它也是有缺点的;
内联本质上是一种用空间换时间的玩法,也就是即时编译器在编译期间,把方法调用连接起来,从而减少进栈和出栈的开销,但是经过内联之后的代码会变多,而增加的代码量又取决于方法的调用次数以及方法本身的大小;
所以,在一些极端场景下,内联可能会导致 CodeCache 的溢出,CodeCache是热点代码的一个缓存区,即时编译器编译后的代码和本地方法代码都会存在CodeCache里面,这块空间是比较有限的,JDK8默认情况下只有240M,这块空间一旦溢出的话,甚至可能会导致JVM放弃编译运行,而退化成解释执行模式;
内联相关JVM参数
参数名 | 默认 | 说明 |
---|---|---|
-XX:+Printlnlining | - | 打印内联详情,该参数需和-XX:+UnlockDiagnosticVMOptions配合使用 |
-XX:+UnlockDiagnosticVMOptions | - | 打印JVM诊断相关的信息 |
-XX:MaxlnlineSize=n | 35 | 如果非热点方法的字节码超过该值,则无法内联,单位:字节 |
-XX:FreqlnlineSize=n | 325 | 如果热点方法的字节码超过该值,则无法内联,单位:字节 |
-XX:lnlineSmallCode=n | 1000 | 目标编译后生成的机器码开销大于该值,则无法内联,单位:字节 |
-XX:MaxlnlineLevel=n | 9 | 内联方法的最大调用帧数(嵌套调用的最大内联深度) |
-XX:MaxTrivialSize=n | 6 | 如果方法的字节码少于该值,则直接内联,单位:字节 |
-XX:MinlnliningThreshold=n | 250 | 如果目标方法的调用次数低于该值,则不内联 |
-XX:LiveNodeCountlnliningCutoff=n | 40000 | 编译过程中最大活动节点数(IR节点)的上限,仅对C2编译器有效 |
-XX:lnlineFrequencyCount=n | 100 | 如果方法的调用点(call site)的执行次数超过该值,则触发内联 |
-XX:MaxRecursivelnlineLevel=n | 1 | 递归调用大于这么多次就不内联 |
-XX:+lnlineSynchronizedMethods | 开启 | 是否允许内联同步方法 |