https://www.cnblogs.com/chenpt/p/9803298.html
先看下G1 垃圾回收器
之前介绍的几组垃圾收集器组合,都有几个共同点:
年轻代、老年代是独立且连续的内存块;
年轻代收集使用单eden、双survivor进行复制算法;
老年代收集必须扫描整个老年代区域;
都是以尽可能少而块地执行GC为设计原则。
G1垃圾收集器也是以关注延迟为目标、服务器端应用的垃圾收集器,被HotSpot团队寄予取代CMS的使命,也是一个非常具有调优潜力的垃圾收集器。虽然G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制,但单纯地以类似前三种的过程描述显得并不是很妥当。事实上,G1收集与以上三组收集器有很大不同:
G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。因此,G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间目标自动调整年轻代和总堆大小,暂停目标越短年轻代空间越小、总空间就越大;
G1采用内存分区(Region)的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩);
G1虽然也是分代收集器,但整个内存分区不存在物理上的年轻代与老年代的区别,也不需要完全独立的survivor(to space)堆做复制准备。G1只有逻辑上的分代概念,或者说每个分区都可能随G1的运行在不同代之间前后切换;
G1的收集都是STW的,但年轻代和老年代的收集界限比较模糊,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分老年代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿
+++++++++++++++++++++++++++++++++++++++++++++++++++
一 G1简介
-
JDK7增加,成为HotSpot重点发展的垃圾回收技术,被HotSpot团队寄予取代CMS的使命,将会被安排成为JDK9的默认垃圾收集器
-
低停顿(stw)、高吞吐;调优简单,设置2个参数
低停顿(stw)、高吞吐;调优简单,设置2个参数
G1是一种分代收集器,只有逻辑上的分代概念,与物理上分代有本质区别
- 年轻代:采用复制算法
- 年老代:标记-清除算法,类似CMS
G1的特点
- G1的设计原则是"首先收集尽可能多的垃圾(Garbage First)"。G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在老年代找出具有高收集收益的分区进行收集。同时G1可以根据用户设置的暂停时间自动调整年轻代和总堆大小,暂停越短年轻代空间越小、总空间就越大
- G1采用内存分区(Region)的思路,回收时则以分区为单位进行回收,存活的对象复制到另一个空闲分区中。由于都是以相等大小的分区为单位进行操作,因此G1天然就是一种压缩方案(局部压缩)
- G1的收集都是STW的,采用了混合(mixed)收集的方式。即每次收集既可能只收集年轻代分区(年轻代收集),也可能在收集年轻代的同时,包含部分年老代分区(混合收集),这样即使堆内存很大时,也可以限制收集范围,从而降低停顿
G1有详细的日志信息,建议使用下面的参数,当G1出问题,可以获取很多有用的信息
- -xx:+PrintGCDateStamps 打印日期和正常云行时间
- -xx:+PrintGCDetails 打印G1详细信息
- -xx:+PrintAdaptiveSizePolicy 打印自适应调节策略;自适应策略:GC会根据中统计的GC时间、吞吐量、内存占用量,重新计算堆内存中各区大小
- -xx:+PrintTenuringDistribution 打印survivor region区域内的对象的age信息
-
G1整个堆空间分成若干个大小相等的内存区域-Regions
-
默认将整堆划分为2048个分区,可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂)
2048个分区,每个分区大小可以通过参数配置
Regions分为
- Eden Regions
- Survivor Regions
- Old Regions
- Humongous Regions
- 当一个对象大于Region大小的50%,称为巨型对象;它就会独占一个或多个Region,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区-Humongous Region
Humongous 分区依旧是老年代的分区
Card
- 很小的内存区域,G1将Java堆划分为相等大小的一个个区域,这个小的区域大小是512 Byte,称为Card
- Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用这个数组映射着每一个Card
- Card中对象的引用发生改变时,Card在Card Table数组中对应的值被标记为dirty,就称这个Card被脏化了
- 分配对象会占用物理上连续若干个卡片
G1保留了分代的概念,但是年轻代和年老代不再是物理上的隔离,他们都是一部分的Regions(不需要连续)的集合,每个Region都可能随G1的运行在不同代之间切换
- 年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲Region加入到年轻代空间。
- 整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。
TLAB
- G1中每个线程本地的内存分配不需要顾及分区是否连续
三 G1工作解析
1 分配内存
- 当jvm开始运行时,堆内存开始分区,分成若干个大小相等的Regions
- 新的对象被分配到Eden Region,一个Eden Region满了之后,分配下一个Eden Region
- 当所有的Eden Regions都满了,就进行一次Young GC
-
Old Region中的对象也会指向了Eden Region中的对象:例如 一个"old" Map 存放进了 一个 "new" 的对象
-
- 上图中B指向了E,但是没有其他对象指向B了,显然B和E都是垃圾对象
- Young GC只会清理Eden Region,发现E有引用指向它,就不会去回收E,G1必须追踪这些内部区域之间的引用指向,才能正确的标记存活对象,回收垃圾对象
- 进过Young GCEden之后,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据对象的年龄而晋升到新的survivor分区和老年代分区
Card Table:
- Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用这个数组映射着每一个Card
- Card中对象的引用发生改变时,Card在Card Table数组中对应的值被标记为dirty,就称这个Card被脏化了
- 所以Card Table其实就是映射着内存中的对象,Young GC的时候只需要扫描状态是dirty的card
Remembered Set: RSet
- 每一个Region都有自己的RSet
- RSet里面记录了引用——就是其他Region中指向本Region中所有对象的所有引用,也就是谁引用了我的对象
- RSet其实是一个Hash Table,Key是其他的Region的起始地址,Value是一个集合,里面的元素是Card Table 数组中的index,既Card对应的Index,映射到对象的Card地址。
- 比如A对象在regionA,B对象在regionB,且B.f = A,则在regionA的RSet中需要记录一对键值对,key是regionB的起始地址,Value的值能映射到B所在的Card的地址,所以要查找B对象,就可以通过RSet中记录的卡片来查找该对象
-
本分区对象引用本分区自己的对象,这种引用不用落入RSet中;同时,G1 GC每次都会对年轻代进行整体收集,因此young->old和young->young也不需要在RSet中记录。而对于old->young和old->old的跨代对象引用,需要拥有RSet
-
G1进行GC时,只要扫描本Region中RSet所记录的引用指向的对象是否存活,进而确定本分区内的对象存活情况。而不需要扫描整个堆了。
3 三色标记算法
-
并发标记中的三色标记算法,将对象分成三种类型
- 黑色: 根对象,或者该对象与它的子对象都被扫描
- 灰色: 对象本身被扫描,但还没扫描完该对象中的子对象
- 白色: 未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
并发标记引起的对象丢失问题
- 在标记的同时应用程序也在运行,对象的引用随时会改变。当垃圾收集器扫描到下面情况时:
-
当垃圾收集器扫描到下图时,应用程序执行了A.c=C;B.c=null;
-
那么对象之间的引用就会如下图,A.c = c
因为一开始A.c = c , 后面程序执行,B.c = null, 垃圾回收器再次扫描,标记c为白色
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
并发标记引起的对象丢失问题在CMS和G1的2种不同解决方式
-
栅栏 Barrier:栅栏是指在原生代码片段中,当某些语句被执行时,栅栏代码也会被执行。而G1主要在赋值语句中,使用写前栅栏(Pre-Write Barrrier)和写后栅栏(Post-Write Barrrier)。
- 写前栅栏 Pre-Write Barrrier:即将执行一段赋值语句时,等式左侧对象将修改引用到另一个对象,那么JVM就需要在赋值语句生效之前,记录丧失引用的对象。JVM并不会立即维护RSet,而是通过批量处理,在将来RSet更新(见SATB)。
- 写后栅栏 Post-Write Barrrier:当执行一段赋值语句后,等式右侧对象获取了左侧对象的引用,那么等式右侧对象所在分区的RSet也应该得到更新。同样为了降低开销,写后栅栏发生后,RSet也不会立即更新,同样只是记录此次更新日志,在将来批量处理(见Concurrence Refinement Threads)。
- 在CMS中:只要在写栅栏(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的
- 在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤
- 在开始标记的时候生成一个快照图,标记存活对象
- 在并发标记的时候,在写前栅栏,记录所有引用被改变了的对象,再把这些对象都变成非白的
- 这样可能会生成游离的垃圾对象,没关系,将在下次收集周期被收集
A->B修改为A->C, 那么不会立即维护RSet,而是先记录丧失引用的对象B到更新日志,未来批量处理
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
起始快照算法
起始快照算法 Snapshot at the beginning (SATB)
Taiichi Tuasa贡献的增量式完全并发标记算法起始快照算法(SATB),主要针对标记-清除垃圾收集器的并发标记阶段,非常适合G1的分区块的堆结构,同时解决了CMS的主要烦恼:重新标记暂停时间长带来的潜在风险。
SATB会创建一个对象图,相当于堆的逻辑快照,从而确保并发标记阶段所有的垃圾对象都能通过快照被鉴别出来。当赋值语句发生时,应用将会改变了它的对象图,那么JVM需要记录被覆盖的对象。因此写前栅栏会在引用变更前,将值记录在SATB日志或缓冲区中。每个线程都会独占一个SATB缓冲区,初始有256条记录空间。当空间用尽时,线程会分配新的SATB缓冲区继续使用,而原有的缓冲去则加入全局列表中。最终在并发标记阶段,并发标记线程(Concurrent Marking Threads)在标记的同时,还会定期检查和处理全局缓冲区列表的记录,然后根据标记位图分片的标记位,扫描引用字段来更新RSet。此过程又称为并发标记/SATB写前栅栏。
并发优化线程
并发优化线程 Concurrence Refinement Threads
G1中使用基于Urs Hölzle的快速写栅栏,将栅栏开销缩减到2个额外的指令。栅栏将会更新一个card table type的结构来跟踪代间引用。
当赋值语句发生后,写后栅栏会先通过G1的过滤技术判断是否是跨分区的引用更新,并将跨分区更新对象的卡片加入缓冲区序列,即更新日志缓冲区或脏卡片队列。与SATB类似,一旦日志缓冲区用尽,则分配一个新的日志缓冲区,并将原来的缓冲区加入全局列表中。
并发优化线程(Concurrence Refinement Threads),只专注扫描日志缓冲区记录的卡片来维护更新RSet,线程最大数目可通过-XX:G1ConcRefinementThreads(默认等于-XX:ParellelGCThreads)设置。并发优化线程永远是活跃的,一旦发现全局列表有记录存在,就开始并发处理。如果记录增长很快或者来不及处理,那么通过阈值-X:G1ConcRefinementGreenZone/-XX:G1ConcRefinementYellowZone/-XX:G1ConcRefinementRedZone,G1会用分层的方式调度,使更多的线程处理全局列表。如果并发优化线程也不能跟上缓冲区数量,则Mutator线程(Java应用线程)会挂起应用并被加进来帮助处理,直到全部处理完。因此,必须避免此类场景出现。
并发标记周期
并发标记周期 Concurrent Marking Cycle
并发标记周期是G1中非常重要的阶段,这个阶段将会为混合收集周期识别垃圾最多的老年代分区。整个周期完成根标记、识别所有(可能)存活对象,并计算每个分区的活跃度,从而确定GC效率等级。
当达到IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,便会触发并发标记周期。整个并发标记周期将由初始标记(Initial Mark)、根分区扫描(Root Region Scanning)、并发标记(Concurrent Marking)、重新标记(Remark)、清除(Cleanup)几个阶段组成。其中,初始标记(随年轻代收集一起活动)、重新标记、清除是STW的,而并发标记如果来不及标记存活对象,则可能在并发标记过程中,G1又触发了几次年轻代收集。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-
Collection Set: CSet
- 它记录了GC要收集的Regions集合
- 在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲Region中。
- CSet包括需要收集的Eden Regions、Survivor Regions,而且还包括部分(1/8)Old Regions
-
G1也有类似CMS的收集动作:初始标记、并发标记、重新标记、清除、转移回收,并且也以一个串行收集器做担保机制
-
可以看出:初始标记、重新标记、清除、转移回收会造成stw
-
年轻代收集
- 年轻代收集会,不会进行并发标记,所以它全程都是STW
- 应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集
- 工作过程
- 根扫描 Root Scanning:静态和本地对象等被扫描
- 更新已记忆集合 Update RSet:对dirty卡片的分区进行扫描,来更新RSet
- RSet扫描:在收集当前CSet之前,扫描CSet分区的RSet,检测old->young这种引用情况
- 转移和回收-Object Copy:讲CSet分区存活对象的转移到新survivor或old Region,回收CSet内垃圾对象
- 引用处理:主要针对软引用、弱引用、虚引用、final引用、JNI引用;当占用时间过多时,可选择使用参数-XX:+ParallelRefProcEnabled激活多线程引用处理
-
在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;原有Survivor分区存活的对象,将根据对象的年龄而晋升到新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。
-
-
-
年老代收集
- 当堆内存占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会进行年老代收集
- 在年轻代收集之后或巨型对象分配之后,会去检查这个空间占比
- 年老代收集同时会执行年轻代收集,进行年老代的roots探测,既初始标记,stw
- 然后恢复应用线程,进行年老代并发标记
- stw,重新标记
- STAB处理
- 引用处理
- 继续stw,清除垃圾
- 恢复应用线程
- 当堆内存占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会进行年老代收集
混合收集
- 在进行正常的年轻代垃圾收集,也会回收一部分老年代分区。会优先选取垃圾多(垃圾占用大于85%,复制算法存活对象越少效率越高)的Regions,一共1/8的年老代Regions加入Cset中
- 假设一个Region的存活对象达到95%,而进行复制,效率很低,所以G1允许浪费部分内存,那么这个Region不会被混合收集,-XX:G1HeapWastePercent:默认5%
- stw,然后将Cset中的Regions进行收集,使用复制算法
- 下一次年轻代垃圾收集进行时,在将第二个1/8的年老代Regions加入Cset中进行收集
- 当年老代内单个Region的垃圾小于等于G1HeapWastePercent时,复制大量存活对象,效率很低。此时G1会确定结束混合收集周期。所以混合收集次数可能小于8次。
转移失败的担保机制 Full GC
- 当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。
- G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure
- 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
- 从老年代分区转移存活对象时,无法找到可用的空闲分区
- 分配巨型对象时在老年代无法找到足够的连续分区
- 由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。
5 其他
- 避免Full GC
- -xx:+PrintAdaptiveSizePolicy 打印自适应调节策略,当出现Full GC的时候可以看到相关信息
- 避免内存空间枯竭,根据情况增加G1堆内存
- 避免过多巨型对象分配
- 避免"引用处理"过程耗时过长
- 引用处理默认是stw、单线程,可选择使用参数-XX:+ParallelRefProcEnabled激活多线程引用处理
- 找出弱引用过多的原因
- 设置合理的暂停目标,从下面例子可以看出并不是越短的暂停越好,而是需要根据实际情况取一个合理的值; 吞吐量与暂停目标对应关系,
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
为了解决这个问题,STAB的做法在GC开始时对内存进行一个对象图的逻辑快照(snapshot),通过GC Roots tracing 参照并发标记的过程,只要被快照到对象是活的,那在整个GC的过程中对象就被认定的是活的,即使该对象的引用稍后被修改或者删除。同时新分配的对象也会被认为是活的,除此之外其它不可达的对象就被认为是死掉了。这样STAB就保证了真正存活的对象不会被GC误回收,但同时也造成了某些可以被回收的对象逃过了GC,导致了内存里面存在浮动的垃圾(float garbage)。
其特点如下:
-
并行性:G1在回收期间,可以由多个GC线程同时工作,有效利用多核计算能力。
-
并发性:G1拥有与应用程序交替执行的能力,因此一般来说,不会在整个回收期间完全阻塞应用程序。
-
分代GC:与之前回收器不同,其他回收器,它们要么工作在年轻代要么工作在老年代。G1可以同时兼顾年轻代与老年代。
-
空间整理:G1在回收过程中,会进行适当的对象移动,不像CMS,只是简单的标记清除,在若干次GC后CMS必须进行一次碎片整理,G1在每次回收时都会有效的复制对象,减少空间碎片。
-
可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,这样缩小了回收范围,因此对于全局停顿也能得到更好的控制。
G1的收集过程可能有4个阶段:
-
新生代GC
-
并发标记周期
-
混合收集
-
(如果需要)进行Full GC。
并发标记周期可分为以下几步:
-
初始标记:标记从根节点直接可达的对象。这个阶段会伴随一次新生代GC,它是会产生全局停顿的,应用程序在这个阶段必须停止执行。
-
根区域扫描:由于初始标记必然会伴随一次新生代GC,所以在初始化标记后,eden被清空,并且存活对象被移到survivor区。在这个阶段,将扫描由survivor区直接可达的老年代区域,并标记这些直接可达的对象。这个过程是可以和应用程序并发执行的。但是根区域扫描不能和新生代GC同时发生(因为根区域扫描依赖survivor区的对象,而新生代GC会修改这个区域),故如果恰巧此时需要新生代GC,GC就需要等待根区域扫描结束后才能进行,如果发生这种情况,这次新生代GC的时间就会延长。
-
并发标记:和CMS类似,并发标记将会扫描并查找整个堆的存活对象,并做好标记。这是一个并发过程,并且这个过程可以被一次新生代GC打断。
-
重新标记:和CMS一样,重新标记也是会使应用程序停顿,由于在并发标记过程中,应用程序依然运行,因此标记结果可能需要修正,所以在此阶段对上一次标记进行补充。在G1中,这个过程使用SATB(Snapshot-At-The-Begining)算法完成,即G1会在标记之初为存活对象创建一个快照,这个快照有助于加速重新标记的速度。
-
独占清理:顾名思义,这个阶段会引起停顿。它将计算各个区域的存活对象和GC回收比例并进行排序,识别可供混合回收的区域。在这个阶段,还会更新记忆集。该阶段给出了需要被混合回收的区域并进行了标记,在混合回收阶段,需要这些信息。
-
并发清理阶段:识别并清理完全空闲的区域。它是并发的清理,不会引起停顿。
五、必要时的Full GC
和CMS类似,并发收集让应用程序和GC线程交替工作,因此在特别繁忙的情况下无可避免的会发生回收过程中内存不足的情况,当遇到这种情况,G1会转入一个Full GC 进行回收。
以下4种情况会触发这类的Full GC:
2、晋升失败
(to-space exhausted或者to-space overflow)
G1收集器完成了标记阶段,开始启动混合式垃圾回收,清理老年代的分区,不过,老年代空间在垃圾回收释放出足够内存之前就会被耗尽。(G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用),由此触发了Full GC。
下面日志中(可以在日志中看到(to-space exhausted)或者(to-space overflow)),反应的现象是混合式GC之后紧接着一次Full GC。
这种失败通常意味着混合式收集需要更迅速的完成垃圾收集:每次新生代垃圾收集需要处理更多老年代的分区。
解决这种问题的方式是:
-
增加 -XX:G1ReservePercent选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。
-
通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
-
也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。
十、GC 发展趋势
其实可以看到Java 垃圾回收器的趋势,就是在大内存堆的前提下尽 GC 可能的降低对应用程序的影响;从 CMS 的分阶段增量标记,到 G1 通过 SATB 算法改正 remark 阶段的 Stop The World 的影响,再到 ZGC/C4甚至在标记阶段无需 Stop The World,莫不如此。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
SATB详解:
- SATB是维持并发GC的一种手段。G1并发的基础就是SATB。SATB可以理解成在GC开始之前对堆内存里的对象做一次快照,此时活的对像就认为是活的,从而开成一个对象图。
- 在GC收集的时候,新生代的对象也认为是活的对象,除此之外其他不可达的对象都认为是垃圾对象。
- 如何找到在GC过程中分配的对象呢?每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。
- 通过这种方式我们就找到了在GC过程中新分配的对象,并把这些对象认为是活的对象。
- 解决了对象在GC过程中分配的问题,那么在GC过程中引用发生变化的问题怎么解决呢?
- G1给出的解决办法是通过Write Barrier。Write Barrier就是对引用字段进行赋值做了额外处理。通过Write Barrier就可以了解到哪些引用对象发生了什么样的变化。
- mark的过程就是遍历heap标记live object的过程,采用的是三色标记算法,这三种颜色为white(表示还未访问到)、gray(访问到但是它用到的引用还没有完全扫描)、back(访问到而且其用到的引用已经完全扫描完)。
- 整个三色标记算法就是从GC roots出发遍历heap,针对可达对象先标记white为gray,然后再标记gray为black;遍历完成之后所有可达对象都是balck的,所有white都是可以回收的。
- SATB仅仅对于在marking开始阶段进行“snapshot”(marked all reachable at mark start),但是concurrent的时候并发修改可能造成对象漏标记。
- 对black新引用了一个white对象,然后又从gray对象中删除了对该white对象的引用,这样会造成了该white对象漏标记。
- 对black新引用了一个white对象,然后从gray对象删了一个引用该white对象的white对象,这样也会造成了该white对象漏标记。
- 对black新引用了一个刚new出来的white对象,没有其他gray对象引用该white对象,这样也会造成了该white对象漏标记。