Java即时编译器原理解析及实践( 八 )


由于编译情况复杂 , JVM也会动态调整相关的阈值来保证JVM的性能 , 所以不建议手动调整编译相关的参数 。 除非一些特定的Case , 比如codeCache满了停止了编译 , 可以适当增加codeCache大小 , 或者一些非常常用的方法 , 未被内联到 , 拖累了性能 , 可以调整内敛层数或者内联方法的大小来解决 。
2. 通过JITwatch分析编译日志
通过增加-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintInlining -XX:+PrintCodeCache -XX:+PrintCodeCacheOnCompilation -XX:+TraceClassLoading -XX:+LogCompilation -XX:LogFile=LogPath参数可以输出编译、内联、codeCache信息到文件 。 但是打印的编译日志多且复杂很难直接从其中得到信息 , 可以使用JITwatch的工具来分析编译日志 。 JITwatch首页的Open Log选中日志文件 , 点击Start就可以开始分析日志 。
Java即时编译器原理解析及实践文章插图
Java即时编译器原理解析及实践文章插图
如上图所示 , 区域1中是整个项目Java Class包括引入的第三方依赖;区域2是功能区Timeline以图形的形式展示JIT编译的时间轴 , Histo是直方图展示一些信息 , TopList里面是编译中产生的一些对象和数据的排序 , Cache是空闲codeCache空间 , NMethod是Native方法 , Threads是JIT编译的线程;区域3是JITwatch对日志分析结果的展示 , 其中Suggestions中会给出一些代码优化的建议 , 举个例子 , 如下图中:
Java即时编译器原理解析及实践文章插图
我们可以看到在调用ZipInputStream的read方法时 , 因为该方法没有被标记为热点方法 , 同时又“太大了” , 导致无法被内联到 。 使用-XX:CompileCommand中inline指令可以强制方法进行内联 , 不过还是建议谨慎使用 , 除非确定某个方法内联会带来不少的性能提升 , 否则不建议使用 , 并且过多使用对编译线程和codeCache都会带来不小的压力 。
区域3中的-Allocs和-Locks逃逸分析后JVM对代码做的优化 , 包括栈上分配、锁消除等 。
3. 使用Graal编译器
由于JVM会去根据当前的编译方法数和编译线程数对编译阈值进行动态的调整 , 所以实际服务中对这一部分的调整空间是不大的 , JVM做的已经足够多了 。
为了提升性能 , 在服务中尝试了最新的Graal编译器 。 只需要使用-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler就可以启动Graal编译器来代替C2编译器 , 并且响应C2的编译请求 , 不过要注意的是 , Graal编译器与ZGC不兼容 , 只能与G1搭配使用 。
前文有提到过 , Graal是一个用Java写的即时编译器 , 它从Java 9开始便被集成自JDK中 , 作为实验性质的即时编译器 。 Graal编译器就是脱身于GraalVM , GraalVM是一个高性能的、支持多种编程语言的执行环境 。 它既可以在传统的 OpenJDK上运行 , 也可以通过AOT(Ahead-Of-Time)编译成可执行文件单独运行 , 甚至可以集成至数据库中运行 。
前文提到过数次 , Graal的优化都基于某种假设(Assumption) 。 当假设出错的情况下 , Java虚拟机会借助去优化(Deoptimization)这项机制 , 从执行即时编译器生成的机器码切换回解释执行 , 在必要情况下 , 它甚至会废弃这份机器码 , 并在重新收集程序profile之后 , 再进行编译 。
这些中激进的手段使得Graal的峰值性能要好于C2 , 而且在Scale、Ruby这种语言Graal表现更加出色 , Twitter目前已经在服务中大量的使用Graal来提升性能 , 企业版的GraalVM使得Twitter服务性能提升了22% 。
使用Graal编译器后性能表现
在我们的线上服务中 , 启用Graal编译后 , TP9999从60ms -> 50ms, 下降10ms , 下降幅度达16.7% 。
运行过程中的峰值性能会更高 。 可以看出对于该服务 , Graal编译器带来了一定的性能提升 。
Graal编译器的问题
Graal编译器的优化方式更加激进 , 因此在启动时会进行更多的编译 , Graal编译器本身也需要被即时编译 , 所以服务刚启动时性能会比较差 。
考虑的解决办法:JDK 9开始提供工具jaotc , 同时GraalVM的Native Image都是可以通过静态编译 , 极大地提升服务的启动速度的方式 , 但是GraalVM会使用自己的垃圾回收 , 这是一种很原始的基于复制算法的垃圾回收 , 相比G1、ZGC(新一代垃圾回收器ZGC的探索与实践)这些优秀的新型垃圾回收器 , 它的性能并不好 。 同时GraalVM对Java的一些特性支持也不够 , 比如基于配置的支持 , 比如反射就需要把所有需要反射的类配置一个JSON文件 , 在大量使用反射的服务 , 这样的配置会是很大的工作量 。 我们也在做这方面的调研 。