JVM优化策略

工欲善其事必先利其器,要了解JVM运行情况,必须用工具获取数据才能发现和诊断问题。 让JVM这个黑盒变成我们可以认识的白盒。

名称作用基本命令
jps显示指定系统内所有的HotSpot虚拟机进程jps -l
jstat用于收集Hotspot虚拟机各方面的运行数据jstat[option vmid[interval[s|ms][count]]] jstat -gc 2764 250 20 进程2764,gc 情况
jinfo显示虚拟机配置信息jinfo-flag CMSInitiatingOccupancyFraction 1444
jmap生成虚拟机的内存转储快照jmap[option]vmid  
jhat用户分析heapdump文件,它会建立一个HTTP/HTML服务器,让用户可以在浏览器上查看分析结果 
jstack显示虚拟机的线程快照jstack[option]vmid jstack -l 3500
JConsoleJava监视与管理控制台 
VisualVM多合一故障处理工具 

1

优化策略主要包括JVM垃圾收集器选择、内存分配、高效编译、代码层面优化。

应用场景中主要问题表现为内存溢出、CPU负载高、内存使用过高、系统响应慢。

内存回收与分配

安全点:程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

安全区域:在一段代码片段之中,引用关系不会发生变化,在这个区域中的任意地方开始GC都是安全的。

在线程执行到SafeRegion中的代码时,首先标识自己已经进入了SafeRegion,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为SafeRegion状态的线程了。在线程要离开SafeRegion时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程),如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开SafeRegion的信号为止。

回收方法区

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

无用的类条件:

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

堆对象内存分配

  • 对象优先在Eden分配
  • 大对象直接进入老年代:大于PretenureSizeThreshold参数
  • 长期存活的对象将进入老年代:达到MaxTenuringThreshold参数值
  • 动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保:在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么MinorGC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次MinorGC,尽管这次MinorGC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次FullGC。

JVM垃圾收集器

对象是否活着

  • 引用计数算法:很难解决对象之间相互循环引用的问题。
  • 可达性分析算法:通过一系列的称为“GCRoots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(ReferenceChain),当一个对象到GCRoots没有任何引用链相连(用图论的话来说,就是从GCRoots到这个对象不可达)时,则证明此对象是不可用的。

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

如果GC Roots的对象长期不回收,会导致内存泄漏。

收集器整理

名称算法工作区域方式目标场景
Serial复制算法新生代串行响应速度优先单CPU环境下的Client模式
ParNew复制算法新生代并行响应速度优先多CPU环境时在Server模式下与CMS配合
Parallel Scavenge复制算法新生代并行吞吐量优先在后台运算而不需要太多交互的任务
Serial Old标记-整理老年代串行响应速度优先单CPU环境下的Client模式、CMS的后备预案
Parallel Old标记-整理老年代并行吞吐量优先在后台运算而不需要太多交互的任务
CMS标记-清除老年代并发停顿时间集中在互联网站或B/S系统服务端上的Java应用 重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验
G1标记-整理整堆并行 并发停顿时间 时间可预测面向服务端应用,将来替换CMS

1

  • 停顿时间优先:交互多,对响应速度要求高
  • 吞吐量优先:交互少,计算多,适合在后台运算的场景。

吞吐量就是CPU****用于运行用户代码的时间CPU****总消耗时间的比值,即

**吞吐量 = 运行用户代码时间 /(运行用户代码时间 + **垃圾收集时间)。

假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

Java11 ZGC

ZGC全称是Z Garbage Collector,是一款可伸缩(scalable)的低延迟(low latency garbage)、并发(concurrent)垃圾回收器,旨在实现以下几个目标:

  • 停顿时间不超过10ms
  • 停顿时间不随heap大小或存活对象大小增大而增大
  • 可以处理从几百兆到几T的内存大小

在不同的场景(高并发低延迟、高吞吐量计算型)选择不同的收集器是必然的优化手段,个中参数需要在实战中不断摸索。

编译篇

编译期优化

编译过程:

  • 解析与填充符号表过程:词法、 语法分析;填充符号表
  • 插入式注解处理器的注解处理过程:提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个Round。
  • 分析与字节码生成过程:对结构上正确的源程序进行上下文有关性质的审查。标注检查;数据及控制流分析;解语法糖;字节码生成

Java语法糖

  • 泛型与类型擦除
  • 自动装箱、 拆箱
  • 遍历循环
  • 变长参数

条件编译

在Java语言之中并没有使用预处理器。

根据布尔常量值的真假,编译器将会把分支中不成立的代码块消除掉,这一工作将在编译器解除语法糖阶段(com.sun.tools.javac.comp.Lower类中)完成。

运行期优化

  • 解释器:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。
  • 编译器:在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。

编译对象与触发条件

栈上替换:OSR编译(On Stack Replacement ),即方法栈帧还在栈上,方法就被替换了。

基于计数器的热点探测:虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”。

热点探测方法:

  • 方法调用计数器
  • 回边计数器

热点代码的探测参数修改,可以优化一部分代码执行时间。

编译过程

Client Compiler

  1. 平台独立的前端将字节码构造成一种高级中间代码表示(HighLevel Intermediate Representaion,HIR)。 
  2. 平台相关的后端从HIR中产生低级中间代码表示(Low-Level IntermediateRepresentation,LIR),而在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便让HIR达到更高效的代码表示形式。
  3. 在平台相关的后端使用线性扫描算法(LinearScanRegisterAllocation)在LIR上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后产生机器代码。

Server Compiler

  • 无用代码消除(Dead Code Elimination)
  • 循环展开(Loop Unrolling)
  • 循环表达式外提(Loop Expression Hoisting)
  • 消除公共子表达式(Common Subexpression Elimination)
  • 常量传播(Constant Propagation)
  • 基本块重排序(Basic Block Reordering) 

Server Compiler的寄存器分配器是一个全局图着色分配器,它可以充分利用某些处理器架构(如RISC)上的大寄存器集合。以即时编译的标准来看,Server Compiler无疑是比较缓慢的,但它的编译速度依然远远超过传统的静态优化编译器,而且它相对于Client Compiler编译输出的代码质量有所提高,可以减少本地代码的执行时间,从而抵消了额外的编译时间开销,所以也有很多非服务端的应用选择使用Server模式的虚拟机运行。

编译器优化技术

  • 语言无关的经典优化技术之一: 公共子表达式消除。
  • 语言相关的经典优化技术之一: 数组范围检查消除。
  • 最重要的优化技术之一: 方法内联。
  • 最前沿的优化技术之一: 逃逸分析。

消除操作

  • 数组边界检查消除
  • 自动装箱消除(AutoboxElimination)
  • 安全点消除(Safepoint Elimination)
  • 消除反射(Dereflection)

方法内联

把目标方法的代码“复制”到发起调用的方法之中,避免发生真实的方法调用。 

类型继承关系分析(Class Hierarchy Analysis,CHA):基于整个应用程序的类型分析技术,它用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等信息。

守护内联:如果遇到虚方法,则会向CHA查询此方法在当前程序下是否有多个目标版本可供选择,如果查询结果只有一个版本,那也可以进行内联,不过这种内联就属于激进优化,需要预留一个“逃生门”(Guard条件不成立时的SlowPath)

内联缓存:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接收者的版本信息,并且每次进行方法调用时都比较接收者版本,如果以后进来的每次调用的方法接收者版本都是一样的,那这个内联还可以一直用下去。如果发生了方法接收者不一致的情况,就说明程序真正使用了虚方法的多态特性,这时才会取消内联,查找虚方法表进行方法分派。

逃逸分析

当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸

  • 栈上分配( Stack Allocation):如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
  • 同步消除( Synchronization Elimination):如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以消除掉。
  • 标量替换( Scalar Replacement):如果把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问。

 

 

 

 

 

参考:

Java——七种垃圾收集器+JDK11最新ZGC 

Java Class 文件结构

代码交流 2021