【Java核心-性能基础】JVM 对代码执行的优化
1. JVM 对代码执行的两类优化
1.1 运行时优化
主要是针对解释执行和动态编译的一些通用机制的优化。如,锁(包括偏斜锁)和内存分配(如 TLAB)。
还包括一些专门优化解释执行效率的机制。如,模板解释器、内联缓存(优化虚方法调用的动态绑定)。
1.2 JIT优化
JVM根据运行时统计信息(Profile),动态决定部分方法(热点代码)被编译成机器码,直接运行在底层硬件上。
JIT 编译器的技术有 方法计数器、循环展开(Loop unrolling)、方法内联、无效代码消除(Dead Code Elimination)、逃逸分析、分支预测(Branch Prediction)等
这些编译所得的机器码会被暂存于Code Cache。
因为Code Cache容量有限,所以当热点代码不再是热点时,其对应的本地代码会被从Code Cache中移除。这就是 逆优化。
栈上替换技术(OSR,On-Stack Replacement)
针对热点循环代码,如果方法本身的调用频度还未达到编译标准,但是内部有大的循环,那么该方法还是会被判定为有优化价值而被编译。
2. 探查JVM优化的具体情况
针对Java编译期优化,我们可以通过反编译工具查看被优化的细节(如,常量折叠)
针对JVM运行期间的优化,我们一般通过设置启动参数让JVM输出相关日志,或一些专门的监控工具来查看相关细节。(《JVM内存监控与诊断》)
2.1 打印编译发生的细节
-XX:+PrintCompilation
输出更多编译细节:
-XX:UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile=<your_file_path>
2.2 打印内联发生的细节
-XX:PrintInlining
2.3 查看 Code Cache 的使用情况
JMC、JConsole等工具
3. JVM 代码执行优化的调优角度和手段
3.1 调整热点代码门限值
-XX:CompileThreshold=N
通常是降低该值,以降低预热时间。
server模式默认10000次,client模式默认1500次
此外,JVM 会周期性的对方法计数的数值进行衰减,从而导致某些代码永远都达不到默认的热点代码门限值。所以降低降低该门限值可以让这些代码有机会成为热点。
当然也可以关闭计数衰减 -XX:-UseCounterDecay
3.2 调整 Code Cache 大小
-XX:ReservedCodeCacheSize=<SIZE>
通常是增大其容量,以容纳更多编译后的代码。
调整其初始容量:
-XX:InitialCodeCacheSize=<SIZE>
3.3 调整编译器线程数,或选择适当的编译器模式
JVM的编译器线程数与JVM的模式有关。
client模式默认只有一个编译线程(C1,Client Compiler)
server模式默认是两个(C2,Sever Compiler)
HotSpot 默认启用了分层编译(Tiered-Compilation),JVM会根据CPU内核数计算C1 和 C2 的数值。
可以通过启动参数指定编译线程数。-XX:CICompilerCount=N
如果CPU核数多资源足,可以增大编译线程数,以充分利用资源,加快相关优化执行过程。
如果CPU资源少,或系统中有多个应用在争抢资源,则可以考虑减少编译线程数,降低线程切换的开销。
但是,通常可以在服务器上关闭分层编译(-XX:-TieredCompilation)。虽然预热速度稍慢,但省下的CPU资源可能提高某些业务场景的吞吐量。
“优化”没那么高大上,就怕瞎捣腾,毫无章法地胡乱设置一通浪费时间。
其实JVM已经优秀到足以应付绝大多数场景了。通常都无需这么冷门艰涩的调优手段。
性能差往往是因为业务代码本身有逻辑硬伤,或是服务器性能(包括网络环境)真的不行。
绝大多数Java项目组织根本没资格在这个层面拼优化,做好需求、设计和工程管理就已经有资格评优了。