GC-分代收集器

GC收集器介绍

十款GC收集器

上图中共有十款GC收集器,它们可以根据回收时的属性分为分代和分区两种类型:

  • 分代收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old(MSC)、Parallel Old

  • 分区收集器:G1、ZGC、Shenandoah

GC收集器中的名词解释

在GC收集器中存在一些经常出现的名词,这些名词也是在认识GC收集器之前不得不了解的,如:串行回收、并行回收、独占执行、并发执行、吞吐量、停顿时间、吞吐量优先、响应时间优先等。

串行、并行与独占、并发

  • 串行Serial收集:所有用户线程停止,单条GC线程回收堆的情况被称为串行回收。
  • 并行Parallel收集:所有用户线程停止,多条GC线程回收堆的情况(需多核CPU支持)。
  • 独占Monopoly执行:这里是指GC工作时,GC线程会抢占所有资源执行,整个应用程序会被停止。
  • 并发Concurrent执行:这里的并发是指用户线程和GC线程同时(交替)执行的情况,不会停下某类线程。

吞吐量

吞吐量是性能优化中的一个重要指标,它是指CPU用于执行用户代码的时间与CPU总耗时的比值,在Java中,吞吐量的计算公式为:

吞吐量 = 用户代码执行总时长 /(用户代码执行总时长 + 垃圾回收总时长)。

如JVM在线上执行了100min,其中执行用户代码花费了99min,垃圾回收总用时1min,那么吞吐量则为99min/(99min+1min)=99%。

停顿时间

停顿时间是指GC收集器在工作时,所有用户线程(整个应用程序)的暂停时间。对于独占类的GC收集器而言,停顿时间会比较长。而对于并发类的GC收集器来说,因为GC线程和用户线程是交替执行的,所以程序的停顿时间会缩短,但总体GC效率不如独占GC收集器,因此系统的吞吐量会降低。

基于独占收集器和并发收集器的特性而言,就牵扯出了两个调优时的新名词:吞吐量优先与响应时间优先。 相对而言,在设计系统架构选择GC收集器或进行调优时,最终都是在追求更高的吞吐量以及更短的响应时间。

  • 吞吐量优先:为了确保程序的更高吞吐,允许GC发生时出现长时间暂停。

  • 响应时间优先:为了确保用户更好的体验,可以牺牲一定的吞吐量换取更快的响应速度,发生GC时暂停时间越短越好。

分代收集器

● 新生代收集器:Serial、ParNew、Parallel Scavenge

● 年老代收集器:CMS、Serial Old(MSC)、Parallel Old

新生代收集器

前面提到过新生代收集器主要包含Serial、ParNew、Parallel Scavenge,首先来看看作用于新生代的Serial收集器。

Serial收集器(单线程)

Serial是最原始的新生代收集器,同时它属于单线程的GC收集器,所以也被称为串行收集器。顾名思义,它在执行GC工作时,是以单线程运行的,并且该收集器在发生GC时,会产生STW,也就是会停止所有用户线程。但正由于会停止其他用户线程,所以在执行GC时并不会出现线程间的切换。因此,在单颗CPU的机器上,它的清理效率非常高。

收集动作:串行GC,单线程。

采用算法:复制算法。

STW:GC过程在STW中执行。

GC发生时,执行过程如下:

GC过程中是需要全程发生在STW中的,所以基于系统层面来说,对用户体验感欠佳。

ParNew收集器(多线程)

ParNew收集器是基于Serial收集器的演进版,从严格意义上来看,它可以被称为Serial收集器的多线程版本,同样是作用于新生代区域的收集器。在整个实现上,除开GC收集阶段会使用多条线程回收外,其他实现几乎与Serial收集器大致相同

收集动作:并行GC,多线程。

采用算法:复制算法。

STW:GC过程发生在STW中,采用多线程回收。

GC发生时,执行过程如下:

与Serial唯一的不同点就在于使用了多线程,所以GC发生时仍旧会造成程序停顿。但也因为使用了多线程回收,因此能够在很大程度上缩短系统的停顿时间,从而能够带来比Serial更好的用户体验。更关注吞吐量

Parallel Scavenge收集器(多线程)

Parallel Scavenge同样是一款作用于新生代的多线程GC收集器,但与ParNew收集器不同的是:ParNew通过控制GC线程数量来缩短程序暂停时间,更关心程序的响应时间,而Parallel Scavenge更关心的是程序运行的吞吐量

收集动作:并行GC,多线程。

采用算法:复制算法。

STW:GC过程发生在STW中,采用多线程回收。

GC发生时,执行过程如下:

Parallel收集器和ParNew收集器好像并未有太大的区别。但实际上它们两者之间基于的底层GC框架完全不同,同时关注的方向也完全不同。PS收集器的目标是让程序达到一个可控制的吞吐量(Throughput),所以PS也被称为吞吐量优先的垃圾收集器

GC追求响应时间的时候必然会牺牲吞吐量,而追求吞吐量的同时必然会牺牲响应时间。

老年代GC收集器详解

年老代收集器主要有CMS、Serial Old(MSC)、Parallel Old三款,与新生代的收集器一样,同样存在单线程和多线程收集器之分,接下来我们对年老代收集器进行依次分析。

Serial Old(MSC)收集器(单线程)

Serial Old(MSC)与Serial收集器相同,同样是一款单线程串行回收的收集器,但不同的是:MSC是一款作用于年老代空间的收集器,它采用标记-整理算法对年老代空间进行回收

收集动作:串行GC,单线程。

采用算法:标记-整理算法。

STW:GC过程发生在STW中,采用单线程执行串行回收。

GC发生时,执行过程如下:

Parallel Old收集器(多线程)

Parallel Old则是Parallel Scavenge收集器的年老代版本,同样采用多线程进行并行收集,其内部采用标记-整理算法。与新生代的PS收集器相同的是:PO同样追求的是吞吐量优先。

收集动作:并行GC,多线程。

采用算法:标记-整理算法。

STW:GC过程发生在STW中,采用多线程回收。

GC发生时,执行过程如下:

Parallel Old收集器的年老代版本同样关注于吞吐量系统。

CMS收集器(多线程/并发)

CMS收集器全称为ConcurrentMarkSweep,该款回收器是GC机制中的一座里程碑,在该款收集器中首次实现了并发收集的概念,也就是不停止用户线程,GC线程与用户线程一同工作的情况。同时该款收集器追求的是最短的回收时间,属于多线程收集器,其内部采用标记-清除算法。
jkd9标记为弃用,jdk14正式移除

收集动作:并发GC,多线程并行执行

采用算法:标记-清除算法。

STW:GC过程会发生STW,但并非整个GC过程都在STW中执行,采用多线程回收。

GC发生时,执行过程如下:

CMS对比其他的GC收集器,回收过程明显复杂很多,CMS收集器的回收工作会分为四个步骤:初始标记、并发标记、重新标记以及并发清除。

在整个收集过程中,除开初始标记与重新标记阶段,其他的收集动作都是与用户线程并发执行的。因此,CMS收集器在发生GC时,造成的程序暂停是非常短暂的,对于用户体验感而言,相对比之前的收集器而言是最优者。也正由于CMS收集器并发收集、停顿延迟低的特性,所以在有些地方也被称为并发低停顿收集器。

CMS也存在几个致命的缺点:会产生且无法回收浮动垃圾、对CPU资源非常依赖、GC完成后会造成大量内存碎片

分代GC收集器总结

分代收集器有两个指标,低延迟收集器和高吞吐收集器,根据服务自身业务需求去指定GC收集器

ParNew通过控制GC线程数量来缩短程序暂停时间,更关心程序的响应时间,而Parallel Scavenge更关心的是程序运行的吞吐量

一般而言,如果你的程序是更为关注用户体验度,那么可以采用响应速度优先的收集器工作,因为该类收集器造成的程序暂停不会很久。但如若你的程序不需要与用户有特别多的交互,如批量处理、订单处理、报表计算、科学计算等类型的后台系统,那你则可以采用吞吐量优先的收集器,因为高吞吐量可以高效率地利用CPU资源。

GC组合方案分析

  • 组合一

如果你的程序追求低延迟,用户交互度较为频繁,那你可以采用ParNew + CMS组合(这也是淘宝早期的选择,但后面采用了自研JVM)。

  • 组合二

如若你的程序追求高吞吐,后台计算工作较多,那么Parallel Scavenge + Parallel Old这组PS+PO的收集器会更适合你。

  • 组合三

但你的程序写出来后,更多的情况下部署在单核或双核的机器时,那么最经典的Serial + Serial Old组合绝对是你的最佳选择。

我们再一次将目光聚集在这张图上,需要值得注意的是:在JDK1.8之前,可以采用虚线组合,但在JDK1.8之后,取消了上图中红线的组合,被视为弃用的收集器组合(但如果要用,也是可以用的)。到了JDK1.9时,红线组合被移除,也就代表着在1.9中无法再指定红线组合作为收集器使用。而到了后面的JDK14时,绿线组合也被弃用,同时官方也移除了CMS收集器,为了给G1铺路,使用G1代替了CMS。

三色标记算法

三色标记算法是自CMS收集器后,应用比较广泛的一种并发标记算法,它可以让JVM在发生GC时,只发生短暂的STW即可实现存活对象标记的一种算法。JVM中的CMS以及后续的不分代收集器,之所以可以做到低延迟的根本原因便在于此处。

  1. 白色 (White):
    ○ 未被访问的对象被标记为白色。
    ○ 白色对象是未被标记的对象,可能是垃圾。
  2. 灰色 (Gray):
    ○ 正在访问的对象被标记为灰色。
    ○ 灰色对象是已经被发现但还未完全处理的对象,即它们的引用还没有被完全遍历。
  3. 黑色 (Black):
    ○ 已经完全访问的对象被标记为黑色。
    ○ 黑色对象是已经被完全处理的对象,即它们的所有引用都已经遍历完毕。

三色标记执行过程

标记执行图

实现了三色标记算法的GC收集器,在启动时会分别创建:黑、白、灰三个集合,在最开始所有的对象都在白色集合中。

● 在GC发生时,发生短暂的STW,将所有与GcRoots直接相连的对象转入灰色集合中。

● 之后并发执行,对灰色集合中的对象进行遍历,根据可达性分析算法进行对象存活标记,当一个对象的所有成员全部被标记完成后,该对象则会被移入到黑色集合中。同时,也会将该对象中被标记的成员从白色集合移入灰色集合中。

● 不断重复上一步操作,直至灰色集合彻底没了对象为止。

● 标记完成所有对象后,再次触发STW,通过write-barrier写屏障检测对象是否有变化,如果发生了改变则重新标记,纠正并发标记期间的"误标"。

● 并发执行清除工作,将白色集合中的所有对象全部回收(因为根据GCRoots节点进行可达性分析后,所有的存活对象都会从白色集合移入到黑色集合中,所以依旧留在白色集合中的对象必然为垃圾对象,这些对象就是需要被回收的对象)。

● 最终等待清除工作完成后,代表着整个GC过程结束,再把标记复位,将所有的对象再次放入白色集合中,等待迎接下次GC的到来。

三色标记-并发标记导致的错标问题

被标记的黑色对象中,突然断开了对另一个对象的引用,导致另外一个原本已经被标记为黑色的对象突然变为了垃圾。

GC多回收几次就会被清除了

三色标记-并发执行导致的漏标问题

1一条用户线程在执行过程中,断开了一个未标记的白色对象连接,然后该对象又被一个已经标记成黑色的对象建立起了引用连接

2.白色对象断开了左侧灰色对象的引用,又与右侧的黑色对象建立了新的引用关系。

出现这种情况时,因为重新建立引用的白色对象"父节点"已经被标记黑色了,所以GC线程不会再次标记该对象以及其成员对象,所以这些白色对象会被一直停留在白色集合中。最终导致的结果就是这些依旧存在引用的存活对象会被"误判"为垃圾对象清除掉。而这种情况会直接影响到应用程序的正确性,是不可接受的。

● 采用三色标记算法的收集器又是如何具体解决漏标问题的呢?

● CMS:增量更新 + 写屏障

● G1:STAB + 写屏障

● ZGC:读屏障

写屏障是在对象引用发生变化时进行干预的技术。

当对象引用发生变化时,写屏障会通知垃圾收集器,以便垃圾收集器能够更新标记状态。

在本篇中,先对CMS解决

跨代引用

即老年代的对象引用了年轻代对象,年轻代对象引用老年代对象,在进行可达性分析扫描存活对象时,不可能从新生代一直扫描至年老代的,因为这样就会出现整堆扫描的情况,效率必然会很低。

在HotSpot虚拟机中,为了解决跨代引用的问题,会专门在内存中开辟一块小空间用于维护这些特殊的引用,从而达到让GC不必扫描整个堆空间的目的。而开辟的这块小空间则被称为记忆集、卡表

记忆集

新生代GC时都会通过根可达算法先判断垃圾对象,之后再对非存活对象进行统一回收,但是如果有年老代对象引用了新生代对象,那么根据根可达算法的特性,年老代也会被加入扫描范围,这样下来一次新生代的GC代价太大。所以为了解决跨代引用的问题,在新生代引入了记录集的数据结构,记录从非收集区到收集区的引用指针集合,避免在通过根可达算法判断对象存活时把整个老年代加入扫描范围。

GC时,GC收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需进行详细的根搜索过程。
年轻代发生GC时,扫描一个对象时,通过记忆集发现某个引用指向年老代对象时,此时GC线程会停止扫描这个引用,从而避免出现整堆扫描的情况。

卡表

卡表是记忆集第三种精度的实现,也是HotSpot虚拟机中记忆集的实现方式,卡表中记录中记忆集的记录精度、与堆内存区域的映射关系等。

在HotSpot中卡表是使用一个字节数组实现:CARD_TABLE[this addredd >>9]=0,数组中每个元素对应着其标识的内存区域,称为卡页,hotSpot使用的卡页大小为2^9 即512字节,也就是说内存中每连续的512字节会被当作一个卡页作为卡表的一个元素。

五、GC分代篇总结

从GC的一些基础概念,到分代收集器、各款收集器收集过程、CMS收集器及其执行过程、三色标记算法、三色标记-漏标/多标问题、YoungGC、FullGC日志解读、GC诱发原因等内容进行全面阐述。

在JVM的GC体系中,其实并不存在所谓的最好GC器,不同的场景下采用合适的GC收集器,才能在最大程度上追求最优的方案。各款GC收集器对比如下:

对技术有兴趣的同学可以加群

相关推荐
疯一样的码农3 分钟前
Python 正则表达式(RegEx)
开发语言·python·正则表达式
代码之光_19804 分钟前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi10 分钟前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
&岁月不待人&25 分钟前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
StayInLove28 分钟前
G1垃圾回收器日志详解
java·开发语言
对许32 分钟前
SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“
java·log4j
无尽的大道36 分钟前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
爱吃生蚝的于勒40 分钟前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
小鑫记得努力1 小时前
Java类和对象(下篇)
java
binishuaio1 小时前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git