16.垃圾回收器

一、GC的分类与性能指标

垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。

由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。

从不同角度分析垃圾收集器,可以将GC分为不同的类型。

Java不同版本新特性

  • 语法层面:Lambda表达式、switch、自动拆箱装箱、enum
  • API层面:Stream API、新的日期时间、Optional、String、集合框架
  • 底层优化:JVM优化、GC的变化、元空间、静态域、字符串常量池位置变化

1.1、GC的分类

按照线程数分:串行和并行垃圾回收器

串行回收指的是在同一时间段内只允许有一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集工作结束。

  • 在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以,串行回收默认被应用在客户端的Client模式下的JVM中
  • 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器。

和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了“stop-the-world”机制。

按照工作模式分:并发式和独占式垃圾回收器

按照碎片能处理方式分:压缩式和非压缩式垃圾回收器

按碎片处理方式分,可分为压缩武垃圾回收器和非压缩式垃圾回收器。

  • 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
  • 非压缩式的垃圾回收器不进行这步操作。

按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器。

按照工作的内存空间分:年轻代和老年代垃圾回收器

....

1.2、评估GC的性能指标

简单来说:主要抓住,吞吐量和暂停时间(矛盾的)

吞吐量

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)

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

这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的

吞吐量优先,意味着在单位时间内,STW的时间最短:0.2+0.2=0.4


..

暂停时间

暂停时间”是指一个时间段内应用程序线程暂停,让Gc线程执行的状态

例如,GC期间1ee毫秒的暂停时间意味着在这1e0毫秒期间内没有应用程序线程是活动的。暂停时间优先,意味着尽可能让单次STW的时间最短:0.1+0.1 + 0.1+ 0.1+ 0.1=0.5

..

在设计和使用GC算法时,我们必须确定我们的目标,一个GC算法,可能针对两个目标之一(吞吐量或者暂停时间),或者尝试找一个二者的折中。

现在的标准:在最大吞吐量优先的情况下,降低暂停时间

二、不同的垃圾回收器概述

2.1、七款经典的垃圾回收器:

串行回收器:Serial、Serial old
并行回收器:ParNew、Parallel Scavenge、Parallel old
并发回收器:CMS、G11

七款经典的回收器与垃圾分代的关系

垃圾收集器的组合关系

在jdk8之前,虚线及实现
jdk9把红线删了
jdk14绿线不要了
jdk14把CMS删了

如何查看默认的垃圾回收器是什么?

** 虚拟机参数:-XX:PrintCommandLineFlags:
命令行:jinfo -flag 相关的垃圾回收器参数 进程id
**

三、Serial回收器:串行回收

Serial作为hotspot中Client模式下默认的 新生代垃圾回收器
Serial使用的 复制算法、串行回收和STW机制执行内存回收
除了年轻代之外,老年代采取的是Serial Old收集器,
Serial Old使用的标记-压缩算法、串行回收、STW机制执行内存回收
注意:
Serial Old是运行在Cilent模式下默认的老年代回收器
Serial Old在Server模式下用途有两种:

  • 与新生代的Parrallel Scavenge配合使用
  • 作为老年代CMS收集器的后备方案

3.1、优势:简单、高效(与其他单线程相比、一心一意)

  • 运行在Cilent模式下是个不错的选择

3.2、总结:

这种垃圾收集器大家了解,现在已经不用串行的了。而且在限定单核cpu才可以用。现在都不是单核的了。

对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java web应用程序中是不会采用串行垃圾收集器的。

四、ParNew回收器:并行回收

如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNew收集器则是Serial收集器的多线程版本。

  • Par是Parallel的缩写,New:只能处理的是新生代
  • ParNew仍然采用复制算法、STW机制
  • ParNew(新生代)和Serial Old(老年代)配合使用
  • ParNew是很多JVM运行在Server模式下新生代的默认的垃圾回收器

注意:

  • ParNew在单个CPU的环境下,ParNew收集器不比Serial的效率高

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

Parallel同样采用复制算法,并行回收,STW机制

  • 与ParNew不同的是,该收集器 可以达到 控制的吞吐量,他被称为吞吐量优先的收集器
  • 自适应调节策略 是一大区别,动态调整。

Parallel 收集器在JDK1.6时提供了用于执行老年代垃圾收集的
Parallel old 收集器,用来代替老年代的serial old收集器。

Parallel Old 采用的标记-整理算法,并行收集,STW机制

JVM参数系列 1

JVM参数系列 2

JVM参数系列 3

六、CMS(Concurrent-Mark-Sweep)回收器:低延迟

第一款真正意义上的并发的垃圾收集器,第一次实现了用户线程和垃圾回收线程同时工作

CMS关注点是 尽可能 缩短垃圾回收 用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互的程序,提升用户体验感。

** CMS垃圾收集采用的是 标记--清除算法,并且也会STW,由于使用该算法,避免不了会产生内存碎片,无法使用指针碰撞,使用空闲列表**

6.1、图示

6.2、CMS工作原理:

6.2.1、初识标记(Initial Mark):

  • 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“stop-the-world”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GCRoots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
    ..

6.2.2、并发标记(Concurrent Mark):

  • 并发标记(Concurrent-Mark)阶段:从Gc Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
    ...

6.2.3、重新标记(Remark):

  • 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
    ...

6.2.4、并发清理(Concurrent Sweep):

  • 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
    ...

由于最耗费时间的并发标记并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。

另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure"失败,这时虚拟机将启动后备预案:临时启用Serial old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

提问

6.3、优缺点

6.3.1、优点:

  • 并发收集
  • 低延迟

6.3.2、缺点:

  • 会产生内存碎片
  • CMS收集器对CPU资源非常敏感
  • CMS无法处理浮动垃圾,可能失败,需要后备方案:在并发标记阶段如果产生恒新的垃圾了,CMS将无法对新产生的垃圾进行标记,最终导致这些新产生的对象没有被及时回收

6.4、小结:

hotspot有这么多的GC,Serial GC 、Parallel GC 、CMS有什么不同呢?

口令:
  • 如果想要最小化的内存和并行开销,请选择:Serial GC + Serial Old GC
  • 如果你想要最大吞吐量,请选择:Parallel GC +Parallel Old GC
  • 如果你想要低延迟延迟,请选择:CMS GC + ParNew GC

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

JDK9默认的

目标:官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望。取代了 Parallel gc Parrallel gc Old和CMS gc

G1垃圾收集采用的是** 标记--整理算法,复制算法**

7.1、优缺点

7.1.1、优势:

  • 并行与并发
    并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用计算机的多核,此时用户线程STW。
    并发性:G1拥有和用户线程交替执行的能力,部分工作可以与用户线程同时执行,一般来说,不会在整个垃圾回收阶段发生完全阻塞应用程序的状况。
  • 分代收集
    1、G1依然属于分代型垃圾回收器,不要求他们是连续的,也不再固定大小
    2、将堆空间分成若干个区域(Region),这些区域包含了逻辑上的年轻代和老年代
    3、和之前的垃圾回收器不同,它一人干俩活
  • 空间整合
    1、CMS:“标记清除算法”、内存碎片、若干次GC后进行一次碎片整理
    2、G1:划分成一个个Region,内存回收以Region为单位的。Region是复制算法但整体算是“标记-压缩算法”这两种算法都可以避免碎片化
  • 可预测的停顿时间模型

7.1.2、缺点

相较于CMS,G1不完全碾压CMS,比如在程序运行过程中,
G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行
的额外执行负载(Overload)都要比CMS要高

从经验上来说:在小内存应用上,CMS的表现大概率优于G1,
而G1是大内存上的优势,平衡点在6-8G之间。

7.2、G1回收器的参数设置

7.3、G1回收器的应用场景

7.4、G1 GC垃圾回收过程主要包括如下三个环节:

  • 年轻代GC(Young GC)
  • 老年代并发标记过程(Concurrent Marking)
  • 混合回收(Mixed GC)
  • (如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供一种保护机制,即强力回收)
    ....

顺时针,young gc->young gc+concurrent mark->Mixed GC顺序,进行垃圾回收。

应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。

当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程。

标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。

7.5、G1垃圾回收器的优化建议:

7.5.1、年轻代大小

  • 避免使用-Xmn或者-XX:NewRatio等相关参数设置年轻代大小
  • 固定年轻代大小会覆盖暂停时间目标

7.5.2、暂停时间目标不要太苛刻

  • G1 GC 的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间
  • 评估G1 的吞吐量时,暂停时间目标不要太苛刻。太苛刻的话,表示你愿意承受更多的垃圾回收的开销,从而影响吞吐量。

7.6、G1回收器补充:

从oracle官方透露出来的信息可获知,回收阶段(Evacuation)其实本也有想过设计成与用户程序一起并发执行,但这件事情做起来比较复杂,考虑到G1只是回收一部分Region,停顿时间是用户可控制的,所以并不迫切去实现,而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC)中。另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。

7.7、G1的Region(每个Region附带1个记忆Set)的内存图

记忆Set:RSet:记录的是是谁引用我了,自己引用自己不记录

Remembered Set(记忆集)

Region内存图

7.8、G1回收过程-年轻代GC

JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽时,G1会启动一次年轻代垃圾回收过程。

YGC时,首先G1停止应用程序的执行(Stop-The-Wor1d),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。

然后开始如下回收过程:

  • 第一阶段,扫描根

根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口。

  • 第二阶段,更新RSet

处理dirty card queue(见备注)中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用。

  • 第三阶段,处理RSet

识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。

  • 第四阶段,复制对象。

此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到o1d区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。

  • 第五阶段,处理引用

处理Soft,Weak,Phantom,Final,JNI Weak 等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

7.9、G1回收可选的过程4 - Full GC

G1的初衷就是要避免Fu11GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(stop-The-world),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。

要避免Fu11GC的发生,一旦发生需要进行调整。什么时候会发生Ful1GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到ful1gc,这种情况可以通过增大内存解决。 导致61Fu11GC的原因可能有两个:

EVacuation的时候没有足够的to-space来存放晋升的对象;
并发处理过程完成之前空间耗尽。

八、垃圾回收器总结


GC发展阶段:Seria l=> Parallel(并行)=> CMS(并发)=> G1 => ZGC

最后观点:

  • 没有最好的回收器,更没有万能的回收器
  • 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

九、GC日志分析

YGC 日志分析

FULL GC 日志分析

X、垃圾回收器的新发展

ZGC、、、、、、、、、

XS、ZGC

posted @ 2021-07-16 16:34  宋佳强  阅读(114)  评论(0)    收藏  举报