Java即时编译器原理解析及实践( 五 )
2. 方法内联
方法内联 , 是指在编译过程中遇到方法调用时 , 将目标方法的方法体纳入编译范围之中 , 并取代原方法调用的优化手段 。 JIT大部分的优化都是在内联的基础上进行的 , 方法内联是即时编译器中非常重要的一环 。
Java服务中存在大量getter/setter方法 , 如果没有方法内联 , 在调用getter/setter时 , 程序执行时需要保存当前方法的执行位置 , 创建并压入用于getter/setter的栈帧、访问字段、弹出栈帧 , 最后再恢复当前方法的执行 。 内联了对 getter/setter的方法调用后 , 上述操作仅剩字段访问 。 在C2编译器 中 , 方法内联在解析字节码的过程中完成 。 当遇到方法调用字节码时 , 编译器将根据一些阈值参数决定是否需要内联当前方法的调用 。 如果需要内联 , 则开始解析目标方法的字节码 。 比如下面这个示例(来源于网络):
方法内联的过程
public static boolean flag = true;public static int value0 = 0;public static int value1 = 1;public static int foo(int value) {int result = bar(flag);if (result != 0) {return result;} else {return value;}}public static int bar(boolean flag) {return flag ? value0 : value1;}
bar方法的IR图:
文章插图
内联后的IR图:
文章插图
内联不仅将被调用方法的IR图节点复制到调用者方法的IR图中 , 还要完成其他操作 。
- 被调用方法的参数替换为调用者方法进行方法调用时所传入参数 。 上面例子中 , 将bar方法中的1号P(0)节点替换为foo方法3号LoadField节点 。
- 调用者方法的IR图中 , 方法调用节点的数据依赖会变成被调用方法的返回 。 如果存在多个返回节点 , 会生成一个Phi节点 , 将这些返回值聚合起来 , 并作为原方法调用节点的替换对象 。 图中就是将8号==节点 , 以及12号Return节点连接到原5号Invoke节点的边 , 然后指向新生成的24号Phi节点中 。
- 如果被调用方法将抛出某种类型的异常 , 而调用者方法恰好有该异常类型的处理器 , 并且该异常处理器覆盖这一方法调用 , 那么即时编译器需要将被调用方法抛出异常的路径 , 与调用者方法的异常处理器相连接 。
编译器的大部分优化都是在方法内联的基础上 。 所以一般来说 , 内联的方法越多 , 生成代码的执行效率越高 。 但是对于即时编译器来说 , 内联的方法越多 , 编译时间也就越长 , 程序达到峰值性能的时刻也就比较晚 。
可以通过虚拟机参数-XX:MaxInlineLevel调整内联的层数 , 以及1层的直接递归调用(可以通过虚拟机参数-XX:MaxRecursiveInlineLevel调整) 。 一些常见的内联相关的参数如下表所示:
文章插图
虚函数内联
内联是JIT提升性能的主要手段 , 但是虚函数使得内联是很难的 , 因为在内联阶段并不知道他们会调用哪个方法 。 例如 , 我们有一个数据处理的接口 , 这个接口中的一个方法有三种实现add、sub和multi , JVM是通过保存虚函数表Virtual Method Table(以下称为VMT)存储class对象中所有的虚函数 , class的实例对象保存着一个VMT的指针 , 程序运行时首先加载实例对象 , 然后通过实例对象找到VMT , 通过VMT找到对应方法的地址 , 所以虚函数的调用比直接指向方法地址的classic call性能上会差一些 。 很不幸的是 , Java中所有非私有的成员函数的调用都是虚调用 。
C2编译器已经足够智能 , 能够检测这种情况并会对虚调用进行优化 。 比如下面这段代码例子:
virtual call
public class SimpleInliningTest{public static void main(String[] args) throws InterruptedException {VirtualInvokeTest obj = new VirtualInvokeTest();VirtualInvoke1 obj1 = new VirtualInvoke1();for (int i = 0; i < 100000; i++) {invokeMethod(obj);invokeMethod(obj1);}Thread.sleep(1000);}public static void invokeMethod(VirtualInvokeTest obj) {obj.methodCall();}private static class VirtualInvokeTest {public void methodCall() {System.out.println("virtual call");}}private static class VirtualInvoke1 extends VirtualInvokeTest {@Overridepublic void methodCall() {super.methodCall();}}}
经过JIT编译器优化后 , 进行反汇编得到下面这段汇编代码: 0x0000000113369d37: callq0x00000001132950a0; OopMap{off=476};*invokevirtual methodCall//代表虚调用; - SimpleInliningTest::invokeMethod@1 (line 18);{optimized virtual_call}//虚调用已经被优化
可以看到JIT对methodCall方法进行了虚调用优化optimized virtual_call 。 经过优化后的方法可以被内联 。 但是C2编译器的能力有限 , 对于多个实现方法的虚调用就“无能为力”了 。
- 现状|程序员现状揭秘:平均年薪20.36万,Java人才需求量最大
- 工程师|AWS偏爱Rust,已将Rust编译器团队负责人收入囊中
- 程序员学英语第1天——JavaScript 程序测试的介绍1
- 三年Java开发,刚从美团、京东、阿里面试归来,分享个人面经
- 《深入理解Java虚拟机》:对象创建、布局和访问全过程
- java面试题整理
- Kotlin集合vs Kotlin序列与Java流
- Java安全之Javassist动态编程
- 推荐Java工程师必看,12个Hadoop领域的上手项目
- 震惊!京东T4大佬面试整整三个月,才写了两份java面试笔记