Java类加载
类加载/执行加载过程过程对象的创建类加载器双亲委派打破双亲委派常见以tomcat和SPI为例tomcatSPI执行解释执行 or 编译执行关于编译JIT编译器与解释器的工作模式JIT编译器分层编译热点监测热点代码热点监测codeCahecodeCache调优总结加载过程过程将字节码load到虚拟机中的过程称为类的加载加载 ,load字节码验证 ,包括文件格式、元数据、字节码、符号引用的校验等;准备
类加载/执行
类加载
过程
将字节码load到虚拟机中的过程称为类的加载
- 加载 ,load字节码
- 验证 ,包括文件格式、元数据、字节码、符号引用的校验等;
- 准备 ,包括类变量初始化赋值:基本类型0、引用类型null、常量=类中赋值;
- 解析 ,符号引用换直接引用、静态绑定等;
- 初始化 ,静态变量赋值、执行静态代码块等;
- 卸载 ,用户自定义的类的Class对象不再被引用时,类在方法区内的数据会被卸载;
对象的创建
- 堆分配对象的内存空间;
- 在栈中定义变量,将内存地址给变量
- 对象变量赋值(方法区类的信息);
- 初始化,先基类后子类,先执行实例代码块后构造器;
类加载器
双亲委派
- 保证安全性 ,防止重复加载;
- 保证唯一 ,核心class不被篡改,但可以扔到<HAVA_HOME>\lib目录;
打破双亲委派
常见以tomcat和SPI为例
tomcat
- 隔离不同应用 ,例如版本不同的两个应用:Spring2.5 和 3.0;
- 灵活性 ,多web修改文件,不会互相影响;
- 性能 ,使用相同的类用common类加载器,不同的话用各自的类加载器
SPI
- classpath/META-INF/service下查文件
- ServerClassLoader加载
执行
解释执行 or 编译执行
解释:输入程序代码–>得到结果
编译:输入程序代码–>得到可执行代码–>执行可执行的代码得到结果
关于编译
JVM在执行代码的时候并不立即编译代码,主要有个原因:
1.有些代码可能执行频率比较低,甚至就只运行一次,这种情况下,将代码翻译成java字节码比编译这段代码并运行来说要快得多。
2.当 JVM 执行某一方法或遍历循环的次数越多,就会更加了解代码结构,那么 JVM 在编译代码的时候就做出相应的优化。
在主流商用JVM(HotSpot、J9)中,Java程序一开始是通过解释器(Interpreter)进行解释执行的。当JVM发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“热点代码(Hot Spot Code)”,然后JVM会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为:即时编译器(Just In Time Compiler,JIT)
JIT编译器与解释器的工作模式
1、混合模式(Mixed Mode)
JIT编译器(无论C1还是C2)与解释器配合工作的方式;
这是默认的方式,也可通过“-Xmixed”参数设定;
2、解释模式(Interpreted Mode)
全部代码由解释器解释执行,JIT编译器不介入工作;
可以通过“-Xint”参数设定;
3、编译模式(Compiler Mode)
优先采用编译方式执行程序,但解释器仍要在编译无法时行时介入执行过程;
可以通过“-Xcomp”参数设定;
该参数强调的是首次调用方法时执行编译,并不是不用解释器;
一般情况下(不开启分层编译),一个方法需要解释执行一定次数后才编译;
JDK8作为默认开启分层编译策略;
可以通过java -version来查看工作模式
JIT编译器
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;
codeCahe
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应该也存在相同的问题,并且没有被修复。
codeCache调优
以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升高不能容忍的话,建议使用分层编译,可以容忍的话,一般两三分钟就会恢复
注意: 开启分层编译只是一个手段. 最好的办法, 还是排查代码问题, 是不是有热点代码/数据, 通过预加载, 低流量预热等方式从根本解决问题
更多推荐
所有评论(0)