深入JVM:详解G1垃圾回收器原理
一、序言
对于Java工程师而言,深入理解JVM(Java虚拟机)不仅是掌握Java程序运行机制的基础,也是提升系统性能、优化应用和解决复杂问题能力的重要一步,更是Java进阶之路的重中之重。
本文小豪将带大家认识常见垃圾回收器,了解各类垃圾回收器的基础概念与应用场景,同时重点讲解目前最为流行的G1垃圾回收器的工作原理,话不多说,我们直接进入正题。
本文最后附有G1垃圾回收器论文下载地址,便于大家快捷下载
二、常见垃圾回收器
在上一篇【深入JVM:详解垃圾判定与垃圾回收算法】一文中,我们介绍了四种常见的垃圾回收算法:标记清除、复制、标记整理和分代垃圾回收。
而垃圾回收器是在JVM中负责自动管理内存,其通过自动检测和回收垃圾对象,释放内存空间,垃圾回收器正是采用了垃圾回收算法来识别和回收垃圾对象。
一般常见的垃圾回收器区分为年轻代和老年代,除了G1垃圾回收器外其它垃圾回收器必须成对使用。
1、Serial(年轻代)
Serial垃圾回收器是一种单线程串行的垃圾回收器,采用的是复制算法,作用于年轻代,在进行垃圾回收时必须暂停其它所有的线程。
其在单CPU处理器环境下表现比较好,不会存在线程切换导致的性能开销,吞吐量较高,而在多CPU环境下无法充分利用系统资源,吞吐量较低
(1)应用场景
适用于硬件资源配置有限的场景,比如用户桌面的客户端
(2)常用参数配置
// 整堆启用Serial + Serial Old的垃圾回收器组合
-XX:+UseSerialGC
2、Serial Old(老年代)
Serial Old垃圾回收器也是单线程串行的垃圾回收器,它是Serial垃圾回收器在老年代内存的实现,采用的是标记-整理算法,作用于老年代。
同样的,其在单CPU处理器环境下表现比较好,吞吐量较高,在多CPU环境下无法充分利用系统资源
(1)应用场景
Serial Old一般与Serial垃圾回收器联合使用,提供整个堆空间的垃圾回收,同样也较多应用于硬件资源配置有限的场景
3、ParNew(年轻代)
ParNew垃圾回收器是一种多线程并行的垃圾回收器,是Serial垃圾回收器的多线程版本,也采用的是复制算法,作用于年轻代。
其针对Serial垃圾回收器进行了优化,支持利用多个CPU资源同时处理垃圾回收,提高了资源的利用率,有效地降低了STW停顿时间
(1)应用场景
ParNew垃圾回收器可以充分利用多核CPU资源,适用于需较低停顿时间和较高吞吐量的应用程序
(2)常用参数配置
// 年轻代启用ParNew垃圾回收器
-XX:+UseParNewGC
4、CMS(老年代)
CMS(Concurrent Mark Sweep)垃圾回收器是一种专注于减少系统暂停时间的垃圾回收器,采用的是标记清除算法,共分为四个阶段:初始标记、并发标记、重新标记和并发清理,部分阶段与用户线程并发执行,提高了系统响应能力,作用于老年代。
CMS垃圾回收器的主要执行阶段:
- 初始标记:在初始标记阶段通过极短的时间仅标记出
GC Root
根对象直接关联的对象,速度较快,本阶段暂停用户线程。 - 并发标记:并发标记阶段从
GC Root
直接关联的对象进行完整遍历,标记出所有存活的对象,耗时较长,但本阶段与用户线程可同时并发执行,不会暂停用户线程。 - 重新标记:由于并发标记阶段用户线程并没有暂停,可能存在对象引用变化的情况,需要进行重新标记,修正并发标记期间产生变动的对象,耗时短于并发标记阶段,本阶段也会暂停用户线程。
- 并发清理:并发清理阶段直接清除垃圾对象,不需要移动或复制存活对象,本阶段与用户线程同时执行。
在初始标记和重新标记阶段虽然也暂停用户线程,但是处理速度较快,而耗时较长的并发标记和并发清理阶段均与用户线程同时执行,因此CMS垃圾回收器最大程度的减少了用户线程的等待时间,但CMS垃圾回收器也存在部分缺点:
- 存在内存碎片:由于CMS采用标记清除算法,在垃圾回收之后会产生大量的内存碎片。CMS虽然会在经历几次标记清除算法后进行碎片整理,但整理时会暂停用户线程。
- 存在浮动垃圾:CMS垃圾回收器在并发标记和并发清理阶段由于用户线程并未停止,该阶段可能会产生浮动垃圾,无法在本次被回收,只能等到下一次垃圾回收
- 退化问题:当老年代内存不足以分配对象时,CMS会退化为Serial Old单线程串行进行垃圾回收,导致性能下降
- 线程资源争抢:在并发标记和并发清理阶段,执行垃圾回收的线程数量由系统计算出,如果CPU核数有限,会影响用户线程的执行性能,导致程序变慢
(1)应用场景
CMS垃圾回收器适用于多核CPU资源下,用户请求数据量较大的互联网系统中,着重关注程序的响应速度,给用户带来更好的交互体验,一般和ParNew垃圾回收器配合使用,覆盖整个堆空间的垃圾回收
(2)常用参数配置
// 老年代启用CMS垃圾回收器
-XX:+UseConcMarkSweepGC
// 控制碎片整理的频率(默认0,表示经过几次的Full GC后进行空间碎片整理)
-XX:CMSFullGCsBeforeCompaction=n
// 控制并发阶段运行的垃圾回收线程数
-XX:ConcGCThreads=n
5、Parallel Scavenge(年轻代)
Parallel Scavenge垃圾回收器也是一种多线程并行的垃圾回收器,采用的同样是复制算法,在JDK8中为默认的年轻代垃圾回收器。
不同于其它垃圾回收器,Parallel Scavenge主要专注于提升系统的吞吐量(也称吞吐量优先垃圾回收器),支持动态的调整内存大小以及手动设置吞吐量和最大暂停时间(此两个参数互斥),最大程度的提高系统的吞吐量。但由于其重点关注吞吐量,并非最长停顿时间,在某些情况下会导致停顿时间较长。
(1)应用场景
适用于多核CPU资源下,对用户响应时间要求没那么高的后台任务中,Parallel Scavenge能够提高系统的吞吐量,提高用户线程的总体执行时间,尽快完成用户任务
(2)常用参数配置
// 整堆启用Parallel Scavenge + Parallel Old的垃圾回收器组合
-XX:+UseParallelGC
// 设置每次垃圾回收时的最大停顿毫秒数
-XX:MaxGCPauseMillis=n
// 设置吞吐量为n(用户线程执行时间 = n / (n + 1))
-XX:GCTimeRatio=n
// 自动调整内存大小
-XX:+UseAdaptiveSizePolicy
6、Parallel Old(老年代)
Parallel Old垃圾回收器同样是一种多线程并行的垃圾回收器,它是Parallel Scavenge垃圾回收器在老年代内存的实现,采用的是标记-整理算法,也是在JDK8中为默认的老年代垃圾回收器
同样Parallel Old在多核CPU环境下有较高的回收效率,最大程度提高系统的吞吐量
应用场景:同样适用于多核CPU资源下,充分利用CPU资源,对用户响应时间要求没那么高的后台任务中,一般与Parallel Scavenge垃圾回收器配套使用,覆盖整个堆空间的垃圾回收
// 整堆启用Parallel Scavenge + Parallel Old的垃圾回收器组合,等效于-XX:+UseParallelGC参数
-XX:+UseParallelOldGC
7、G1(年轻代+老年代)
G1(Garbage First)垃圾回收器不同于上述的几款垃圾回收器,其采用独特的内存管理策略,实现对整个堆空间的垃圾回收。
G1垃圾回收器主要将堆内存划分为多个大小相等的区域(称为Region),各个区域根据需要扮演不同的角色,可以被定义为Eden区、Survivor区、Old区和Humongous区(存放大对象,老年代的一部分),采用复制算法针对每个区域进行垃圾回收,同样也支持动态的调整内存大小。同时各个Region
不需要连续的存储,颠覆了以往堆内存结构的连续性,具备强大的灵活性,也进一步提高了内存利用率。
其中区域Region
的内存大小默认是通过整个堆内存大小除以2048
得到的,例如整个堆内存为4G
,则Region = 4G / 2048 = 2M
,同时也支持通过JVM参数指定Region
的内存大小。
(1)显著优势
G1垃圾回收器在设计时,充分结合了Parallel Scavenge和CMS垃圾回收器的优点,解决Parallel Scavenge和CMS存在的问题,G1也称垃圾优先回收器,不会像其它垃圾回收器一样等到空间快满时才会进行回收,而是提前触发回收(少量多次),每次垃圾回收时间短,吞吐量高,其优势主要包括:
- 充分利用CPU资源:G1通过并行处理提高了垃圾回收的执行效率,在处理大数据时具备良好的性能
- 避免内存碎片:G1采用复制算法实现垃圾回收,解决CMS垃圾回收器采用标记清除算法导致的内存碎片,提高了内存利用率
- 满足最大暂停时间:支持用户设定最大暂停时间,G1在进行垃圾回收时会根据设定的时间,规划出本次最多能够回收几个
Region
,尽可能保证程序的响应时间需求
G1垃圾回收器在吞吐量、内存管理等方面存在明显的优势,目前是JDK 9及之后版本的默认垃圾回收器,在JDK 8及之前的版本存在诸多问题,但在JDK 8最新版本中已比较稳定,建议使用G1垃圾回收器
(2)常用参数配置
// 整堆启用G1的垃圾回收器
-XX:+UseG1GC
// 设置最大暂停时间(默认200ms)
-XX:MaxGCPauseMillis=n
// 指定Region的内存大小,n必须是2的指数幂,其取值范围是从1M到32M
-XX:G1HeapRegionSize=n
// 指定垃圾回收工作的线程数量
-XX:ParallelGCThreads=n
三、G1垃圾回收器原理
1、G1垃圾回收模式
在G1垃圾回收器中,主要采用了两种垃圾回收模式:Young GC和Mixed GC
- Young GC(年轻代回收):主要针对年轻代区域的垃圾回收,包括
Eden
区和Survivor
区。当所有Eden
区使用率达到最大阀值(默认60%)或者G1计算出来的回收时间接近用户设定的最大暂停时间时,会触发一次Young GC
,回收Eden
区和Survivor
区,复制移动到另外的Survivor
幸存者(年龄+1
)或Old
老年代区(提前晋升的) - Mixed GC(混合回收):Mixed GC是G1垃圾回收器独有的,也称混合回收,针对年轻代和部分老年代区域的垃圾回收。当老年代的占有率达到阀值(默认45%)或年轻代被分配大对象时,会触发一次
Mixed GC
,回收所有年轻代和一部分老年代区(选取的策略是垃圾对象最多的老年代区域,确保释放更多内存空间,即回收价值高的),控制最大暂停时间。
很多博客有提到Full GC,但严格来说
Full GC
并不是G1的概念(至少论文中没有提及),只是在内存空间严重不足时Mixed GC
的退化担保机制。当垃圾清理后没有空的Region
区域存放对象时,会触发一次Full GC
,直接暂停用户线程,调用Serial Old垃圾回收器,采用标记整理算法单线程执行垃圾回收,耗时较长
2、年轻代回收(Young GC)原理
G1垃圾回收器年轻代回收时,采用了三种关键技术,分别是记忆集、卡表和写屏障。接下来我们层层递进,研究一下这些技术分别解决了什么问题。
当G1触发Young GC时,只会扫描年轻代区域(Eden
区 + Survivor
区)的对象,从GC Root
根对象出发时,很容易扫描出年轻代的对象以及年轻代对象引用的其它年轻代的对象。
但这样会产生一个问题,如果年轻代的对象被老年代的对象引用了,应该如何识别出来呢?
2.1 记忆集(RememberedSet)
其实G1垃圾回收器内部维护了一种引用详情表,称为记忆集的数据结构,记录跨代引用,即非回收区域(老年代)对象引用回收区域(年轻代)对象的关系。
记忆集仅记录跨代的对象引用关系,不会记录年轻代区域之间的对象引用
在Young GC回收年轻代对象时,会将记忆集中的对象也加入到GC Root
中,有效避免年轻代的对象被错误的回收
2.2 卡表(Card Table)与卡页(Card Page)
在G1垃圾回收器中,为了进一步压缩记忆集占用的内存,其将所有的Region
区域按大小划分为多个分块,称为卡页(Card Page),对每个卡页进行编号
同时每个Region
区域都会有额外配备一小块内存,这块内存称为卡表(Card Table),用于记录整个堆空间中有哪些卡页引用了自己Region
区域的对象,卡表的底层数据结构是字节数组,每一个字节对应一个卡页,当某个卡页中的对象引用自己Region
区域的对象时,会将卡表对应编号位置的字节修改为1
,为1
的字节被称之为脏卡
此时生成记忆集就会比较容易,只用遍历各个Region
的卡表,找到所有字节为1
的脏卡,形成记忆集。
当年轻代垃圾回收标记存活对象时,G1将此记忆集中的所有对象也加入到GC Root
根对象集合中,确保被老年代引用的年轻代对象标记为存活。
2.3 写屏障(Write Barrier)
更新卡表状态的底层采用了写屏障技术(具体为写后屏障),当执行对象引用相关的代码时,会在其代码前后插入对应的指令
写屏障类似与之前学过的
AOP
,会在引用对象赋值前后做一些额外的动作,主要分为两个:
- 写前屏障:引用对象赋值前的特殊处理
- 写后屏障:引用对象赋值后的特殊处理
JVM中的写屏障要与并发乱序执行中的内存屏障不一样,这里要区分它们
写后屏障指令判断到老年代对象引用年轻代对象时,会更改卡表中对应的字节为脏卡,同时会将脏卡放入到一个脏卡队列中,JVM会通过单独的线程,定期读取脏卡队列中的数据,更新记忆集
可能会有小伙伴会产生好奇,在更新完卡表之后,为何不直接把脏卡写入记忆集呢?
这是由于写屏障指令是由用户线程完成的,如果有大量的用户线程修改对象引用关系,会产生线程安全问题,则需要对记忆集进行加锁,加锁之后势必会影响执行效率。
因此这里将脏卡先放入脏卡队列,采用单独的线程异步消费,避免影响用户线程
3、混合回收(Mixed GC)原理
混合回收是针对年轻代和部分老年代区域的垃圾回收,当老年代内存占用率达到设定阈值,或分配大对象时,将会触发混合回收Mixed GC。
由于Old
老年代区往往存在较多对象,G1垃圾回收器为提升执行效率,减少STW,部分耗时较长的阶段采用了与用户线程并发执行,总体分为初始标记、并发标记、最终标记、清理和转移等五个阶段。
同时为进一步提升处理速度,以及解决并发阶段可能存在的对象引用变化问题,采用三色标记和SATB技术。
3.1 初始标记阶段
初始标记阶段G1垃圾回收器会暂停用户线程,通过极短的时间仅标记出GC Root
根对象直接关联的对象,速度较快。
1)三色标记算法(Tri-color marking)
G1垃圾回收器在初始标记阶段采用三色标记算法标识对象,三色指的的黑白灰,简单的说,将所有对象渲染成不同的颜色,便于区分。
- 白色(垃圾对象):白色代表该对象不在
GC Root
的引用链上,在标记开始时,堆内存中的所有对象默认都是白色,当标记结束,如果对象仍然为白色,则被认为是垃圾对象 - 灰色(待处理对象):灰色代表该对象在
GC Root
的引用链上,但该对象所有引用的对象还未被标记过,是一个过渡颜色,最终会被标记为黑色 - 黑色(存活对象):黑色代表该对象在
GC Root
的引用链上,且该对象所有引用的对象均已被标记过,代表存活对象
实际上,标记对象的颜色其实是通过位图(bitmap)实现的,默认的白色对象的bit
为0
,黑色对象的bit
位会被设置为1
,而灰色对象不会体现在位图,会被放置于一个单独的队列,等待后续处理
CMS垃圾回收器也使用到了三色标记算法
3.2 并发标记阶段
并发标记阶段从GC Root
直接关联的对象进行完整遍历,标记出所有存活的对象,耗时较长,但本阶段与用户线程可同时并发执行,不会暂停用户线程。
在本阶段G1垃圾回收器会从灰色队列中获取到未标记完成的灰色对象,标记其引用关联的所有下一级对象(结束后自身会被标记为黑色),将其引用的下一级对象标记为灰色,放入灰色队列,重复这一过程,直到队列为空
但是,并发标记阶段由于用户线程并没有暂停,会产生新问题,即用户线程若在此期间修改了对象的引用关系,就会导致标记结果不准确,这里其实会误判两种情况:多标和漏标
- 多标:即本应该被回收的垃圾对象,却被标记为黑色存活对象。当已被标记的黑色对象或灰色对象,其引用被删除,就会造成多标
- 漏标:即本应该被标记为黑色的存活对象,却没有被正常标记为黑色,被垃圾回收。当未处理完的灰色对象断开了它引用的白色对象,且已处理完的黑色对象重新引用了该白色对象,则该白色对象会被漏标
多标的问题其实并不严重,顶多产生浮动垃圾,等到下一次垃圾回收时也会被回收,但漏标却是很致命的,已经影响到了程序的正常运行,而G1垃圾回收器采用了SATB原始快照技术解决了这一漏标问题
1)SATB(Snapshot At The Beginning)
SATB主要为解决并发标记阶段可能产生的对象引用变化问题,SATB即一个原始快照,类似于拍照一样,记录某一时刻所有的对象,SATB的主要执行逻辑如下:
- 在标记开始时,创建一个原始快照,记录当前所有存活的对象
- 在标记执行过程中,新创建的对象,直接标记为黑色
- 在标记执行过程中,出现对象引用赋值操作,G1垃圾回收器采用写前屏障技术,将引用的对象放入一个待处理的SATB队列,该队列是每个线程独有的,最终会汇总到全局的SATB队列
3.3 最终标记阶段
最终标记阶段会暂停用户线程,主要用于修正并发标记期间产生变动的对象,总体耗时短于并发标记阶段。
-
最终标记阶段首先会暂停用户线程,将所有线程的SATB队列合并到全局SATB队列,逐一消费。
-
在全局SATB队列中的对象,默认按照黑色存活对象处理,同时处理它们引用的其它对象。
缺点:显而易见,SATB也会造成多标的情况,将可以被回收的垃圾对象标记为存活对象,产生了浮动垃圾,这些浮动垃圾需要等到下一轮垃圾回收时被回收。
3.4 清理阶段
清理阶段也会暂停用户线程,在最终标记阶段完成之后,G1垃圾回收器会整理Region
区域,调整对应的记忆集,若识别到某个Region
不存在任何存活对象时,会直接清理掉该Region
,释放内存
3.5 转移阶段
转移阶段同样也会暂停用户线程,需要将某一个Region
区域存活的对象复制到另一个Region
,主要包括以下步骤:
- 区域选择:G1垃圾回收器会针对各个
Region
的回收价值进行排序,评估最大暂停时间,选择特定的Region
进行回收(一般是垃圾对象最多的区域) - 对象复制转移:选定好
Region
后,首先会将GC Root
根对象直接关联的对象转移到新的Region
区域,然后依次转移其引用的对象 - 更新引用关系:对象转移到新的
Region
区域后,会清理到之前的Region
。若其它Region
区域中的对象引用了转移后的对象,则会重新设置它们的引用关系,避免对象移动位置后引用出错
四、后记
本文重点讲解了G1垃圾回收器的原理,同时也额外扩展了一些常见的垃圾回收器概念,相信经过本文,大家已经对G1有了更全面而深入的理解。
在JDK9及之后的版本,G1已经全面取代了其他垃圾回收器,成为默认垃圾回收器,毫无疑问,其最大的优势,就是最大程度减少STW停顿时间。在G1年轻代回收原理解读时,了解了采用到记忆集、卡表和写屏障这三种关键技术,在G1混合回收原理解读时,了解了其为减少STW以及并发阶段引用误判的问题,采用的三色标记算法和SATB技术,当然本文对G1原理的介绍仍较为粗浅,Oracle
公司也号称G1研发了十年,部分深层次的原理,也建议大家有空可以阅读一下相关论文【G1论文下载链接】或在Oracle
官网查阅相关内容【G1垃圾回收器Oracle官网】。
下一篇,小豪将会持续更新JVM相关内容,如果大家觉得内容还不错,可以先点点关注,共同进步~