Java程序在运行的时候,主要就是执行字节码指令,一般这些指令会通过解释器(Interpreter)进行解释执行,这种就是解释执行。
当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为 热点代码。为了提高热点代码的执行效率,在运行时虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,简称 JIT 编译器)。
在HotSpot虚拟机中内置了两个即时编译器,分别称为Client Compiler和Server Compiler,或者简称 C1编译器 和 C2编译器。
C1编译器
一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序。开启的时间较早,应用启动后不久C1就开始进行编译
C2编译器
是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序。运行时间较晚,会等程序运行一段时候后才开始进行编译
无论采用的编译器是 C1 还是 C2,解释器与编译搭配使用的方式在虚拟机中称为“混合模式”。
可以通过参数 -Xint
强制虚拟机运行于“解释器模式”,这里编译器完全不介于工作,全部代码都使用解释方式执行。
还可以使用参数 -Xcomp
强制虚拟机运行于“编译器模式”,这时将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。
热点代码,就是那些被频繁调用的代码,这些会被编译进行被缓存,以备下次使用,但对于那些执行次数很少的代码来说,这种编译动作就纯属浪费。
JVM 提供了一个参数 -XX:ReservedCodeCacheSize
,用来限制 CodeCache 的大小,JIT编译后的代码都会放在 CodeCache 里。 JDK7默认值为 32m~48m,JDK8默认值为240m。
如果该空间不足,JIT就无法继续编译,编译执行会变成解释执行,性能就会较低。同时JIT编译器还会一直尝试去优化代码,从而造成了CPU占用上升。
判断一段代码是不是热点代码,是不是需要出发即时编译,这样的行为称为热点探测,主要的热点探测判定方式有两种,分别如下:
基于采样的热点探测: 采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。
缺点:基于采样的热点探测的好处就是实现简单、高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确地确认一个方法的热度,容易受到线程堵塞或别的外界因素的影响而扰乱热点探测。
基于计数器的热点探测: 采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。
缺点: 这种统计方法实现起来麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对来说更加精确和严谨。
HotSpot虚拟机就是基于第二种——基于计数器的热点探测,它为每个方法准备了两类计数器:方法调用计数器和回边计数器
用于统计方法被调用的次数,它默认的阈值在 Client 模式下是 1500 次,在 Server 模式下是 10000 次,这个阈值可以通过虚拟机参数 -XX:CompileThreshold
来人为设定。
当一个方法被调用时,会先检查该方法是否存在JIT编译过的版本,如果存在,则优先使用编译后的本地代码来执行,如果不存在已被编译过的版本,则将此方法的调用计数器加1,然后判断方法调用计数器与回边调用计数器之和是否超过方法调用计数器的阈值,如果已超过阈值,那么就会向即时编译器提交一个该方法的代码编译请求。
如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,及一段时间之内方法被调用的次数。当超过一定的时间限制,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半,这个过程被称为方法调用计数器热度的衰减。
对于上述所说的方法调用计数器热度的衰减,我们也是可以通过虚拟机参数 -XX:-UseCounterDecay
进行关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样只要系统运行时间足够长,绝大部分代码就会被编译器编译出本地代码。另外,还可以使用 -XX:CounterHalfLifeTime
参数来设置半衰周期的时间,单位是秒。
它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳的指令称为“回边”。与方法调用计数器不同的是,回边计数器没有计数热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数。
基于上述所说的 C1 和 C2 两种编译器的优缺点,虚拟机一般会启动分层编译的策略(开启分层编译参数:-XX:+TieredCompilation
),分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包括:
实施分层编译后,C1 和 C2 将会同时工作,许多代码都可能会被多次编译,用C1编译器获取更高的编译速度,用C2编译器来获取更多的编译质量。
方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。
JVM会自动识别热点方法,并对它们使用方法内联进行优化。但热点方法不一定会被JVM做内联优化,比如该方法体太大,JVM将不执行内联操作,默认情况下方法体小于 325 字节的都会进行内联,我们可以通过 -XX:FreqInlineSize=N
来设置大小值。
逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替。
将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量替换(前提是需要开启逃逸分析)。