三色标记法与垃圾回收器(CMS、G1)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了三色标记法与垃圾回收器(CMS、G1)相关的知识,希望对你有一定的参考价值。

参考技术A

JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。

三色标记法将对象的颜色分为了黑、灰、白,三种颜色。

存在问题:

浮动垃圾:并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,此时,此对象不是白色的不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,这种情况对系统的影响不大,留给下一次GC进行处理即可。
对象漏标问题(需要的对象被回收):并发标记的过程中,一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);因为黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题,而CMS与G1,两种回收器在使用三色标记法时,都采取了一些措施来应对这些问题,CMS对增加引用环节进行处理(Increment Update),G1则对删除引用环节进行处理(SATB)。

在JVM虚拟机中有两种常见垃圾回收器使用了该算法:

CMS(Concurrent Mark Sweep)

CMS,是非常有名的JVM垃圾回收器,它起到了承上启下的作用,开启了并发回收的篇章。
但是CMS由于许多小问题,现在基本已经被淘汰。

增量更新(Increment Update)
在应对漏标问题时,CMS使用了Increment Update方法来做:
在一个未被标记的对象(白色对象)被重新引用后,==引用它的对象==,若为黑色则要变成灰色,在下次二次标记时让GC线程继续标记它的属性对象。
但是就算时这样,其仍然是存在漏标的问题:

在一个灰色对象正在被一个GC线程回收时,当它已经被标记过的属性指向了一个白色对象(垃圾)
而这个对象的属性对象本身还未全部标记结束,则为灰色不变
而这个GC线程在标记完最后一个属性后,认为已经将所有的属性标记结束了,将这个灰色对象标记为黑色,被重新引用的白色对象,无法被标记

补充,CMS除了这个缺陷外,仍然存在两个个较为致命的缺陷:

解决方案:使用Mark-Sweep-Compact算法,减少垃圾碎片

当JVM认为内存不够了,再使用CMS进行并发清理内存可能会发生OOM的问题,而不得不进行Serial Old GC,Serial Old是单线程垃圾回收,效率低

解决方案:降低触发CMS GC的阈值,让浮动垃圾不那么容易占满老年代

G1(Garbage First)

从G1垃圾回收器开始,G1的物理内存不再分代,而是由一块一块的Region组成;逻辑分代仍然存在。

前置知识 — Card Table(多种垃圾回收器均具备)

由于在进行YoungGC时,我们在进行对一个对象是否被引用的过程,需要扫描整个Old区,所以JVM设计了CardTable,将Old区分为一个一个Card,一个Card有多个对象;如果一个Card中的对象有引用指向Young区,则将其标记为Dirty Card,下次需要进行YoungGC时,只需要去扫描Dirty Card即可。

Card Table 在底层数据结构以 Bit Map实现。

CSet(Collection Set)

SATB(Snapshot At The Beginning)
在应对漏标问题时,CMS使用了SATB方法来做:

因为SATB在重新标记环节只需要去重新扫描那些被推到堆栈中的引用,并配合Rset来判断当前对象是否被引用来进行回收;

并且在最后G1并不会选择回收所有垃圾对象,而是根据Region的垃圾多少来判断与预估回收价值(指回收的垃圾与回收的STW时间的一个预估值),将一个或者多个Region放到CSet中,最后将这些Region中的存活对象压缩并复制到新的Region中,清空原来的Region。

问题:G1会不会进行Full GC?
会,当内存满了的时候就会进行Full GC;且JDK10之前的Full GC,为单线程的,所以使用G1需要避免Full GC的产生。
解决方案:

加大内存;
提高CPU性能,加快GC回收速度,而对象增加速度赶不上回收速度,则Full GC可以避免;
降低进行Mixed GC触发的阈值,让Mixed GC提早发生(默认45%)

G1的第一篇paper(附录1)发表于2004年,在2012年才在jdk1.7u4中可用。oracle官方计划在jdk9中将G1变成默认的垃圾收集器,以替代CMS。为何oracle要极力推荐G1呢,G1有哪些优点?

首先,G1的设计原则就是简单可行的性能调优

开发人员仅仅需要声明以下参数即可:

其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。

其次,G1将新生代,老年代的物理空间划分取消了。

这样我们再也不用单独的空间对每个代进行设置了,不用担心每个代内存是否足够。

取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。不过,这些区域的一部分包含新生代,新生代的垃圾收集依然采用暂停所有应用线程的方式,将存活对象拷贝到老年代或者Survivor空间。老年代也分成很多区域,G1收集器通过将对象从一个区域复制到另外一个区域,完成了清理工作。这就意味着,在正常的处理过程中,G1完成了堆的压缩(至少是部分堆的压缩),这样也就不会有cms内存碎片问题的存在了。

在G1中,还有一种特殊的区域,叫Humongous区域。 如果一个对象占用的空间超过了分区容量50%以上,G1收集器就认为这是一个巨型对象。这些巨型对象,默认直接会被分配在年老代,但是如果它是一个短期存在的巨型对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。

PS:在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。

对象分配策略

说起大对象的分配,我们不得不谈谈对象的分配策略。它分为3个阶段:

对TLAB空间中无法分配的对象,JVM会尝试在Eden空间中进行分配。如果Eden空间无法容纳该对象,就只能在老年代中进行分配空间。

最后,G1提供了两种GC模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。下面我们将分别介绍一下这2种模式。

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行。

这时,我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用。

在CMS中,也有RSet的概念,在老年代中有一块区域用来记录指向新生代的引用。这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代。

但在G1中,并没有使用point-out,这是由于一个分区太小,分区数量太多,如果是用point-out的话,会造成大量的扫描浪费,有些根本不需要GC的分区引用也扫描了。于是G1中使用point-in来解决。point-in的意思是哪些分区引用了当前分区中的对象。这样,仅仅将这些对象当做根来扫描就避免了无效的扫描。由于新生代有多个,那么我们需要在新生代之间记录引用吗?这是不必要的,原因在于每次GC时,所有新生代都会被扫描,所以只需要记录老年代到新生代之间的引用即可。

需要注意的是,如果引用的对象很多,赋值器需要对每个引用做处理,赋值器开销会很大,为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。Card Table通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未被引用。当一个地址空间被引用时,这个地址空间对应的数组索引的值被标记为”0″,即标记为脏被引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

Young GC 阶段:

Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

它的GC步骤分2步:

全局并发标记(global concurrent marking)
拷贝存活对象(evacuation)
在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?

在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:

初始标记(initial mark,STW)
在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
根区域扫描(root region scan)
G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
并发标记(Concurrent Marking)
G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断
最终标记(Remark,STW)
该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
清除垃圾(Cleanup,STW)
在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。 首先,我们将对象分成三种类型的。

根对象被置为黑色,子对象被置为灰色。

继续由灰色遍历,将已扫描了子对象的对象置为黑色。

遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。

这看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题

我们看下面一种情况,当垃圾收集器扫描到下面情况时:

这时候应用程序执行了以下操作:

这样,对象的状态图变成如下情形:

这时候垃圾收集器再标记扫描的时候就会下图成这样:

很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:

在插入的时候记录对象
在删除的时候记录对象
刚好这对应CMS和G1的2种不同实现方式:

在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:

这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。混合式垃圾收集如下图:

混合式GC也是采用的复制的清理策略,当GC完成后,会重新释放空间。

至此,混合式GC告一段落了。下一小节我们讲进入调优实践。

MaxGCPauseMillis调优

前面介绍过使用GC的最基本的参数:

前面2个参数都好理解,后面这个MaxGCPauseMillis参数该怎么配置呢?这个参数从字面的意思上看,就是允许的GC最大的暂停时间。G1尽量确保每次GC暂停的时间都在设置的MaxGCPauseMillis范围内。 那G1是如何做到最大暂停时间的呢?这涉及到另一个概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的区域集合。

Young GC:选定所有新生代里的region。通过控制新生代的region个数来控制young GC的开销。
Mixed GC:选定所有新生代里的region,外加根据global concurrent marking统计得出收集收益高的若干老年代region。在用户指定的开销目标范围内尽可能选择收益高的老年代region。
在理解了这些后,我们再设置最大暂停时间就好办了。 首先,我们能容忍的最大暂停时间是有一个限度的,我们需要在这个限度范围内设置。但是应该设置的值是多少呢?我们需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。G1的默认暂停时间是200毫秒,我们可以从这里入手,调整合适的时间。

其他调优参数

避免使用以下参数:

避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。

触发Full GC

在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?

并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。

晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:

巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

由于篇幅有限,G1还有很多调优实践,在此就不一一列出了,大家在平常的实践中可以慢慢探索。最后,期待java 9能正式发布,默认使用G1为垃圾收集器的java性能会不会又提高呢?

G1处理和传统的垃圾收集策略是不同的,关键的因素是它将所有的内存进行了子区域的划分。

总结

G1是一款非常优秀的垃圾收集器,不仅适合堆内存大的应用,同时也简化了调优的工作。通过主要的参数初始和最大堆空间、以及最大容忍的GC暂停目标,就能得到不错的性能;同时,我们也看到G1对内存空间的浪费较高,但通过**首先收集尽可能多的垃圾(Garbage First)的设计原则,可以及时发现过期对象,从而让内存占用处于合理的水平。

参考链接:
https://juejin.cn/post/6859931488352370702
https://blog.csdn.net/qq_39276448/article/details/104470796

带颜色的JVM:三色标记详解


 

 

点击上方关注 “终端研发部

设为“星标”,和你一起掌握更多数据库知识

三色标记法是一种垃圾回收法,它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。JVM中的CMS、G1垃圾回收器所使用垃圾回收算法即为三色标记法。

# 三色标记算法思想

三色标记法将对象的颜色分为了黑、灰、白,三种颜色。

白色:该对象没有被标记过。(对象垃圾)

灰色:该对象已经被标记过了,但该对象下的属性没有全被标记完。(GC需要从此对象中去寻找垃圾)

黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象)


# 算法流程

从我们main方法的根对象(JVM中称为GC Root)开始沿着他们的对象向下查找,用黑灰白的规则,标记出所有跟GC Root相连接的对象,扫描一遍结束后,一般需要进行一次短暂的STW(Stop The World),再次进行扫描,此时因为黑色对象的属性都也已经被标记过了,所以只需找出灰色对象并顺着继续往下标记(且因为大部分的标记工作已经在第一次并发的时候发生了,所以灰色对象数量会很少,标记时间也会短很多), 此时程序继续执行,GC线程扫描所有的内存,找出扫描之后依旧被标记为白色的对象(垃圾),清除。

具体流程:

  1. 首先创建三个集合:白、灰、黑。

  2. 将所有对象放入白色集合中。

  3. 然后从根节点开始遍历所有对象(注意这里并不递归遍历),把遍历到的对象从白色集合放入灰色集合。

  4. 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合

  5. 重复 4 直到灰色中无任何对象

  6. 通过write-barrier检测对象有变化,重复以上操作

  7. 收集所有白色对象(垃圾)


# 三色标记存在问题

1.浮动垃圾:并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,由于不会再对黑色标记过的对象重新扫描,所以不会被发现,那么这个对象不是白色的但是不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,浮动垃圾对系统的影响不大,留给下一次GC进行处理即可。

2.对象漏标问题(需要的对象被回收):并发标记的过程中,一个业务线程将一个未被扫描过的白色对象断开引用成为垃圾(删除引用),同时黑色对象引用了该对象(增加引用)(这两部可以不分先后顺序);因为黑色对象的含义为其属性都已经被标记过了,重新标记也不会从黑色对象中去找,导致该对象被程序所需要,却又要被GC回收,此问题会导致系统出现问题,而CMS与G1,两种回收器在使用三色标记法时,都采取了一些措施来应对这些问题,CMS对增加引用环节进行处理(Increment Update),G1则对删除引用环节进行处理(SATB)。


# 解决办法

在JVM虚拟机中有两种常见垃圾回收器使用了该算法:CMS(Concurrent Mark Sweep)、G1(Garbage First) ,为了解决三色标记法对对象漏标问题各自有各自的法:


# CMS回顾

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非常符合这类应用的需求(但是实际由于某些问题,很少有使用CMS作为主要垃圾回收器的)。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

1)初始标记(CMS initial mark) 

2)并发标记(CMS concurrent mark) 

3)重新标记(CMS remark) 

4)并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GCRoots能直接关联到的对象,速度很快;

并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;

重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;

最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。


# CMS解决办法:增量更新

在应对漏标问题时,CMS使用了增量更新(Increment Update)方法来做:

在一个未被标记的对象(白色对象)被重新引用后,引用它的对象若为黑色则要变成灰色,在下次二次标记时让GC线程继续标记它的属性对象。

但是就算时这样,其仍然是存在漏标的问题:

  • 在一个灰色对象正在被一个GC线程回收时,当它已经被标记过的属性指向了一个白色对象(垃圾)

  • 而这个对象的属性对象本身还未全部标记结束,则为灰色不变

  • 而这个GC线程在标记完最后一个属性后,认为已经将所有的属性标记结束了,将这个灰色对象标记为黑色,被重新引用的白色对象,无法被标记


# CMS另两个致命缺陷

1.CMS采用了Mark-Sweep算法,最后会产生许多内存碎片,当到一定数量时,CMS无法清理这些碎片了,CMS会让Serial Old垃圾处理器来清理这些垃圾碎片,而Serial Old垃圾处理器是单线程操作进行清理垃圾的,效率很低。

所以使用CMS就会出现一种情况,硬件升级了,却越来越卡顿,其原因就是因为进行Serial Old GC时,效率过低。

  • 解决方案:使用Mark-Sweep-Compact算法,减少垃圾碎片

  • 调优参数(配套使用):

-XX:+UseCMSCompactAtFullCollection  开启CMS的压缩
-XX:CMSFullGCsBeforeCompaction 默认为0,指经过多少次CMS FullGC才进行压缩

2.当JVM认为内存不够,再使用CMS进行并发清理内存可能会发生OOM的问题,而不得不进行Serial Old GC,Serial Old是单线程垃圾回收,效率低

  • 解决方案:降低触发CMS GC的阈值,让浮动垃圾不那么容易占满老年代

  • 调优参数:

-XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,让老年代占用率达到该值就进行CMS GC

# G1回顾

G1(Garbage First)物理内存不再分代,而是由一块一块的Region组成,但是逻辑分代仍然存在。G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待,如图所示


# G1前置知识

Card Table(多种垃圾回收器均具备)

  • 由于在进行YoungGC时,我们在进行对一个对象是否被引用的过程,需要扫描整个Old区,所以JVM设计了CardTable,将Old区分为一个一个Card,一个Card有多个对象;如果一个Card中的对象有引用指向Young区,则将其标记为Dirty Card,下次需要进行YoungGC时,只需要去扫描Dirty Card即可。

  • Card Table 在底层数据结构以 Bit Map实现。

RSet(Remembered Set)

是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些类似。

后面说到的CSet(Collection Set)也是辅助GC的,它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。

在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。

而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。每个Region中都有一个RSet,记录其他Region到本Region的引用信息;使得垃圾回收器不需要扫描整个堆找到谁引用当前分区中的对象,只需要扫描RSet即可。微信搜索终端研发部获取2021年面试大全

CSet(Collection Set)

一组可被回收的分区Region的集合, 是多个对象的集合内存区域。

新生代与老年代的比例

5% - 60%,一般不使用手工指定,因为这是G1预测停顿时间的基准,这地方简要说明一下,G1可以指定一个预期的停顿时间,然后G1会根据你设定的时间来动态调整年轻代的比例,例如时间长,就将年轻代比例调小,让YGC尽早行。

# G1解决办法:SATB

SATB(Snapshot At The Beginning), 在应对漏标问题时,G1使用了SATB方法来做,具体流程:

  1. 在开始标记的时候生成一个快照图标记存活对象

  2. 在一个引用断开后,要将此引用推到GC的堆栈里,保证白色对象(垃圾)还能被GC线程扫描到(在**write barrier(写屏障)**里把所有旧的引用所指向的对象都变成非白的)。

  3. 配合Rset,去扫描哪些Region引用到当前的白色对象,若没有引用到当前对象,则回收


# SATB详细流程

  1. SATB是维持并发GC的一种手段。G1并发的基础就是SATB。SATB可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对像就认为是活的,从而开成一个对象图。

  2. 在GC收集的时候,新生代的对象也认为是活的对象,除此之外其他不可达的对象都认为是垃圾对象。

  3. 如何找到在GC过程中分配的对象呢?每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。

  4. 通过这种方式我们就找到了在GC过程中新分配的对象,并把这些对象认为是活的对象。

  5. 解决了对象在GC过程中分配的问题,那么在GC过程中引用发生变化的问题怎么解决呢?

  6. G1给出的解决办法是通过Write Barrier。Write Barrier就是对引用字段进行赋值做了额外处理。通过Write Barrier就可以了解到哪些引用对象发生了什么样的变化。

  7. mark的过程就是遍历heap标记live object的过程,采用的是三色标记算法,这三种颜色为white(表示还未访问到)、gray(访问到但是它用到的引用还没有完全扫描)、back(访问到而且其用到的引用已经完全扫描完)。

  8. 整个三色标记算法就是从GC roots出发遍历heap,针对可达对象先标记white为gray,然后再标记gray为black;遍历完成之后所有可达对象都是balck的,所有white都是可以回收的。

  9. SATB仅仅对于在marking开始阶段进行“snapshot”(marked all reachable at mark start),但是concurrent的时候并发修改可能造成对象漏标记。

  10. 对black新引用了一个white对象,然后又从gray对象中删除了对该white对象的引用,这样会造成了该white对象漏标记。

  11. 对black新引用了一个white对象,然后从gray对象删了一个引用该white对象的white对象,这样也会造成了该white对象漏标记。

  12. 对black新引用了一个刚new出来的white对象,没有其他gray对象引用该white对象,这样也会造成了该white对象漏标记。


# SATB效率高于增量更新的原因?

因为SATB在重新标记环节只需要去重新扫描那些被推到堆栈中的引用,并配合Rset来判断当前对象是否被引用来进行回收;微信搜索终端研发部获取2021年面试大全

并且在最后G1并不会选择回收所有垃圾对象,而是根据Region的垃圾多少来判断与预估回收价值(指回收的垃圾与回收的STW时间的一个预估值),将一个或者多个Region放到CSet中,最后将这些Region中的存活对象压缩并复制到新的Region中,清空原来的Region。

# G1会不会进行Full GC?

会,当内存满了的时候就会进行Full GC;且JDK10之前的Full GC,为单线程的,所以使用G1需要避免Full GC的产生。

解决方案:

  • 加大内存;

  • 提高CPU性能,加快GC回收速度,而对象增加速度赶不上回收速度,则Full GC可以避免;

  • 降低进行Mixed GC触发的阈值,让Mixed GC提早发生(默认45%)

今日好文推荐

GitHub上非常实用的40个开源JAVA项目

XShell收费太贵?快试试开源的NuShell,好用!

GET 和 POST请求的本质区别是什么?看完觉得自己太无知了...

MyBatis批量插入数据你还在用foreach?你们的服务器没崩?

点个在看少个 bug 👇

以上是关于三色标记法与垃圾回收器(CMS、G1)的主要内容,如果未能解决你的问题,请参考以下文章

CMS,G1垃圾回收器中的三色标记。

带颜色的JVM:三色标记详解

JVM 三色标记法与读写屏障

你对JVM三色标记的理解嘛?

一文搞定垃圾回收的三色标记法

一文搞定垃圾回收的三色标记法