8.垃圾回收
8.1.判断对象回收
1.引用计数法
主要思路:当一个对象被引用,则计数+1,释放引用,计数-1.当计数归零【说明对象不再被引用】则会被回收
缺点: 对象循环依赖
A对象引用B,B引用A。虽然它们都不会被使用。但是引用计数也不会归零!
2.可达性分析法🌟
首先了解一下根对象: 根对象:肯定不能当成垃圾被回收的对象
在JVM进行垃圾回收之前。会对堆内存中的对象进行扫描!
==>如果对象被根对象直接或间接的引用了,那么就不能回收
具体做法
扫描堆中对象,看是否能沿着GC Root
对象为起点的引用链找到该对象,找不到则可以回收
哪些对象可以作为
GC Root
? PS:必须是【活跃】的引用
GCRoot:
- 活动线程(栈帧)中局部变量的引用对象【堆中】
- 类静态变量引用
- 本地方法引用
PS: JVM选择可达性分析来判断对象是否为垃圾
3.四种引用
五种: 强引用、软引用、弱引用、虚引用、终结器引用
图中【实线】表示强引用,虚线为其他引用
强引用
- =赋值的就是强引用
- GC Root沿着强引用能够找到的对象就不会被回收。例如上图
B对象
能够连到A1对象
,则不会被回收 - 只有强引用的引用都断开,才会被回收,例如
A1对象
同时被B、C两个GC Root强引用,只有他们都断开才会被回收
软/弱引用
- 当
软/弱引用
所引用的对象,没有被强引用
所引用,那么在垃圾回收时就可能被回收 - 例如
A2对象
,如果他没有被B对象强引用,那么则可能被回收 - PS:当软/弱引用所引用的对象被回收,则他们会进入引用队列【因为他们本身也是对象】【进入队列后方便后续释放他们的内存】
软引用代码例子:
触发两次Full GC垃圾回收,一次垃圾回收后,内存空间仍然不足,就触发了软引用的垃圾回收!把byte数组回收了
- 引用队列
//关联了引用队列,当软引用所关联的byte[]被回收,软引用自己会加入到queue中去!
SoftReference<byte[]> ref = new SoftReference<>(new byte[],queue);
//...
//手动移除list中无用的软引用【也就是存在队列中的软引用】
Reference<? extends byte[]> poll = queue.poll();
while(poll != null){
list.remove(poll);
poll=queue.poll();
}
虚引用
例子:直接内存ByteBuffer
- 在创建ByteBuffer时就会关联一个Cleaner对象。
- 当虚引用关联的对象
ByteBuffer
被垃圾回收,则虚引用入队列! - 此时会有一个ReferenceHandler的线程定时进入队列查找,查看是否有新入队的Cleaner,如果有则调用方法去释放直接内存!
终结器引用(FinalReference)
Object.finalize()
方法【终结方法】
例子:当对象A4
重写了Object的终结方法finalize()
- 当没有强引用引用对象A4时,虚拟机会为对象创建对应的终结器引用
- 当对象将要被垃圾回收,则将终结器引用加入引用队列【此时A4还没被垃圾回收】
- 由一个优先级很低的线程去队列中查看是否有终结器引用,如果有,则会调用该对象的
finalize()
方法 - 调用完方法后该对象就会在下一次垃圾回收被回收
- 不推荐使用【finalize】因为可能迟迟无法释放内存
8.2.垃圾回收算法
1.标记清除
根据可达性分析,将没有被GC Root直接或间接引用的对象做标记,然后清楚这些被特殊标记的对象
- 优点:速度快
- 缺点:容易产生内存碎片!
由于清除后,空余内存空间不连续!那么当我们要申请一个较大的对象内存空间时,可能无法申请!
2.复制算法
解决【标记清除】缺点【内存碎片】问题,可以使用复制算法
直接把【标记】存活的对象【复制】到另一块空间,复制完了,把原有的整块空间干掉!这样就没有内存碎片了
缺点:
- 内存利用率低!
- 占用双倍内存空间
适用于【存活较少】
3.标记整理
将标记出来存活的内存移动到一边,垃圾内存移动到另一边,这就叫【整理】
再将垃圾一起删除!就没有内存碎片问题了!剩下的就是连续的内存空间
缺点:
- 相较于【标记清除】效率较低
适用于【存活较多】
4.三种总结
- 标记清除 Mark Sweep
- 速度快
- 会造成内存碎片问题
- 复制算法 Copy
- 不会有内存碎片
- 占用两倍内存空间
- 标记整理 Mark Compact
- 速度慢
- 没有内存碎片
JVM会结合多种垃圾回收算法进行
8.3.分代垃圾回收
分代原因
经研究表明:大部分对象的生命周期很短!只有少部分对象可能会存活较长时间
又由于【垃圾回收】会导致【stop the world】(应用停止访问)
stop the word:回收垃圾时,程序有短暂的时间不能正常继续运作。
为了使【stop the world】持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率
新生代gc引发的【stop the world】时间很短,因为新生代大部分为垃圾!
很多垃圾收集器都会在【物理】或者【逻辑】上,划分出两类对象,死的快的叫【新生代】,活的久的叫【老年代】
针对不同区域执行不同的垃圾回收算法====>更有效的管理
新生代:垃圾回收频繁
老年代:垃圾回收不频繁
新生代
【新生代】的垃圾收集器使用的都是【标记复制算法】==>年轻的大部分存活时间短,能发挥复制算法最大效率
所以【堆内存】划分时要将新生代划分出Survivor区【from和to】目的就是有一块完整的内存空间供垃圾回收器进行拷贝(复制)
新产生的对象则放入Eden区(伊甸园)
- 实际比例 新生代(8:1:1)
Minor GC
当【eden】区空间不足!则会触发新生代的垃圾回收:Minor GC
使用的是【标记复制算法】,将垃圾回收后存活下来的对象复制到survivor to
区域
之后再交换survivor from
和survivor to
的位置,以便下次垃圾回收
Minor GC会去清除【伊甸园】和【survivor from】中的对象 每次垃圾回收完毕,必须保证
survivor to
区域为空!
晋升老年代
- 每次Minor GC后对象的年龄+1 【达到一定年龄后会进入老年代】【默认为15岁】
- 可设置的最大年龄也是15,因为只提供了4bit存储年龄===> 最大1111=>15
- 对象太大了!【Survivor区没法存下该对象】
卡表
新生代的MinorGC需要进行【标记】动作
可达性分析:从【GC Root】开始遍历,能被遍历到的对象会存活
那么!【分代】了之后,就有可能会遍历到老年代的对象!
- 一般情况下
JVM一般会有一条分界线,【分割新生代/老年代】,可以通过地址判断对象在哪个分代上
只要遍历到老年代对象就会停止! - 问题:万一有老年代对象引用了新生代怎么办?
HotSpot虚拟机下有【card table】卡表,来避免全局扫描【老年代】对象
【卡表】由多个【卡页】组成。当【卡页】对应的对象出现【跨代引用】,
则将该页标记为【dirty】
有了【卡表】后,每次Minor GC的时候只需要去【卡表】找到【dirty】然后将其引用的新生代对象加入GC Root即可,就不用遍历整个【老年代】对象了!
老年代
采用算法:标记+清除/标记+整理
通过上面的条件晋升到老年代
当老年代空间不足时,触发Full GC!
步骤
- 对象首先创建在伊甸园区
- 新生代空间不足时,触发minor gc,伊甸园和from存活到对象使用【标记复制】算法复制到to中,存活的对象年龄+1,并且交换from和to
- 垃圾回收时引发【stop the world】
- 当对象过大,或寿命超过阈值,会晋升老年代
- 老年代空间不足,触发full gc 【stw时间长】
8.4.相关VM参数
含义 | 参数 |
---|---|
堆初始大小 | -Xms |
堆最大大小 | -Xmx / -XX:MaxHeapSize=size |
新生代大小 | -Xmn / (-XX:NewSize=size + -XX:MaxNewSize=size) |
幸存区比例(动态) | -XX:InitialSurvicorRatio=ratio / -XX:+UseAdaptiveSizePolicy |
幸存区比例 | -XX:SurvivorRatio=ratio |
晋升阈值 | -XX:MaxTenuringThreshold=threshold |
晋升详情 | -XX:+PrintTenuringDistribution |
GC详情 | -XX:+PrintGCDetails -verbose:gc |
FullGC 前 MinorGC | -XX:+ScavengeBeforeFullGC |
8.5.GC分析
代码演示
public class GCDemo01 {
private static final int _512KB = 512 * 1024;
private static final int _1MB = 1024 * 1024;
private static final int _6MB = 6 * 1024 * 1024;
private static final int _7MB = 7 * 1024 * 1024;
private static final int _8MB = 8 * 1024 * 1024;
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
}
}
在无任何对象情况下运行,打印出了堆内存占用情况:
PS:Metaspace元空间并不是堆内存的一部分,只是这里打印出来了方便分析
- 然后我们试着创建一个对象,存放一个7Mb的byte数组,此时伊甸园空间不足了
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}
出现了一次GC信息,可以发现from
区占用变多了,这就是gc之后存活下来的对象,而伊甸园此时存放了7MB的byte数组
- 接着我们再次放入两个512k的数组
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
list.add(new byte[_512KB]);
}
我们可以看到触发了两次GC,且由于新生代survivor放不下gc后的存活对象了【7mb】,将其晋升到老年代中了!
大对象晋升
已知新生代:10M:8:1:1,那么将无法存放8MB的数组,将会直接存入老年代!
// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
ArrayList<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
且没有触发gc
8.6.垃圾回收器
简介
HotSpot所包含的垃圾收集器
两个收集器之间存在连线,说明他们可以搭配使用!
历史:
- 先有Serial收集器【串行】,人们觉得效率低
- 然后开发了Parallel【并行】
- 之后开发了CMS【老年代】收集器,他不处理年轻代,所以需要搭配一些年轻代的垃圾处理器
- 由于他和
Parallel Scavenge
无法搭配,所以开发了[ParNew]
- 由于他和
区域划分:
**新生代收集器**:Serial、ParNew、Parallel Scavenge
**老年代收集器**:CMS、Serial Old、Parallel Old
**整堆收集器**: G1
机制划分:
**串行:** SerialGC
**并行:** ParallelGC
**并发:** CMS/G1
一些关键词介绍:
- 安全点(Safe-point):程序执行时并非在所有地方都能停顿下来开始 GC,只有在某些特定的位置才可以,这些特定的位置被称为安全点(Safe-point)
- 并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个CPU上。
- 吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。
例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
1.Serial
Serial收集器是最基本的、发展历史最悠久的收集器。
特点:单线程、简单高效(与其他收集器的单线程相比)。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
应用场景:适用于Client模式下的虚拟机。
开启:-XX:+UseSerialGC=Serial+SerialOld
2.ParNew
ParNew收集器起始就是Serial收集器的多线程版本 除了使用多线程外其余行为均和Serial收集器一模一样
特点:
- 多线程、和Serial收集器一样存在Stop The World问题
- 除了
Serial
收集器以外,唯一能和CMS配合工作的
开启:-XX:+UseParNewGC
参数:可以使用-XX:ParallelGCThreads
参数来限制垃圾收集的线程数
3.Parallel Scavenge
- 吞吐量优先收集器
特点:
- 新生代收集器—-复制算法—-并行多线程
- GC自适应调节策略
GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
开启:-XX:+UseParallelGC
/-XX:UseParallelOldGC
4.Serial Old
是Serial收集器的老年代版本
特点:
- 单线程+标记整理
- 作为CMS的后备方案,在Concurrent Mode Failure时使用
5.Parallel Old
是Parallel Scavenge收集器的老年代版本。
特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
6.CMS收集器
老年代
CMS垃圾回收器 : ConcurrentMarkSweep=>基于并发的标记清除垃圾回收器
CMS的特点是尽可能减少【Stop the world】时间。在垃圾回收时让用户线程和GC线程能够并发执行!
因为使用Seria和Parallel系列的垃圾收集器:都会造成阻塞!
开启:-XX:+UseConcMarkSweepGC
相关指令:
-XX:CMSInitiatingOccupancyFraction=percent
当内存占比达到percent
就进行垃圾回收
由于CMS垃圾回收器会并发的进行清理,那么工作线程就会产生新的垃圾,如果每次都等到堆内存不足才进行垃圾回收,那这些新产生的“浮动垃圾”就没有地方放了!
注意:当保留空间放不下浮动垃圾时!CMS会退化为SerialOld进行垃圾回收
-XX:+CMSScavengeBeforeRemark
在重新标记前对新生代进行垃圾回收
重新标记要进行【可达性分析】而新生代的对象可能会引用老年代对象,这些新生代的其他对象大部分是要被回收的,那么就会使【重新标记】浪费时间!所以该指令减轻了【重新标记】的工作量
初始标记时会阻塞,后续可以并发执行。但是重新标记时会阻塞【由于工作线程导致的地址变动】
- 初始标记:标记GC Roots能直接关联的对象。速度很快但是仍存在Stop The World问题。
- 并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
- 并发预标记【指令设置】:由于并发标记可能会有对象发生变化,需要进行【重新标记】,而【并发预标记】目的在于减少【重新标记】耗时,主要方法在于在重新标记前对新生代进行垃圾回收
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
- 并发清除:对标记的对象进行清除回收,由于是并发的,可能产生【浮动垃圾】
缺点:
- 对CPU资源非常敏感。
- 空间需要预留:预留给浮动垃圾
- 浮动垃圾放不下,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
- 这时将启动应急预案,启用Serial Old 进行垃圾回收,停顿时间会变长。所以-XX:CMSInitiatingOccupancyFraction 参数的值设置的太高,会导致频繁“Concurrent Mode Failure”失败,性能反而降低。
- 内存碎片问题:因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。
8.7.G1收集器
1.简介
Garbage First, 现代垃圾回收器
- JDK9 之后 成为默认的垃圾回收器
为什么诞生G1?
- 现代的堆内存越来越大,G1适合超大堆内存
- 内存越大,传统收集器STW时间就会无限增长!
- G1不要求每次都把垃圾清理的干干净净,它只是努力做它认为对的事情【预计stw时间】
2.特点
- 同时注重【吞吐量】和【低延迟】
- 适合超大堆内存,会将堆划分为多个大小相等的
Region
区域 - 整体上是【标记+整理】算法,两个区域之间是【复制算法】
- 可以设定最大STW停顿时间🌟
-XX:MaxGCPauseMills=N
- 250 by default
3.内存布局
G1打破了之前那种连续的堆内存分布!===>为了实现STW时间可预测,并且堆内存过大,收集时间长
将堆分成若干个等大的区域Region
-XX:G1HeapRegionSize=N
2048 by default
PS:H为大尺寸对象,G1认为超过Region容量一半的对象即大尺寸对象 一旦发现没有引用指向大对象,就直接在Young GC中回收
4.GC模式
YoungGC
在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到另一部分的Survivor区或者晋升老年region。
- 【根扫描】,【Stop the world】,扫描GC Roots对象。
- 【更新RSet】处理Dirty card,更新RSet.
- 【处理RSet】扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。【标记为存活】
- 【复制清除】拷贝扫描出的存活的对象到survivor2/old区【复制清除】【年龄+1】
- 【处理引用队列】,软引用,弱引用,虚引用
MixedGC
当堆空间的内存占用达到阈值(-XX:InitiatingHeapOccupancyPercent,默认45%),触发MixedGC
在G1收集器出现之前的所有 其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老 年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任 何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
当越来越多的对象【晋升】到老年代(old region),为了避免堆内存被耗尽,就会触发mixed gc
- 初始标记:仅标记GCRoot直接关联的对象,该过程需要【STW】但是耗时很短!
- 并发标记:并发的对堆中对象进行可达性分析,由于是并发的,可能会造成【漏标】问题,G1会使用SATB(snapshot-at-the-beginning)原始快照算法来解决
- 最终标记:对用户线程做短暂的暂停,处理【并发标记】时发生变化的对象(包括SATB)
- 筛选回收:对各Region的回收价值和回收成本进行排序,根据用户期望的停顿时间来回收,【回收一部分Region】
通过【复制】来进行,将回收后存活的复制到【空Region】
除了回收【整个young region】,还会回收【一部分的old region】,
这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。
FullGC
G1本没有FullGC这个概念,但是在某些必要的条件下:对象分配速度远大于回收速度
则会触发FullGC,调用serialOldGC进行全堆扫描
5.记忆表/卡表
这里引出一个问题:
跨代引用:由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么回收新生代的话,需要扫描所有从老年代到新生代的所有引用,所以要避免每次YoungGC时扫描整个老年代,减少开销。
===>使用Card Table+Remember Table来避免不必要的扫描
Card Table:
将一个Region分成若干个Card,一旦这个**Card中有对象引用了其他Region的对象**。
于是将这个Card标记为【Dirty】(脏卡)
Remember Set:
如果**当前Region中有对象被其他Region引用**,则RSet会将这个引用记录下来
**记录引用来源的Card!**【记录其他Region引用了当前Region的对象关系】
===> RSet只保存来自【老年代】的引用(因为年轻代没必要存储来自年轻代的引用,而老年代在进行回收前会先进行YoungGC,所以没必要保存年轻的的引用)
这样在进行YoungGC时就不需要扫描全老年代了,只需要根据RSet找到对应Region中的【dirty card】进行扫描即可!
PS:在进行引用改变时,会有一个Write Barrier【写屏障】来维护Rset以及Card Table的
相关参数
-XX:+UseG1GC:设置使用G1垃圾回收器
-XX:MaxGCPauseMills:设定系统停顿时间,默认200ms
-XX:G1HeapRegionSize:设置区域划分的个数和大小,默认堆大小/2048
-XX:G1NewSizePercent:用来设置新生代初始占比的,默认值为5%即可。
-XX:G1MaxNewSizePercent:用来设置新生代最大占比的,默认值为60%即可。
-XX:SurvivorRatio=8:设置新生代Region区域中Eden和Survivor的比例,默认8:1:1
-XX:InitiatingHeapOccupancyPercent:设置Mixed GC的比例,默认45%
-XX:G1MixedGCCountTarget:混合回收阶段最多允许G1执行回收的次数,默认是8次。
-XX:G1HeapWastePercent:默认值5%,Mixed GC时空出来的Region大于5%,就停止混合回收。
-XX:G1MixedGCLiveThresholdPercent:默认值是85%,确定要回收的Region的时候,必须是存活对象低于85%的Region才可以回收。
8.8.FullGC
不同GC产生的情况:
- SerialGC收集器【串行】
- 新生代内存不足:minorGC
- 老年代内存不足:FullGC
- ParallelGC【并行】
- 新生代内存不足:minorGC
- 老年代内存不足:FullGC
- CMS【并发】
- 新生代内存不足:minorGC
- 当并发清除失败时:导致FullGC
- G1【并发】
- 新生代内存不足: minorGC
- 回收速度低于垃圾产生速度:导致FullGC
FullGC的stop the world时间很长,所以在CMS和G1中只有满足特定条件才去触发FullGC
8.9.三色标记
1.场景
垃圾回收算法包括:标记-清除,标记-复制,标记-整理。
在此基础上增加分代,采取不同的算法。
无论使用哪种算法,标记总是必要的一步!
2.标记算法
可达性分析:从GC Root开始遍历,可达的则为存活对象:
我们把对象分为三种情况:
- 白色:未访问【最终白色会作为垃圾】
- 黑色:已经处理完毕
- 灰色:正在处理【还没访问完毕】
PS:标记结束后,白色的对象意味着“找不到”,不会被引用了。则作为垃圾清除
上诉标记在【Stop the World】情况下是正常进行的,
但是在并发情况下,可能会造成对象的引用发生改变。可能会导致【多标】和【漏标】
3.多标情况
多标—产生【浮动垃圾】
例子:
标记过程中,当已经标记E为灰色,此时【D断开了对E的引用】
那么按理来说:已经断开了引用,那么E、F、G需要被回收
但是:E已经被标记了,之后F、G也会可达,所以在本次清除【不会被回收】
- 这样产生的对象和标记开始后产生的新对象被称为——【浮动垃圾】
4.漏标情况
漏标—对象消失
例子:
在标记到E对象,还没有对G对象进行标记时,E断开了对G的引用。那么将不会遍历到G
但是此时用户线程又将D对象引用指向G。
这样最终会导致:G被作为白色【垃圾】清除了!
触发条件:
(1)灰色对象 【断开】白色对象的引用(直接或间接)
(2)黑色对象 【重新引用】该白色对象
从代码角度看分为三步:
1.读取 对象E引用的对象G 【读取】
2.对象E 将对对象G的引用置null 【写入】
3.对象D 引用对象G 【写入】
我们需要对以上三步做一些手脚,来解决该问题
5.写屏障
Store Barrier
所谓【写屏障】就是在【写操作】前后,加一些处理。类似AOP
pre_write_barrier(); //写前
//写操作
post_write_barrier(); //写后
写屏障+SATB
原始快照 Snapshot At the Beginning
就是当对象E的引用发生改变时,将E之前引用的对象G记录下来
【当原来成员变量的引用发生变化之前,记录下原来的引用对象】
这种做法的思路是:尝试保留开始时的对象图,即原始快照
- 保存下来的快照,保证其最后会被扫描!
该方法破坏了触发条件一:灰色对象 【断开】白色对象的引用,从而保证不会漏标
写屏障+增量更新
当对象的成员变量有【新引用】时。利用写屏障将其记录下来
【当有新引用插入进来时,记录下新的引用对象】
这种做法的思路是:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)。
该方法破坏了触发条件二:黑色对象 【重新引用】该白色对象,从而保证不会漏标
6.读屏障
该方法针对【读取】对象
- 当第一次读取对象时,一律记录下来
这种做法是【保守】的,也是【安全】的。
因为条件二中【黑色对象 重新引用了 该白色对象】,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。
7.应用
【可达性分析】的垃圾回收器几乎都借鉴了三色标记的算法思想,
尽管实现的方式不尽相同:比如白色/黑色集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通 过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等。
- CMS:写屏障+增量更新
- G1:写屏障+SATB
- ZGC:读屏障