赞
踩
将字节码load到虚拟机中的过程称为类的加载
解释:输入程序代码–>得到结果
编译:输入程序代码–>得到可执行代码–>执行可执行的代码得到结果
JVM在执行代码的时候并不立即编译代码,主要有个原因:
1.有些代码可能执行频率比较低,甚至就只运行一次,这种情况下,将代码翻译成java字节码比编译这段代码并运行来说要快得多。
2.当 JVM 执行某一方法或遍历循环的次数越多,就会更加了解代码结构,那么 JVM 在编译代码的时候就做出相应的优化。
在主流商用JVM(HotSpot、J9)中,Java程序一开始是通过解释器(Interpreter)进行解释执行的。当JVM发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“热点代码(Hot Spot Code)”,然后JVM会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为:即时编译器(Just In Time Compiler,JIT)
1、混合模式(Mixed Mode)
JIT编译器(无论C1还是C2)与解释器配合工作的方式;
这是默认的方式,也可通过“-Xmixed”参数设定;
2、解释模式(Interpreted Mode)
全部代码由解释器解释执行,JIT编译器不介入工作;
可以通过“-Xint”参数设定;
3、编译模式(Compiler Mode)
优先采用编译方式执行程序,但解释器仍要在编译无法时行时介入执行过程;
可以通过“-Xcomp”参数设定;
该参数强调的是首次调用方法时执行编译,并不是不用解释器;
一般情况下(不开启分层编译),一个方法需要解释执行一定次数后才编译;
JDK8作为默认开启分层编译策略;
可以通过java -version来查看工作模式
Java 程序最初是仅仅通过解释器解释执行的,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其当某个方法或代码块运行的特别频繁时,这种方式的执行效率就显得很低。于是后来在虚拟机中引入了 JIT 编译器(即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是 JIT 编译器。
HotSpot虚拟机内置两个即时编译器,分别Client Compiler和Server Comiler,如下:
1、Client Compiler
简称C1编译器;
(A)、应用特点
较为轻量,只做少量性能开销比较高的优化,它占用内存较少,适合于桌面交互式应用;
(B)、优化技术
它是一个简单快速的三段式编译器;
主要关注点在于局部性的优化,而放弃了许多耗时较长的全局优化;
在寄存器分配策略上,JDK6以后采用的为线性扫描寄存器分配算法,其他方面的优化,主要有方法内联、去虚拟化、冗余消除等;
(C)、设置参数
可以使用"-client"参数强制选择运行在Client模式(Client VM);
(D)、编译过程
它是一个简单快速的三段式编译器,主要关注点在于局部性的优化,而放弃了许多耗时较长的全局优化;
三段式编译过程如下
(1)、在字节码上进行一些基础优化,如方法内联、常量传播等;
然后将字节码构造成一种高级中间代码表示(High-Level Intermediate Representaion,HIR);
HIR使用静态单分配(SSA)的形式表示代码值;
(2)、在HIR基础上再次进行一些优化,空值检查消除、范围检查消除等;
然后将HIR转换为LIR(低级中间代码表示)
(3)、在LIR基础上分配寄存器、做窥孔优化,然后生成机器码;
2、Server Compiler
简称C2编译器,也叫Opto编译器;
(A)、应用特点
较为重量,采用了大量传统编译优化的技巧来进行优化,占用内存相对多一些,适合服务器端的应用;
(B)、优化技术
它会执行所有经典的优化动作,如无用代码消除、循环展开、循环表达式外提、消除公表达式、常量传播、基本块重排序等;
还会一些与Java语言特性密切相关的优化技术,如范围检查消除、空值检查消除等;
另外,还进行一些不稳定的激进优化,如守护内联、分支频率预测等;
(C)、收集性能信息
由于C2会收集程序运行信息,因此其优化范围更多在于全局优化,不仅仅是一个方块的优化;
收集的信息主要有:分支的跳转/不跳转的频率、某条指令上出现过的类型、是否出现过空值、是否出现过异常等。
(D)、与C1的不同点
和C1的不同主要在于寄存器分配策略及优化范围,寄存器分配策略上C2采用传统的全局图着色寄存器分配算法;
C2编译速度较为缓慢,但远远超过传统的静态优化编译器;
而且编译输出的代码质量高,可以减少本地代码的执行时间;
(E)、设置参数
可以使用"-server"参数强制选择运行在Server模式(Server VM);
为了在程序启动响应速度与运行效率之间达到最佳平衡,会启用分层编译(Tiered Compilation)策略;
1、编译层次
根据编译器编译、优化的规模与耗时,划分出不同的编译层次,包括:
(I)、第0层
程序解释执行,解释器不开启性能监控功能(Profiling),可触发第1层编译;
(II)、第1层
也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要加入性能监控的逻辑;
(III)、第2层
也称为C2编译,也是将字节码编译为本地代码,但进行一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化;
2、优点
这时C1和C2同时进行工作,许多代码都可能被编译多次;
用C1获取更高的编译速度,用C2获取更好的编译质量;
解释器执行时也无须再承担收集性能监控信息的任务(如果不开启分层编译,又工作在Server模式,解释器提供监控信息给C2使用);
最终在程序启动响应速度与运行效率之间达到最佳平衡;
3、设置参数
JDK6开始出现,需要“-XX:+TieredCompilation”指定开启;
JDK8作为默认的策略,可以通过“-XX:-TieredCompilation”关闭策略;
注意,只能在Server模式下使用;
上边说只有热点代码才会被编译成机器码,什么样的代码会认为是热点代码?达到什么样的标准就会被认为是热点代码呢?
JIT编译对象为"热点代码",包括两类:
1、被多次调用的方法
由方法调用触发的编译,以整个方法体为编译对象;
JVM中标准的的JIT编译方式;
2、被多次执行的循环体
由循环体触发,仍然以整个个方法体为编译对象;
发生在方法执行过程中,方法栈帧还在栈上,方法就被替换;
称为栈上替换(On Stack Replacement),简称OSR编译;
1.基于采样的热点探测
采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这段方法代码就是“热点代码”。
优点:这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系
缺点:很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测
2.基于计数器的热点探测
采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。
优点:更加精确和严谨
缺点:这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系
在 HotSpot 虚拟机中使用的是第二种——基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器
方法调用计数器
方法调用计数器用来统计方法调用的次数,在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。
回边计数器
回边计数器用于统计一个方法中循环体代码执行的次数(准确地说,应该是回边的次数,因为并非所有的循环都是回边),在字节码中遇到控制流向后跳转的指令就称为“回边”。
阈值设置
方法调用计数器
默认C1时为1500次(sparc平台才是1000),C2时为10000次;
可以通过"-XX:CompileThreshold"参数设定;
启用分层编译时将忽略此选项,请参阅选项"-XX:+ TieredCompilation";
回边计数器
C1:
计算规则:方法调用计数器阈值(CompileThreshold)*OSR比率(OnStackReplacePercentage)/100;
默认:OnStackReplacePercentage=933, CompileThreshold=1500,计算阈值为14895;
C2:
前面介绍分层编译时曾说:如果不开启分层编译,又工作在Server模式,解释器提供监控信息给C2使用,所以多了个解释器监控比率(InterpreterProfilePercentage);
计算规则:CompileThreshold*(OnStackReplacePercentage-InterpreterProfilePercentage)/100;
默认:OnStackReplacePercentage=140, CompileThreshold=10000,InterpreterProfilePercentage=33,计算阈值为10700;
Java代码在执行时一旦被编译器编译为机器码,下一次执行的时候就会直接执行编译后的代码,也就是说,编译后的代码被缓存了起来。缓存编译后的机器码的内存区域就是codeCache。这是一块独立于java堆之外的内存区域。除了jit编译的代码之外,java所使用的本地方法代码(JNI)也会存在codeCache中。不同版本的jvm、不同的启动方式codeCache的默认大小也不同。
JVM 版本和启动方式
默认 codeCache大小
我们现在线上所使用的大多数都是JDK8 64位 Server模式,codeCache空间是240M,随着时间推移,会有越来越多的方法被编译,codeCache使用量会逐渐增加,直至耗尽。在codeCache满了之后会发生什么?
在jdk1.7.0_4之前,你会在jvm的日志里看到这样的输出:
Java HotSpot™ 64-Bit Server VM warning: CodeCache is full. Compiler has been disabled.
Jit编译器被停止了,并且不会被重新启动。已经被编译过的代码仍然以编译方式执行,但是尚未被编译的代码就只能以解释方式执行了。
针对这种情况,jvm提供了一种比较激进的codeCache回收方式:Speculative flushing。在jdk1.7.0_4之后这种回收方式默认开启,而之前的版本需要通过一个启动参数来开启:-XX:+UseCodeCacheFlushing。在Speculative flushing开启的情况下,当codeCache将要耗尽时,最早被编译的一半方法将会被放到一个old列表中等待回收。在一定时间间隔内,如果方法没有被调用,这个方法就会被从codeCache充清除。
很不幸的是,在jdk1.7中,当codeCache耗尽时,Speculative flushing释放了一部分空间,但是从编译日志来看,jit编译并没有恢复正常,并且系统整体性能下降很多,出现大量超时。在oracle官网上看到这样一个bug:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8006952 由于codeCache回收算法的问题,当codeCache满了之后会导致编译线程无法继续,并且消耗大量cpu导致系统运行变慢。Bug里影响版本是jdk8,但是从网上其他地方的信息看,jdk7应该也存在相同的问题,并且没有被修复。
以client模式或者是分层编译模式运行的应用,由于需要编译的类更多(C1编译器编译阈值低,更容易达到编译标准),所以更容易耗尽codeCache。当发现codeCache有不够用的迹象(通过上一节提到的监控方式)时,可以通过启动参数来调整codeCache的大小。
-XX:ReservedCodeCacheSize=256M
1.如果有一天你的系统在发布的时候突然间load上升、CPU上涨,几分钟之后恢复,可以考虑开启分层编译(-XX:+TieredCompilation)。应用中心可以查看系统的jit编译时间。(注意:先排除不是自己本次发布给弄起来的再考虑这点!!!!)
2.一旦开启分层编译就要考虑codeCache的大小,合理的调整codeCahe才能使分层编译达到目的,否则结果比较致命。(-XX:ReservedCodeCacheSize=256M)
注意:调整codeCache大小的时候注意PermSize大小,之前我的理解一直是错的,认为PermSize=永久代大小,实际PermSize=非堆内存大小
3.如果发布期间超时、load升高不能容忍的话,建议使用分层编译,可以容忍的话,一般两三分钟就会恢复
注意: 开启分层编译只是一个手段. 最好的办法, 还是排查代码问题, 是不是有热点代码/数据, 通过预加载, 低流量预热等方式从根本解决问题
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。