JIT即时编译
即时编译(Just-In-Time Compilation, JIT)是一种强大的技术,旨在增强基于字节码的语言(如Java、.NET)的运行时性能。它的工作原理是在程序运行过程中动态地将频繁执行的字节码转换成本地机器码,从而大幅提高执行效率。这一过程克服了纯解释执行的性能瓶颈,同时保留了跨平台的灵活性。
HotSpot虚拟机的JIT实现
HotSpot是Java虚拟机的一个著名实现,它通过三种主要的即时编译器来平衡启动时间和运行时性能:
-
C1编译器(Client Compiler):设计用于快速启动和较小的内存占用,牺牲了一定的优化程度以换取更快的编译速度。适用于对启动时间敏感的应用场景。
-
C2编译器(Server Compiler):专注于代码的深度优化,尽管编译速度较慢,但能够产生高度优化的机器码,适合长时间运行且对运行时性能要求极高的服务端应用。
-
C1+C2混合模式(分层编译,Tiered Compilation):结合了C1和C2的优点,程序启动初期使用C1快速编译以加速启动,随着代码“热点”(即频繁执行的代码块)的识别,这些热点代码会被C2重新编译,以达到最佳的运行时性能。在Java 8之前,默认不启用分层编译,需手动添加
-server -XX:+TieredCompilation
参数开启。
逃逸分析及其优化
逃逸分析是JIT编译器中的一个高级特性,它分析对象的生命周期和作用域,判断对象是否“逃逸”出其创建的方法或线程,以此来决定是否可以采取进一步的优化措施。逃逸的两种情况包括:
- 方法逃逸:对象被作为参数传递给其他方法,其引用超出创建方法的范畴。
- 线程逃逸:对象被赋予了全局变量或类变量,有可能被其他线程访问。
当分析得知对象未发生逃逸时,可以执行以下优化:
- 同步消除:如果确定一个对象不会被多线程访问,那么针对该对象的同步操作(如加锁解锁)可以安全地被消除,减少不必要的性能开销。
- 标量替换:将对象拆解成若干个单独的变量(标量)直接存储在栈上,避免堆分配,提高访问速度和垃圾回收效率 通过
-XX:+EliminateAllocations
可以开启标量替换,-XX:+PrintEliminateAllocations
查看。 - 栈上分配:对于生命周期短且未逃逸的对象,直接在栈上分配内存,而非堆上,栈的分配速度快于堆,且对象生命周期结束时自动释放内存,减少了GC负担。
分层编译和逃逸分析在1.8中是默认是开启的
编译阈值与OSR编译解析
在即时编译(JIT)的背景下,编译阈值是一个关键参数,它决定了代码从解释执行过渡到编译执行的时机。这一机制确保了程序在频繁执行的“热点”代码上投资编译资源,以最大化性能提升,同时避免了对不常执行代码的过度优化,减少启动时间和内存占用。
标准编译阈值
-
Client与Server编译器的默认阈值:如前所述,HotSpot JVM中的Client编译器(C1)默认将编译阈值设为1500次方法调用或回边(循环)计数,而Server编译器(C2)则更为保守,将阈值设为10000次。这意味着在默认情况下,一个方法或循环必须被执行足够的次数后,才会被JVM认为是“热点”并进行编译优化。
-
调整CompileThreshold参数:通过JVM启动参数
-XX:CompileThreshold=<value>
,用户可以根据具体应用的需求调整这个阈值。减小该值可以让编译发生得更早,有助于快速提升性能,但可能会增加启动时间和内存使用;增大则相反,适用于那些启动时间敏感但运行时间较长的应用。
OSR(On Stack Replacement)编译
-
概念:OSR编译是即时编译技术中的一种特殊形式,旨在优化长时间运行的循环或方法体内部的代码,即使整个方法本身执行次数不多。它通过在方法栈帧上直接替换正在执行的字节码为优化后的本地代码,无需等待整个方法达到标准编译阈值。
-
触发阈值:OSR编译的触发条件相对复杂,不仅仅取决于执行次数,还与循环体内代码的复杂度、循环次数等因素相关。虽然没有直接的固定阈值供用户直接设置,但可以通过JVM参数间接影响,如
-XX:OnStackReplacePercentage=<percentage>
,这个参数指定了执行OSR编译的循环回边计数占标准编译阈值的比例。例如,若设置为93%,则当一个循环执行次数达到标准编译阈值的93%时,将会触发OSR编译。 -
公式估算:虽然具体的OSR阈值计算涉及JVM内部机制,不易直接给出一个通用公式,但大致思路是根据方法的实际运行情况(如循环次数)与上述百分比计算一个大致的触发点。理解OSR编译的核心在于认识到它是对特定代码段(通常是循环)的针对性优化,而非整个方法。
-XX:CompileThreshold = 10000
-XX:OnStackReplacePercentage = 140
-XX:InterpreterProfilePercentage = 33
OSR trigger = (CompileThreshold * (OnStackReplacePercentage -
InterpreterProfilePercentage)) / 100 = 10700
其中trigger即为OSR编译的阈值。
那么如果把CompileThreshold设置适当小一点,是不是可以提早触发编译行为,减少在堆上生成User
对象?我们可以进行通过不同参数验证一下:
- 1、 -XX:CompileThreshold = 5000 ,结果如下:
- 2、 -XX:CompileThreshold = 2500 ,结果如下:
- 3、 -XX:CompileThreshold = 2000 ,结果如下:
- 4、 -XX:CompileThreshold = 1500 ,结果如下:
在我的机器中,当设置到1500时,在堆上生成的User对象反而升到4w个,目前还不清楚原因是啥…
JIT编译在默认情况是异步进行的,当触发某方法或某代码块的优化时,先将其放入编译队列,然后由编
译线程进行编译,编译之后的代码放在CodeCache中,CodeCache的大小也是有限的,通过 -XX:-
BackgroundCompilation 参数可以关闭异步编译,我们可以通过执行 java -cp . -Xmx3G -Xmn2G -
server -XX:CompileThreshold=1 -XX:-TieredCompilation -XX:-BackgroundCompilation JVM 命
令看看同步编译的效果:在java堆上只生成了2个对象。
1、热点代码的编译过程是有成本的,如果逻辑复杂,编程成本更高;
2、编译后的代码会被存放在有大小限制的CodeCache中,如果CompileThreshold设置的太低,JIT会
将一大堆执行不那么频繁的代码进行编译,并放入CodeCache,导致之后真正执行频繁的代码没有足够
的空间存放;