JVM-垃圾回收器

一、GC分类与性能指标

因为没有明确规定,可以由不通厂商和不同版本JVM来实现。所以衍生出来众多的GC回收器版本。

线程数:串行、并行

JVM-垃圾回收器

工作模式:并发式、独占式

 JVM-垃圾回收器

碎片处理方式:压缩式、非压缩式

工作内存区间:年轻代和老年代

评估GC的性能指标

  • 吞吐量:运行用户代码的时间占总运行时间的比例
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用时间于总运行时间的比例
  • 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间(STW)
  • 收集频率:相对应用程序的执行,收集彩舟发生的频率
  • 内存占用:java堆所占内存大小
  • 快速:一个对象从诞生到被回收精力的时间

三点中,暂停时间最重要,因为毕竟要服务用户线程,但是一款GC做多满足三点中的两点,一般关注:吞吐量、暂停时间,也是调优关心的2个指标。

JVM-垃圾回收器

因为高吞吐量和低延迟是两个竞争关系的指标,所以针对不同情况选择不同指标进行优化。现在GC标准是:在最大吞吐量有限的情况下,降低停顿时间。

JVM-垃圾回收器

  • 串行回收器:Serial、Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel OId
  • 并发回收器:CMS、G1
  • 新回收器:ZGC、Epsilon、Shenandoah GC

JVM-垃圾回收器

JVM-垃圾回收器

这么多的垃圾回收器,主要是因为不同的应用场景,服务端、移动端等等,开发者可以针对具体的场景选择适合的垃圾回收器。查看 -XX:+PrintCommandLineFlags或者jinfo -flag 参数 进程ID

二、不同的垃圾回收期概念

1、Serial回收器:串行回收

Serial回收器是历史最悠久的,最基本的回收器。采用复制算法、串行回收和STW机制进行回收。Old区使用标记压缩算法进行回收。

  • Serial Old 是运行在Client模式下的默认老年代回收器
  • Serial Old 在Server模式下主要用作:a、与新生代Parallel Sacvenge配合使用 b、作为CMS收集器的后备垃圾回收器

JVM-垃圾回收器

 优势:

  • 简单而高效(相对在单线程\CPU),因为没有切换的开销
  • 在桌面场景下可以使用,内存小、回收频繁
  • 设置-XX:UseSerialGC = 新生代用 SerialGC 老年代用SerialOldGC

总结:

  • 单核场景下优秀,所以使用场景逐渐减少
  • 因为是STW,交互强的应用也无法使用

2、ParNew回收器:并行回收

Serial回收器的多线程版本,Par->多线程、New->新生代,所以ParNew是处理新生代的垃圾回收。也是采用复制算法、STW机制。相对单线程,STW时间少点。在很多Server模式下进行收集新生代进。老年代可以配合Serial Old或者CMS使用,JDK9移除了。应用场景已经很少了。

JVM-垃圾回收器

多CPU的时候,ParNew相比Serial优秀,但是单CPU场景下Serial更好。使用参数-XX:UseParNewGC设置新生代使用ParNew回收器,-XX:ParallelGCThreads设置线程数量,默认和CPU数相同。

3、Parallel回收器:吞吐量优先

也是一款并行回收器,性能和ParNew差不多,使用的都是复制算法、并行回收和STW机制。甚至性能和ParNew差距不是很大。JDK8中是默认的垃圾回收器。因为在服务端发挥出很好的性能。

为什么出现:

  • 达到一个可控制的吞吐量,也被称为吞吐量有限的垃圾回收器
  • 自适应调节策略也是一个重要区别,就是能够根据情况调节内存、吞吐量等
  • 高吞吐量是高效的使用CPU,适合没有太多的交互场景,交互场景需要响应有限
  • Parallel Old采用了标记-压缩算法,但是同样的也是基于并行和STW机制

JVM-垃圾回收器

 参数设置:

  • -XX:UseParallelGC:年轻代的回收,跟老年代相互激活
  • -XX:UseParallelOldGC:老年代的回收,跟年轻代互相激活
  • -XX:ParaellelGCThreads:设置年轻代并行回收线程数,默认情况下同CPU相同,大于8的时候=3+[5*cpu_count]/8
  • -XX:MaxGCPauseMillis:最大暂停时间,默认即可,设置需要谨慎,为了能够符合这个时间,可能自动调整java堆的大小
  • -XX:GCTimeRatio:垃圾回收时间比例,默认99
  • -XX:+UseAdaptoveSizePolicy:设置是否自适应调节,默认调整Edn和S区的比例,新生代和老年代比例

4、CMS回收器:低延迟

第一款真正实现了并发的回收器(回收线程和用户线程同时执行),满足了强交互性场景。尽可能的减少SWT的时间。合适在B\S架构上的。使用标记-清除算法和STW机制。(分配对象的时候使用空闲列表)。

JVM-垃圾回收器

  • 初始标记:STW,在这个过程仅仅是为了标记GC Roots关联到的对象,所以速度很快
  • 并发标记:标记出GC Roots关联对象所关联的全部对象的过程,这个耗时长,但是不STW
  • 重新标记:因为并发标记在用户线程在执行,所以再标记一次,耗时较短,需要STW
  • 并发清除:标记完了,因为不需要碎片整理,所以用户线程可以运行,但是存在碎片问题

缺点:

  • 因为是并发的执行的,所以再回收的过程中,内存需要能够执行用户线程,所以不会等到堆内存满了之后再执行。所以可能存在,当我们执行的时候,出现用户线程太多,CMS无法回收,这个时候启动Serial Old 启动。
  • 因为要再回收的时候,用户线程需要执行,所以没办法使用标记-压缩算法。所以标记-压缩算法的时候一定是STW
  • 产生内存碎片
  • 对CPU资源敏感:虽然用户线程不同停顿,但是因为一起执行,导致应用程序变慢,吞吐量降低
  • CMS无法处理浮动垃圾:没办法处理再并发标记阶段,用户线程心产生的垃圾,只能在第二次进行清除

参数设置:

  • -XX:UseConcMarkSweepGC:手动设置,年轻代自动使用ParNewGC
  • -XX:CMSlnitiatingOccupanyFraction:达到什么情况使用CMS
  • -XX:+UseCMSCompactAtFullCollection:是否在下次GC前,整理内存
  • -XX:CMSFullGCsBeforeCompaction:执行几次后,对内存进行压缩
  • -XX:ParallelCMSThreads:设置CMS的线程数

jdk9之后,GMS已经开始废弃了,jdk14,已经删除GMS了。

小结:

  • 最小化使用内存和并行开销-Serial GC
  • 最大化的吞吐量-Parallel GC
  • 最小的STW-CMS GC + ParaNew GC

5、G1回收器:区域划分代式

G1诞生的原因,随着应用负载越来越高,硬件水平越来越高,GC在不断的优化,G1诞生了。在延迟可控的情况下获得尽可能高的吞吐量,所以才能才维全能收集器。

  • 并行回收器,把内存分割成不同的区域(Region,物理上可以不连续),使用不同的Region表示Eden、s1、s0、老年代等
  • G1有计划的避免了在整个java堆中进行垃圾回收,维护一个Region列表,优先回收价值最高的(回收获得的空间和花费的时间)
  • 由于这种侧重区域垃圾回收,命名为Grabage First - G1
  • 适合多核CPU和大容量内存机器,JDK8 需要开启参数 +XX:UseG1GC,现在就是G1的下了,JDK9之后都是默认回收器

优势

  • 并行和并发
    • 并行:多个GC线程同时工作,会出现STW
    • 并发:可以和用户线程一起运行,不会出现整个应用停顿的情况
  • 分代收集
    • 会区分新生代、老年代针对不同的进行不同的操作,可以物理不连续JVM-垃圾回收器
    • 同时兼顾新生代、老年代的垃圾回收
  • 空间整合
    • 每个Region是复制算法,整体看是标记-压缩,有利于程序长时间运行,堆很大的时候很适合G1
  • 可预测的停顿时间模型
    • 可以在M时间内,在GC上消耗N
    • 由于分区,这样可以缩小了回收范围,因此对于全局的SWT可控
    • 相对CMS,最好的情况G1可能没办法做到,但是最差的情况要好很多

缺点:

  • 相对于CMS,G1还不具备完全碾压的能力,G1需要占用额外的内存空间,大概有10%~20%的额外内存开销
  • 小内存GMS好于G1,平衡点在6~8GB

参数设置:

  • -XX:UseG1GC:使用G1GC,JDK9开始后默认
  • -XX:G1HeapRegionSize:设置Region设置大小,2的幂
  • -XX:MaxGCPauseMillis:最大的停顿时间,默认是200ms
  • -XX:ParallelGCThread:设置并行线程个数,默认8
  • -XX:ConGCThreads:并发的线程数,是并行线程数的4分1
  • -XX:InitiatingHeapOccupancyPercent:触发GC周期的堆内存占用率的阈值

使用步骤:

  1. 开启G1
  2. 设置堆内存
  3. 设置最大停顿时间

G1使用场景,具有较大内存、多处理器能更好的发货出性能。低延迟+内存较大。

分区Region:化整为零

  • 一个Region同时只能充当一个角色,GC后可以转换角色(Eden、S、Old、H)
  • Humongous 大对象区,主要存储一些大对象,达到1.5个Region就是大对象,如果一个Region存储不下,寻找多个区域
  • Region对象分配是指针碰撞、也可以分配TLAB

G1的回收过程

  • Young GC:年轻代的Eden区要用尽的时候,进行并行的独占式的GC
  • 老年代并发标记过程:当堆内存使用达到阈值(默认45%),开始老年代的并发标记过程
  • 混合回收:G1老年代的回收不是对整个老年代的回收,而是有选择的对价值最大的Region进行回收
  • 如果需要单线程、独占式、高强度的Full GC可能存在,是一种保护机制

JVM-垃圾回收器

记忆集-RSet(Remembered Set)

一个对象可能被任意的Region所引用,可能存在老年代引用年轻代,如果扫描整个堆STW时间太长,导致Young GC 需要扫描老年代。

JVM-垃圾回收器

  • 每个Region 都有一个RSet
  • 每次饮用的读写,都会判断引用和被引用的对象是否在Region中,如果不同就在RSet中记录引用所在的Region
  • 这样在垃圾回收的时候,根据RSet确定GC Roots,就不会扫描整个堆 

过程1 Young GC

当Eden满了的时候就会触发Young GC,同时STW。分为如下几步:扫描根、更新Rset、处理Rset、复制对象、处理引用

过程2 并发标记过程 - 老年代

  • 初始标记:标记根可达的对象。这个阶段STW
  • 根区扫描:扫描S区直接可达Old区的对象,并标记被引用对象。这个过程必须YGC之前完成
  • 并发标记:标记堆中的垃圾,当发现Region全都是垃圾立即回收,并计算每个Region的活性
  • 再次标记:因为并发标记,所以需要修正,也是STW
  • 独占清理:根据活性进行Region排序。这个阶段STW
  • 并发清理:清理垃圾

过程3 混合回收 - Mixed GC

老年代+年轻代一起回收,回收价值最高的部分老年代(耗时可控),整个过程是复制算法,注意不是Full GC。

  • 老年代中100%是垃圾的就回收了,但是部分是垃圾的老年代Region就是分8次回收
  • 混合回收全部垃圾老年代+8分一老年代+Eden区+S区,同YGC相比多了老年代
  • 垃圾占有率越高,回收优先级越高
  • 也可以设置回收的阈值

过程4 Full GC

尽量避免Full GC,因为性能差。导致Full GC有2个原因:“

  • 没有足够的空间普升对对象
  • 并发处理过程之前空间耗尽

优化建议

  • 年轻代大小(YGC 独占)
    • 尽量避免显示的设置年轻代的大小
    • 固定年轻代的大小会覆盖暂停时间目标,STW时间不可控
  • 暂停时间目标不要太过苛刻
    • G1 GC吞吐量的目标是90%用户线程和10%的垃圾回收时间
    • 因为目标设置太苛刻直接影响到吞吐量,不能太短

6、垃圾回收器总结 

JVM-垃圾回收器

JVM-垃圾回收器怎么选择垃圾回收器:

  • 优先让JVM自适应
  • 内存小、单核、单机程序、没有停顿需要,用串行收集器
  • 多CPU、高吞吐量、可以停顿时间1秒,选择并行或JVM自适应
  • 多CPU、追求低停顿、需要快速响应,使用并发回收器
  • 官方推荐G1,现在最常用的也是G1

三、GC日志分析

相关参数:

  • -XX:+PrintGC 输出GC日志 == -verbose:gc
  • -XX:+PrintGCDetails 输出GC详细日志
  • -XX:+PrintGCTimeStamps 输出GC时间戳
  • -XX:+PrintGCDateStamps 输出GC时间戳,按照日期格式
  • -XX:+PrintHeapAtGC 在GC前后的打印出堆信息
  • -Xloggc:../log/gc.log 设置GC日志输出路径

1、打印GC日志

-verbose:gc

JVM-垃圾回收器-XX:+PrintGCDetails

JVM-垃圾回收器

GC说明

  • GC和Full GC说明了这次垃圾回收的停顿类型,如果有Full就发生了STW
  • 使用Serial GC 在新生代的名字是Defaulf New Generation,显示“DefNew”
  • 使用ParNew GC 在新生代显示的 ParNew
  • 使用 Parallel GC 在新生代显示的名字是 PSYoungGen
  • 老年代的名字也是由垃圾回收器决定的
  • 使用G1 GC,统一显示成 garbage-frist heap
  • Allocation Failure 表示本次引起GC的原因是在年轻代中没有足够的空间分配了
  • [PSYoungGen: 1234K->23K(456K)] 1234K->700K(500K)
    • 括号内部:GC回收前年轻代大小,回收后大小(年轻代总大小)
    • 括号外部:GC回收前堆大小,回收后大小(堆总大小)
  • user 代表用户耗时,sys内核回收耗时,rea实际耗时。由于是多核时间总和可能会超过rea时间

YG GC

JVM-垃圾回收器

Full GC

JVM-垃圾回收器 日志分析可以使用GCEasy 

四、垃圾回收器的新发展

前面已经说了一种7中经典垃圾GC,现阶段一直在对G1一直优化。但是在不同的场景下可以使用不同的GC。

JDK11后出现了几种新的垃圾回收器:Epsilon、ZGC,也是以后GC的未来。

OpenJdk12的Shenandoah GC,主打低延迟。因为由RedHat推出,没有Oracle的支持。

JVM-垃圾回收器

令人震惊、革命性的ZGC,也是主打在尽可能不影响吞吐量的情况下,降低延迟。

上一篇:vs code配置c/c++开发环境


下一篇:LSMESTIMATE