垃圾回收算法
概述
两类不同的垃圾回收算法
- 依据:[依据判断对象消亡的方式划分的两类垃圾回收算法]{.red}
- 分类:
- 引用计数式垃圾收集(Referrence Counting)
- 追踪式垃圾收集(Tracing GC)
- 细节:Java 虚拟机中采用的是追踪式垃圾收集,所有以下所有算法都是基于追踪式垃圾收集
:::info
① 希望了解更多关于垃圾回收算法的相关内容可以阅读《垃圾回收算法手册》
② 不建议全部读完,挑自己有一定疑惑的部分阅读是非常有帮助的
:::

引用计数式垃圾收集
引用计数法
名称:引用计数法(Referrence Counting)
代表语言:Python、JavaScript([Java 采用的不是引用计数法]{.red})
定义:
- [为每个对象都维护相应的引用计数器,对象的引用增加时,计数器增加;对象的引用减少时,计数器减少]{.blue}
- [如果引用计数器归零,那么该对象就会被回收,如果引用计数器不为零,那么对象就继续存活]{.blue}
特点:[每个对象拥有的计数器通常保存在自己的对象头中]{.orange}
- CPython 中将引用计数器保存在对象头中
- C++ 中的有些库将引用计数器保存在对象之外
优点:
- [对象在成为垃圾之后能够 立刻 被垃圾回收器回收]{.red}
- [立刻回收垃圾不代表引用计数法不会造成 STW 延迟]{.red}
- [引用计数器保存在对象头中,即使垃圾回收器部分出现故障,对象依然可以被回收]{.red}
- [对象在成为垃圾之后能够 立刻 被垃圾回收器回收]{.red}
缺点:
[每个对象的引用数量过多时会造成大量的空间开销]{.green}
[每次维护(增加/删除)对象的引用数量时具有的操作开销]{.green}
- [多线程环境下需要确保增加/删除操作的原子性,即需要对操作进行同步]{.aqua}
- [对象的引用变化十分频繁,相应的增加/删除的操作也非常的频繁]{.aqua}
[难以解决的循环引用问题]{.green}
[某些极端情况下会导致递归删除引用,造成比追踪式垃圾收集更长的延迟]{.green}
循环引用问题
问题描述
- 变量 A 引用的对象实例引用了变量 B 引用的对象
- 变量 B 引用的对象实例又引用了变量 A 引用的对象
问题图示:
解决方式:
- 手动去除两个对象实例之间的循环引用
- [采用引用计数+追踪式结合的方式回收]{.red}
- 引用计数收集大多数没有出现循环引用的对象
- 追踪式垃圾收集处理偶尔发生的循环引用的对象
- [引入弱引用的概念解决循环引用:所有可达对象均为强引用,强引用不产生环]{.red}
- 维护引用计数器的时候会更改引用的强弱程度,处于弱引用的对象就会被直接回收
- 弱引用解决循环引用问题并不是特别的安全,会导致有些对象被提前回收(?)
- 实验删除算法(?)
测试:Java 是否采用引用计数法
// 测试: Java 是否采用引用计数算法
public class ReferrenceCountingGC
{
// 成员变量:
private Object referrence;
// 占用内存的对象: 确保触发内存回收机制
private byte[] bigSize = new byte[1024 * 1024];
public static void main(String[] args)
{
// 变量 A 引用对象实例
ReferrenceCountingGC objA = new ReferrenceCountingGC();
// 变量 B 引用对象实例
ReferrenceCountingGC objB = new ReferrenceCountingGC();
// A 引用的对象实例去引用 B 引用的对象实例
objA.referrence = objB;
// B 引用的对象实例去引用 A 引用的对象实例
objB.referrence = objA;
// 显示调用垃圾回收
System.gc();
// 显式执行垃圾回收之后, 内存相比于没有执行垃圾回收发生了明显的变化:
// 结论:Java 采用的不是引用计数
}
}
追踪式垃圾收集
可达性分析
基本内容
:::primary
① 可达性分析本身不难理解,但是在关于 GC Roots 到底是对象还是引用存在疑问
②《深入理解虚拟机》中提到 GC Roots 是一组对象,网络上大多数博客都是直接照抄的书所以也是对象
③ 宋红康老师的视频中没有点明 GC Roots 到底是什么,黑马视频中认为 GC Roots 也是对象
④ 最后找到一篇博客和 R大 的回答,他们认为 GC Roots 还是引用
⑤ 这本身倒是一个无足轻重的概念,估计面试官大多数也分不清楚吧
参考博客和回答
Java GC 内存回收机制详解(二)GC Roots 和 可达链
:::
名称:可达性分析 (Reachability Analysis)
代表语言:Java、C#、Lisp
定义:
[枚举根结点集合(GC Roots) 后开始根据引用关系向下遍历,沿着引用关系遍历过的路径称为引用链]{.blue}
[如果对象与根结点集合没有任何引用链相连,称为不可达对象;反之就是可达对象]{.blue}
[垃圾回收器会标记可达对象后 回收那些没有被标记的对象(不可达对象)]{.blue}
:::info
标记的是可达对象而不是不可达对象,但是垃圾回收的是不可达对象
《深入理解虚拟机》中提到既可以标记可达对象也可以标记不可达对象,但是不可达对象显然没有办法标记到
:::
图示:
优点:[避免循环引用造成内存泄漏问题]{.red}
缺点:[标记阶段必须确保可达性分析过程的一致性]{.green}
- 串行/并行式垃圾回收器采用 STW 机制确保用户线程不会干扰可达性分析的过程
- 并发式垃圾回收器避免对用户线程采用 STW 机制,所以需要采用增量更新/原始快照两种方式保证一致性
:::info
如果不了解什么是 STW 或者说不明白什么叫保证可达性分析的一致性,请参考我此前写的笔记 垃圾回收概述-STW
:::
根结点枚举
:::primary
① 这几个算法的实现细节是真的抽象,书上也没说清楚,网上讲清楚的人也少之又少,真的很难彻底弄明白
② 借用《深入理解虚拟机》中的话,如果觉得看不明白或者很枯燥的话,建议直接先跳过,或者理解个大概吧
参考博客:
:::
什么是根结点?
名称:GC Roots
定义:[根结点集合是一组长期活跃的 引用]{.red}
固定的根结点集合:
- [虚拟机栈中的引用类型变量(指向堆中的对象):局部变量、参数、临时变量都可以作为根结点]{.red}
- 方法区中的变量(两种变量在 JDK 7 之后都被移动到堆空间中了)
- [静态的引用类型变量]{.red}
- [引用类型的常量(字符串)]{.red}
- [本地方法栈中的引用类型变量]{.red}
- [虚拟机内部的引用(指向 Class 对象、异常对象、类加载器对象)]{.red}
- [被同步锁持有的对象的引用]{.red}
- 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调和本地代码缓存等(不知道是啥)
非固定的根结点集合:[跨代引用:老年代的对象引用年轻代的对象]{.red}
- 仅关注老年代的垃圾回收器,在回收年轻代的时候是不会关心老年代的
- 但是老年代有可能对年轻代产生引用,所以说此时老年代的引用也可以认为是 GC Roots
:::info
① 年轻代引用老年代其实也是跨代引用,但是由于这种跨代引用不会造成任何实质性的影响,所以就不在此解释
② 想要了解为什么没有影响,可以参考《深入理解虚拟机》
:::

如何找到根结点?
引入:可达性分析的前提是必须获取根结点集合(GC Roots),那么根结点集合是怎么得到的呢?
方式:
保守式垃圾回收
历史:早期的 Classic 虚拟机就是采用这种方式
内容:[遍历所有可能存放 GC Roots 的区域进行寻找]{.green}
优点:实现简单便于嵌入没有实现自动垃圾回收机制的语言中(C/C++)
缺陷:
[如果程序使用的内存空间非常大,那么遍历所有可能的区域就是非常消耗时间的]{.green}
疑似 GC Roots 的引用可能指向将要被回收的对象导致其无法被回收,导致内存泄漏
虚拟机无法对遍历过程中可能出现的疑似 GC Roots 做出判断,所以疑似 GC Roots 的引用都无法被修改
也就意味着对象无法移动,早期的 Classic 虚拟机采用句柄式访问解决了这个问题,但是效率很差
[无法采用需要移动对象的清除算法(标记整理、标记复制算法)]{.green}
:::info
问题:这个疑似指针(GC Roots)是个什么东西?
:::
半保守式垃圾回收(可以跳过)
:::warning
① 半保守和保守式在寻找 GC Roots 上没有任何区别,只能够采用遍历的方式
② 区别在于半保守在确定引用类型上会比保守式更快
:::
历史:Android 操作系统中早期的 Dalvik 虚拟机采用的方式
内容:
- 在每个对象中添加类型信息,在遍历到对象的时候就可以直接知道对象拥有的是什么引用了
- 确定 GC Roots 仍然需要去遍历各个可能的区域
缺陷:
半保守式垃圾回收也具有保守式垃圾回收的前两个缺点
半保守式垃圾回收对于直接扫描到的对象是不可以移动的,但是对象引用的对象是可以移动的
因为对象已经准确记录了引用的类型,不需要虚拟机再来判断了,只要移动对象后修改即可
[可以采用移动对象的算法(标记整理、标记复制算法)]{.green}
[准确式垃圾回收]{.red}
:::warning
① 前两者都是采用的最为常见的遍历去查找 GC Roots,显然是非常消耗时间的
② 那么能不能采用某种方式减少这种时间开销呢?显然,空间换时间是非常自然的想法
:::
历史:HotSpot、JRockit、J9等主流的虚拟机都是采用这种方式
内容:
- [采用映射表将对象的引用和对象使用的引用保存下来]{.red}
- [垃圾回收线程就根据对象的引用不断向上递归遍历,最后查找到 GC Roots]{.red}
:::info
① 映射表中的并不会直接存储 GC Roots,而是存储的对象的引用
② 每个对象都具有自己的映射表,也就是说映射表并不是全局唯一的
:::
映射表名称:HotSpot 中称为 Oopmap、JRockit 中称为 livemap、J9 称为 GC map
映射表如何得到:
- [类加载阶段就]{.red} 将每个对象的大小计算完成,能够明确知道对象的引用和对象成员变量的数据类型
- [即时编译阶段(JIT)也会生成相应的映射表]{.red}
映射表如何使用:
- [解释式使用(HotSpot采用):每次都会遍历原始的映射表并且递归查找 GC Roots]{.blue}
- [编译式使用:为每个映射表都生成执行代码,每次只需要执行相应的代码就可以找到 GC Roots]{.blue}
优点:[避免对整个内存空间的遍历,减少枚举根结点的时间]{.red}
缺点:增加垃圾回收过程占用的内存空间
问题:我们是否真的需要将所有对象的引用都保存在 Oopmap 中?
安全点与安全区域
安全点
什么是安全点?
- 定义:[虚拟机在特定的位置生成的映射表,这些特定的位置就称为安全点]{.red}
- 特点:
- [用户线程将会在安全点停止(STW),垃圾回收线程开始根据映射表进行根结点枚举,最后执行可达性分析]{.red}
- 所以垃圾回收线程无法想执行就执行其中之一的原因就是因为映射表不是到处都有,另外一个原因就是因为线程优先级太低
:::info
① 解释:没有安全点的位置也就没有映射表,没有映射表就无法进行根结点枚举,无法执行可达性分析
② 至于为什么用户线程需要暂停,参考为什么需要采用 STW
:::
为什么需要使用安全点
- 原因:
- [堆空间中有非常多的对象,相应的引用数量也非常多,为每个对象都记录引用会占用巨大的内存空间]{.red}
- [对象的引用会随着程序的执行发生变化,每次变化都去修改大量的映射表,开销太大]{.red}
如何选取安全点?
- 核心:[指令序列可以复用的位置可以被选作安全点]{.red}
- 方法调用
- 循环跳转
- 异常跳转
虚拟机要进行垃圾回收的时候,没有在安全点的线程如何到达安全点?
- 抢先式中断(Preemptive Suspension)
- 定义:[没有到达安全点的线程尽快执行到距离最近的安全点,然后暂停执行]{.green}
- 细节:没有任何虚拟机采用这种实现
- 主动式中断(Voluntary Suspension)
- 定义:
- [每个安全点都设置相应的标志位,用户线程每次执行到安全点都判断标志位是否为真]{.red}
- [如果标志位为真就自行停止,反之就继续执行,垃圾回收线程就等待所有用户线程主动停止]{.red}
- 细节:虚拟机将询问暂停的操作交给操作系统完成了,仅采用一条汇编指令
- 定义:
安全区域
什么是安全区域?
- 定义:[用户线程不会造成对象引用关系变化的代码区域称为安全区域]{.red}
为什么需要使用安全区域?
- 用户线程无论采用哪种方式都需要自己执行到最近的安全点区
- 如果线程处于无限期等待状态或者阻塞态,那么显然是永远不可能移动到安全点去的
- 那也就意味着垃圾回收线程永远无法执行了
- 只要用户线程处于安全区域内,[对象的引用不会改变]{.red},那么垃圾回收线程就可以开始工作
用户线程在安全区域中行为
- [用户线程进入安全区域后会标识自己已经进入安全区域,并且通知垃圾回收线程]{.red}
- [只要所有线程都处于安全区域或者安全点中,垃圾回收线程就可以开始执行]{.red}
- [用户线程可以在安全区域中继续执行,在即将离开安全区域的时候询问垃圾回收线程]{.red}
- 如果垃圾回收线程已经完成所有工作,那么用户线程就可以离开安全区域
- 如果垃圾回收线程还没有完成回收,那么用户线程只能够继续在安全区域中等待
垃圾回收线程开始执行的时候,线程可能处于安全区域外吗?
- 答案:[不可能]{.red}
- 解释:
- 用户线程只要不进入安全区域或者安全点,垃圾回收线程就不可能开始执行
- 因为用户线程还没有停止,垃圾回收线程与其并行显然会造成标记的不一致性
- 只有等到所有用户线程进入安全区域或者安全点的时候,垃圾回收线程才会开始执行
:::info
这是我自己之前看视频的时候想到的一个问题,不知道有没有人又类似的迷惑
:::
并发可达性分析
:::info
参考博客:
:::
什么是并发可达分析
- 定义:允许在标记阶段用户线程和 GC 线程并发执行,不再对用户线程采用 STW 机制
为什么需要采用并发可达性分析?
- 可达性分析时延
- 根结点枚举:[根结点枚举造成的时延相对固定:因为根结点的数量较少且比较固定(就那么几类)]{.red}
- 遍历对象图:[遍历的时间和堆空间大小以及对象的数量成正比:对象数量越多,遍历的时延越长]{.red}
- 核心原因:[减少在可达性分析过程中用户线程暂停的时间]{.red}
并发可达性分析存在什么问题?如何解决?
问题描述:可达性分析前后不一致
三色标记:
目的:利用三色标记推导并发情况下出现的可达性分析不一致的情况
颜色:
- 白色:表示没有被标记的对象(不可达对象)
- 黑色:表示被标记过的对象且该对象引用的其他对象也已经被标记过
- 灰色:表示被标记过的对象但是该对象引用的其他对象还没有被全部标记
过程:
[GC Roots 直接关联的对象全部灰色集合中]{.blue}
- 因为没有后续的扫描,所以无法知道直接关联的对象是否有其他引用,所以不能直接放入黑色集合
[遍历对象图:扫描直接关联对象引用的对象]{.blue}
- 如果直接关联对象没有引用其他对象,就可以直接放入黑色集合中
- 如果直接关联对象引用的其他对象都已经被遍历完成,可以直接放入黑色集合中;反之,仍为灰色
[遍历结束后 仍为白色 的对象将被直接回收]{.blue}
问题:
多标:
定义:[垃圾回收器将本应该被回收的垃圾误标成存活对象,导致无法回收]{.red}
过程:(这并不是唯一导致多标的情况)
- 用户线程在 GC 线程标记 E 对象结束之后立刻将引用断开
- GC 线程无法得知用户线程的操作依然认为 E 对象是可达的,沿着引用链遍历其余对象
- 最后 E、F、G 三个对象都将被标记为黑色,成为当次无法被回收的浮动垃圾
- 注:没有被红色框包含的的对象本身就是垃圾
后果:[不会对程序造成太大的影响(除非垃圾对象太大),只需要等待到下次垃圾回收时回收即可]{.green}
漏标:
定义:[垃圾回收器将本来存活的对象标记成垃圾,导致直接被回收]{.red}
过程:
- 用户线程在 GC 线程标记对象之前立刻将引用断开
- 在 GC 线程标记其他对象的同时,用户线程又将引用重新指向此前的对象
- GC 线程依然无法得知用户线程的操作,将会在标记结束的阶段回收这些垃圾
后果:[本应存活的对象被当成垃圾回收显然会对程序的运行造成非常严重的影响]{.green}
解决方式:
- 前提:解决方式主要针对的是漏标的情况
- 多标产生的后果非常容易解决,只需要等待下次垃圾回收发生就行
- 漏标就没有办法依靠垃圾回收器自身解决了,需要为其设计一定的方式来解决
- 发生条件(下列条件同时满足)
- [用户线程在 GC 过程中 新增黑色对象到白色对象的引用]{.red}
- [用户线程在 GC 过程中 删除灰色对象到白色对象的引用]{.red}
- 核心:破坏其中一个条件就可以避免出现漏标的情况
- 增量更新(Incremental Update)
- 定义:
- [每次用户线程增加黑色对象到白色对象间引用时,将该引用记录下来]{.red}
- [并发标记结束之后,采用 STW 机制 以黑色对象为根重新扫描 记录 的引用关系]{.red}
- 细节:增量更新破坏第一个条件从而避免对象消失
- 定义:
- 原始快照(Snapshot At The Beginning SATB)
- 定义:
- [每次用户线程删除灰色对象到白色对象的引用时,将其引用记录下来(相当于保留原始对象图)]{.red}
- [并发标记结束后,采用 STW 机制以 灰色对象 为根重新扫描记录的引用关系]{.red}
- 定义:
- 增量更新(Incremental Update)
- 前提:解决方式主要针对的是漏标的情况
回收算法
:::warning
① 在此前运行时数据区中关于堆空间的分代布局已经提到过了
② 所有的清除算法理论上是既可以用于新生代,又用于老年代的
③ 但是在分代算法提出之后,不同的清除算法就应用于不同的分代了
④ [下列介绍的所有回收算法都是基于可达性分析算法而不是引用计数法]{.red}
:::
标记-清除算法
名称:标记-清除算法(Mark Sweep)
定义:[标记阶段结束后直接清除所有不可达对象]{.red}
特点:
- [标记-清除算法主要应用于老年代的回收算法]{.red}
- [标记-清除算法需要搭配空闲列表的方式分配内存]{.red}
- 标记-清除算法是其余所有回收算法的基础
优点:[不需要改变存活对象在内存中的位置,也就不需要改变存活对象的引用]{.red}
缺点:
- 回收不可达对象的效率偏低
- [不可达对象被回收之后容易造成大量的内存碎片,需要连续内存空间的大对象无法存放]{.green}

标记-复制算法
名称:标记-复制算法(Mark Copying)
定义:
- [将内存区域划分为大小相等的两块,每次仅使用其中的一块]{.red}
- [标记阶段执行的同时就将所有存活的对象全部 复制 到另一块空的内存区域中]{.red}
- [复制的过程中会将对象在内存中 按照顺序 存放]{.red}
- [更新所有对象的引用指向的地址]{.red}
特点:
- [标记-复制算法主要针对新生代的算法]{.red}
- [标记-复制算法搭配指针碰撞使用]{.red}
- 对象在被复制完成后是按照顺序规整存放的,所以可以直接使用指针碰撞
优点:
- [核心:避免碎片问题的产生,提高内存的利用率]{.red}
- [复制算法的执行过程中 “没有” 标记阶段,减少操作开销]{.red}
- 并不是真正意义上的没有标记过程,只是在标记不可达对象的同时就将对象复制到另一块区域中了
缺点:
- [浪费内存空间,内存空间将会有一半没有被使用]{.green}
- [每次都需要执行复制对象的操作,具有一定的操作开销]{.green}
问题:
+++danger 为什么标记-复制算法主要针对年轻代而不针对老年代呢?
① 年轻代的存活对象相对较少,每次进行复制操作的开销较少
② 老年代大多数对象都是存活的,对老年代采用复制算法无疑会产生大量对象的复制开销
③ 老年代的内存空间相对于年轻代要大一倍,对老年代采用复制算法无疑会浪费更多的空间
+++
+++danger 标记-复制算法难道不会浪费年轻代的空间吗?
① 大多数对象都是“朝生夕灭”的,生命周期非常短暂,也就意味着存活的对象非常少
② 没有太大必要等比例划分新生代空间,将其中的一半用来存放少量的存活对象
③ 所以此后采用了更加优化的半区复制策略,不是简单地将新生代 1:1 划分,而是采用 8:1:1 的比例划分为伊甸园区和幸存者区
④ 只有两块幸存者区中会出现来回复制,这样即减少了内存的浪费,又避免了的内存碎片的问题
+++

标记-整理算法
- 名称:标记-整理算法(Mark Compact)
- 定义:
- 标记阶段结束后清除所有不可达对象
- [将所有存活的对象全部向内存的一端移动,将其按照顺序紧密排列]{.red}
- [更新所有被移动对象的引用指向的地址]{.red}
- 特点:
- [标记-整理算法主要针对老年代的算法]{.red}
- [标记-整理算法搭配指针碰撞分配内存]{.red}
- 优点:
- [避免内存碎片的产生,提高内存空间的利用率]{.red}
- [消除标记-复制算法带来的内存减半的高额代价]{.red}
- 缺点:
- [每次都需要移动大量的对象和更改大量的引用地址,操作开销特别大]{.green}
- [造成用户线程暂停时间(STW)是三种算法中最长的]{.green}
- [效率相对较低]{.green}
分代算法
基本内容
- 前提:与其说分代收集是一种算法不如说是一种理论
- 定义:
- [堆空间划分为不同的区域(年轻代 + 老年代)]{.red}
- [不同的区域采用不同的算法,不同的垃圾回收器收集的区域也不同]{.red}
- [年轻代采用标记-复制算法,老年代采用标记-整理或者采用标记-清除算法]{.red}
- 优点:[避免垃圾回收器整堆收集,提升垃圾回收效率]{.red}(参考堆空间-概述)
- 缺点:[存在跨代引用问题]{.green}
- 细节:现代几乎所有虚拟机/垃圾收集器都采用分代收集算法(ZGC、Shenandoah 默认不采用分代算法)
卡表与记忆集
什么是跨代引用
- 描述:
- 分代收集理论导致垃圾回收器可能仅对年轻代或者老年代进行收集
- [但是没有被收集的区域的对象可能引用了收集区域的对象 => 跨代引用]{.red}
- 带来的问题:
- 垃圾回收器如何才能够得知哪些收集区域的对象被非收集区域的对象所引用呢?
- 方式:
- [遍历非收集区域的对象并将其加入 GC Roots 后进行可达性分析]{.green}
- 优点:实现简单
- 缺点:[非收集区域空间太大或者对象数量太多的情况下会严重影响根结点枚举的时间]{.aqua}
- [记忆集:直接记录存在跨代引用的内存区域,将其直接加入 GC Roots 后进行可达性分析]{.red}
- [遍历非收集区域的对象并将其加入 GC Roots 后进行可达性分析]{.green}
- 细节:[只要采用分代收集算法或者分区算法的垃圾回收器都存在跨代引用问题]{.red}

为什么需要使用记忆集
直接原因:避免被跨代引用的对象被识别不可达对象,从而垃圾回收器回收
核心原因:[避免采用遍历的方式去搜索非收集区域中的 GC Roots,减少根结点枚举时的开销]{.red}
什么是记忆集?
名称:Remember Set
定义:[用于存储非收集区域对象引用收集区域对象的指针集合的 抽象数据结构]{.red}
具体实现:
- 字长精度:记忆集保存记录了跨代引用的机器字长
- 卡精度:[记忆集保存内存区域的地址,该内存区域中存在对象包含跨代指针]{.red}
- 对象精度:[记忆集直接保存跨代引用收集区域的对象,对象中包含跨代指针]{.green}
+++ 为什么不使用对象精度的记忆集而是使用卡精度的记忆集呢?
① 每个对象包含的数据非常多,记忆集只需要知道跨代引用在哪里就可以了,不需要关心其他的数据
② 所有就不需要以对象为单位那么高的精度,只需要记录跨代引用可能存在的位置就行
+++
优点:[避免遍历非收集区域寻找 GC Roots,节省根结点枚举的时间]{.red}
缺点:
- [需要采用写屏障的技术维护记忆集]{.green}
- [记忆集需要占用一定的空间(Garbage First 垃圾回收器的记忆集占用空间特别大(20%左右))]{.green}
:::info
① 我们可以做个类比,大概就能够明白记忆集是个什么东西了
② 此前我们为了确定哪些是 GC Roots 首先想到的方法就是遍历整个内存空间,但是为了避免遍历的开销采用了 Oopmap 优化这个过程
③ 现在我们为了确定跨代引用的对象首先想到的依然是遍历整个非收集区域,但是为了避免遍历的开销采用了 Remember Set 优化
④ 总结来说,记忆集就是为了确保被跨代引用的对象不会被错误识别成垃圾,同时优化根结点枚举过程的手段
:::
什么是卡表?
- 定义:采用卡精度实现的记忆集就是卡表
- 原理:
- [采用 HashTable / HashMap 实现]{.red}
- 每个 Card_Table(key)都对应一块 Card_Page(内存区域 value)
- 如果 Card_Page 中存在对象包含跨代引用,那么该 Card_Page 就会被标记(Dirty)
- 如果 Card_Page 中不存在对象包含跨代引用,那么该 Card_Page 就不会被标记
- 分类
- 单向卡表:[仅记录非收集区域对收集区域对象的引用]{.red}
- 双向卡表:[]{.red}
写屏障
问题:记忆集用于记录跨代引用,那么跨代引用是怎么被记录进去的呢?
定义:
- 写屏障会检查该引用是否为跨代引用
- 如果是跨代引用,就会写入记忆集;如果不是跨代引用,就不会写入记忆集
触发条件:[每次对象的引用更新时触发写屏障操作]{.red}
特点:[无论是否为跨代引用都会产生写屏障的操作]{.red}
代码:
void oop_field_store(oop* field, oop new_value)
{
*field = new_value;
// 写屏障-写后操作:① 检查是否为跨代引用 ② 如果是跨代引用就会写入记忆集
post_write_barrier(field, value);
}