类加载

过程

请添加图片描述
将字节码load到虚拟机中的过程称为类的加载

  1. 加载 ,load字节码
  2. 验证 ,包括文件格式、元数据、字节码、符号引用的校验等;
  3. 准备 ,包括类变量初始化赋值:基本类型0、引用类型null、常量=类中赋值;
  4. 解析 ,符号引用换直接引用、静态绑定等;
  5. 初始化 ,静态变量赋值、执行静态代码块等;
  6. 卸载 ,用户自定义的类的Class对象不再被引用时,类在方法区内的数据会被卸载;

对象的创建

  1. 堆分配对象的内存空间;
  2. 在栈中定义变量,将内存地址给变量
  3. 对象变量赋值(方法区类的信息);
  4. 初始化,先基类后子类,先执行实例代码块后构造器;

类加载器

请添加图片描述

双亲委派

  1. 保证安全性 ,防止重复加载;
  2. 保证唯一 ,核心class不被篡改,但可以扔到<HAVA_HOME>\lib目录;

打破双亲委派

常见以tomcat和SPI为例
tomcat
  1. 隔离不同应用 ,例如版本不同的两个应用:Spring2.5 和 3.0;
  2. 灵活性 ,多web修改文件,不会互相影响;
  3. 性能 ,使用相同的类用common类加载器,不同的话用各自的类加载器
SPI
  1. classpath/META-INF/service下查文件
  2. 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升高不能容忍的话,建议使用分层编译,可以容忍的话,一般两三分钟就会恢复

注意: 开启分层编译只是一个手段. 最好的办法, 还是排查代码问题, 是不是有热点代码/数据, 通过预加载, 低流量预热等方式从根本解决问题

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐