JIT
是什么
JIT(Just in Time Compiler)即使编译器,为提升代码执行效率,JVM虚拟机会将热点代码(方法或者代码块)编译成机器码,并进行优化。
编译器
HotSpot虚拟机中内置有两个即时编译器,分别称为Client Compiler和Server Compiler,启动后会看到这两个线程!或者简称为C1编译器和C2编译器。目前主流的HotSpot虚拟机中,默认采用解释器与其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行模式,HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数指定虚拟机运行在Client模式或Server模式!
解释器和编译器
主流的商用虚拟机,如HotSpot,J9等,都同时包含解释器与编译器,解释器执行节约内存,反之可以使用编译执行来提升效率
虚拟机默认采用“混合模式”进行Java代码编译后执行,可以使用 -Xint优先采用解释器解释执行;使用-Xcomp 优先采用即时编译器编译执行,但是解释器仍然要在编译无法进行的情况下介入执行过程!
分层编译
由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间可能更长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行的速度也有影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启动分层编译的策略,分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:
-
第0层,程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译。
-
第1层,也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑;
-
第2层,也称为C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
实施分层编译后,Client Compiler和Server Compiler将同时工作,许多代码都可能会被多次编译,用Client Compiler获取更高的编译速度,用Server Compiler获取更好的编译质量,在解释执行的时候也无需再承担收集性能监控信息的任务!
热点对象
在运行过程中会被即时编译器编译的“热点代码”有两类:
1)被多次调用的方法;
2)被多次执行的循环体; 编译发生在方法执行的过程中,因此形象的称之为栈上替换(On Stack Replacement,简称OSR编译,即方法栈帧还在栈上,方法就被替换了)。
检测方法
基于采样的热点探测(Sample Based Hot Spot Detection) 周期性检测线程栈的栈顶,将经常出现在栈顶的方法作为热点方法。
基于计数器的热点探测(Counter Based Hot Spot Detection) 每个方法建立计数器,统计代码执行次数,超过一定阈值的作为热点方法。
Code Cache
code cache是什么
Code Cache代码缓存区 它主要用于存放JIT所编译的热点代码。CodeCache代码缓冲区的大小在client模式下默认最大是32m,在server模式下默认是48m,这个值也是可以设置的,
即时编译器(just-in-time,JIT)是代码缓存区的最大消费者,所以此区域又被开发者称为 JIT code cache。
它所对应的JVM参数为ReservedCodeCacheSize 和 InitialCodeCacheSize,可以通过如下的方式来为Java程序设置。
InitialCodeCacheSize – 初始大小, 默认值为 160KB
ReservedCodeCacheSize – 保留给Code Cache的空间, 也就是最大空间, 默认值: 48MB
CodeCacheExpansionSize – 每次扩充的大小, 一般为 32KB 或者 64KB
合理地增加 ReservedCodeCacheSize 是一种解决办法, 毕竟现在很多应用加上依赖库的代码量一点都不少。 但我们也不能无限制地增大这个区域的大小。
幸运的是,JVM提供了一个启动参数 UseCodeCacheFlushing, 用来控制Code Cache的刷新。 这个参数的默认值为 false。 如果将其开启(-XX:+UseCodeCacheFlushing),则会在满足以下条件时释放占用的区域:
-
code cache用满; 如果该区域的大小超过某个阈值,则会刷新。
-
自上次清理后经过了一定的时间间隔。
-
预编译的代码不够热。 对于每个JIT编译的方法,JVM都会有一个热度跟踪计数器。 如果计数器的值小于动态阈值,则JVM会释放这段预编译的代码。
Code Cache查看
想要监控代码缓存的使用情况,我们可以跟踪当前使用的内存大小。
指定JVM启动参数: –XX:+PrintCodeCache, 会打印Code Cache区的使用情况。 程序执行过程中, 我们可以看到类似下面的输出:
CodeCache: size=32768Kb used=542Kb max_used=542Kb free=32226Kb
size 表示此内存区域的最大值,与 ReservedCodeCacheSize 相等。 used 是此区域当前实际使用的内存大小。 max_used 是程序启动以来的历史最大使用量 free 是此区域尚未使用的空闲空间 PrintCodeCache 选项非常有用,可以帮助我们:
-
查看何时进行了刷新(flushing)
-
确定内存使用量是否达到关键点位
Code Cache分段
从Java 9开始,JVM将 Code Cache 细分为三个不同的段,每个段包含一种类型的编译代码。 具体是:
非方法段(non-method segment), 保存相关的JVM内部代码,例如字节码解释器。 默认情况下,此段约为 5 MB。 可通过 -XX:NonNMethodCodeHeapSize 参数进行调整。 待分析代码段(profiled-code segment), 包含经过简单优化的代码,使用寿命很短。 此段的大小默认为 122 MB,可以通过 -XX:ProfiledCodeHeapSize 参数进行调整。 静态代码段(non-profiled segment), 保存经过全面优化的本地代码,使用寿命可能很长。 默认大小同样是 122 MB。 可以通过-XX:NonProfiledCodeHeapSize 参数进行调整。 这种新的分段结构,以不同方式处理各种类型的编译代码,整体上具有更好的性能。
例如,将已编译的短命代码和长寿代码分开,提高方法清除器的性能 - 毕竟需要扫描的内存区域变小了。