简介

垃圾收集(Garbage Conleection,简称GC)

不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到 内存块的首地址。Java 中对象是采用 new 或者反射的方法创建的,这些对象的创建都是在 堆(Heap)中分配的,所有对象的回收都是由 Java 虚拟机通过垃圾回收机制完成的。GC 为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等 状况进行监控,Java 会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题。在 Java 语言中,判断一个内存空间是否符合垃圾收集标准有两个:一个是给对象赋予了空值 null,以下再没有调用过, 另一个是给对象赋予了新值,这样重新分配了内存空间。

判断一个对象是否可以被回收?

引用计数法(Reference Counting)

在对象中添加一个引用计数器,每当有一个地方引用他时,计数器就加一;当引用失效时,计数器就减一。任何情况下,计数器为零的对象就是不可能再被使用的。

引用计数法优点:实现逻辑简单

引用计数法缺点:无法解决循环引用问题;目前没有在使用

可达性分析(Reliability Analysis)

从 GC Roots 作为起点开始搜索,那么整个连通图中的对象便都是活对象,对于 GC Roots 无法到达的对象便成了垃圾回收的对象,随时可被 GC 回收。
Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象扫描堆中的对象,看是否能够沿着 GC Root 对象为起点的引用链找到该对象,找不到表示可以回收。

具体来说:

就是在根引用的过程中暂停所有的线程。当mutator暂停了它的执行时,只有将所有引用信息保存在其上下文中,才能枚举根的集合,这意味着,mutator需要能够告知哪些栈的槽位有一用,哪些寄存器持有引用。如果GC能够准确的获取上述引用信息,它就称作精准根集合枚举。

mutator的本意是改变者。他这里使用这个词想表达的就是通过程序来改变对象之间的引用关系。看上去好像很高深,其实仔细一想,我们所写的所有Java程序都能改变对象,以及它们之间的引用关系,那么这些Java程序就是mutator。

在Java中,用户线程专门负责运行mutator,而GC线程则专门负责垃圾回收,因此,mutator相对更清晰一点。

固定可作为GC Roots的对象有哪几种?

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
  • 在本地方法栈中JNI(即通常所说的native方法)引用的对象
  • Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointerException, OutOfMemoryError)等,还有系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反应Java虚拟机内部情况的JMXBean, JVMTI中注册的回调、本地代码缓存等

内存泄漏

定义

一个不再被程序使用的对象或变量还在内存中占有存储空间。

由于java的JVM引入了垃圾回收机制,垃圾回收器会自动回收不再使用的对象,了解JVM回收机制的都知道JVM是使用引用计数法和可达性分析算法来判断对象是否是不再使用的对象,本质都是判断一个对象是否还被引用。那么对于这种情况下,由于代码的实现不同就会出现很多种内存泄漏问题(让JVM误以为此对象还在引用中,无法回收,造成内存泄漏)。

举例

1、静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

1
2
3
4
Object obj = new Object();
List<Object> list = new ArrayList<>();
list.add(obj);
obj = null;

2、各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

3、变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。

1
2
3
4
5
6
7
8
public class UsingRandom {

private String msg;
public void receiveMsg(){
readFromNet();// 从网络中接受数据保存到msg中
saveDB();// 把msg保存到数据库中
}
}

如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。

实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间。

4、内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。

5、改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露

引用的分类

在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)软引用(Soft Reference)弱引用(Weak Reference)虚引用(Phantom Reference)4种,强度依次递减

强引用

强引用时最传统的“引用”的定义,是指在程序代码中普遍存在的引用赋值,即类似Object obj = new Object()这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就永远不会回收掉被引用的对象。

只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。 被强引用关联的对象不会被回收。使用 new 一个新对象的方式来创建强引用

1
Object obj = new Object();

软引用

软引用是用来描述一些还有用,但是非必需的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

可以配合引用队列来释放软引用自身。使用 SoftReference 类来创建软引用。

1
2
3
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;

弱引用

弱引用也是来描述非必需的对象,但是强度比软引用更弱一些,被弱引用关联的对象只能存活到下一次垃圾收集发生为止。

仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象;可以配合引用队列来释放弱引用自身。被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。使用 WeakReference 类来创建弱引用。

1
2
3
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

虚引用

虚引用也称为“幽灵引用”或者“幻影引用”,是最弱的引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间造成影响,也无法通过虚引用来获得一个对象实例。

必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队, 由 Reference Handler 线程调用虚引用相关方法释放直接内存。为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。使用 PhantomReference 来创建虚引用。

1
2
3
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj, null);
obj = null;

垃圾收集算法

标记-清除算法

标记清除算法是最基础的回收算法,分为标记和清除两个部分:首先标记出所有需要回收的对象,这一过程在可达性分析过程中进行。在标记完之后统一回收所有被标记的对象。

优点:速度较快

缺点:

  • 效率问题:标记和清除这两个过程的效率不高。

  • 空间问题:会造成内存碎片,导致在程序运行过程中需要分配较大对象的时候,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作。

标记-清除算法

标记-整理

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

优点:没有内存碎片

缺点:速度慢

复制算法

将可用内存容量划分为大小相等的两块,每次只用其中一块。当这块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

优点:自带整理功能,这样不会产生大量不连续的内存空间(内存碎片),适合老年代的大对象存储。

缺点:效率也不是很高,需要占用双倍内存空间

新生代和老年代

新生代和老年代是针对于分代收集算法来定义的,新生代又分为 Eden 和 Survivor 两个区。加上老年代就这三个区。数据会首先分配到 Eden 区 当中(当然也有特殊情况,如果是大对象那么会直接放入到老年代(大对象是指需要大量连续内存空间的 java 对象),当 Eden 没有足够空间的时候就会 触发 jvm 发起一次 Minor GC。如果对象经过一次 Minor GC 还存活,并且又能被 Survivor 空间接受,那么将被移动到 Survivor 空 间当中。并将其年龄设为 1,对象在 Survivor 每熬过一次 Minor GC,年龄就加 1,当年龄达到一定的程度(默认为 15)时, 就会被晋升到老年代中了,当然晋升老年代的年龄是可以设置的。如果老年代满了就执行: Full GC,因为不经常执行,因此采用了 Mark-Compact 算法清理。

新生代回收的判断条件是新生代内存不足时候。新生代主要是用来存放新生的对象,会频繁创建对象,所有垃圾收集会频繁进行回收。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

分代垃圾回收

分代垃圾回收当前商业虚拟机的垃圾收集都采用分代收集。此算法没啥新鲜的,就是将上述 三种算法整合了一下。具体如下:根据各个年代的特点采取最适当的收集算法:

  • 在新生代中,每次垃圾收集时候都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
  • 老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须用标记-清除或者标记-整理。
  • 对象首先分配在伊甸园区域。
  • 新生代空间不足时,触发minor gc,伊甸园和from存活的对象使用copy复制到to中,存活的对象年龄加 1 并且交换 from to。
  • minor gc会引发stop the word,暂停其它用户线程,等垃圾回收结束,用户线程才恢复运行。
  • 当对象寿命超过阈值时,会晋升至老年代,最大寿命15(4bit)。
  • 当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW 的时间更长。

分代垃圾回收

默认比例:

年轻代:Eden(8/10) From(1/10) To(1/10)

老年代:2/3

老年代晋升条件

  • 1、躲过15次GC,进入老年代
    可以通过JVM参数 “-XX:MaxTenuringThreshold”来设置,默认情况是15岁
  • 2、分配担保;
    CMS+Serial或ParNew:将Eden+servivor -->移动到另一个servivor中的时候,servivor空间不够用,会在老年代做分区担保
    • 在GC 新生代前,会去查看老年代是否有连续的空间,存放所有的新生代的对象,如果有空间,则直接GC,如果没有空间,则需要按照配置(JVM参数配置)是否尝试进行GC
  • 3、大对象:
    CMS(老年代收集器)配合Serial和ParNew(年轻代):可参数配置。直接进入老年代,跟随老年代的GC
    G1:当一个对象大于一个Region区50%的时候,会是大对象,堆中会分配连续的(Humongous Region)区来存放
  • 4、动态对象年龄判定
    当servivor 空间相同年龄的空间总和大于servivor空间一半的时候,会将servivor区大于等于此年龄的,全部移入到老年代

跨代引用

在分析JVM的分代垃圾收集算法的时候,可能存在老年代对新生代的引用,无法真正确定对象已死。

解决跨代引用:记忆集

记忆集(Remembered Set):一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构,在对象层面来说就是非收集区域对象对收集区域对象的引用的记录。

它存放在收集区域,比如在新生代里面存放着老年代对新生代对象的每一个引用。这样在收集新生代的时候,我们就可以根据记忆集知道哪些对象被老年代对象所引用,不能回收,这就解决了跨代引用的问题。

记忆集根据记录的精度分三类:

字长精度:记录的是老年代指向新生代地址。

对象精度:记录的是老年代引用的新生代对象。

卡精度:记录的是新生代一段地址是否存在被老年代引用的记录。

记忆集的实现:卡表

卡表(Card Table):是以第三种卡精度的方式实现的记忆集,也是目前最常用的方式。记忆集是抽象的概念,而卡表就是记忆集的一种具体实现。

卡表最简单的形式可以是一个字节数组,HotSpot就是这样实现的。

如图:

垃圾收集器

CMS回收器

是一种以获取最短回收停顿时间为目标,基于标记-清除算法,过程相对复杂,CMS 收集器 是基于“”标记–清理”算法实现的,整个过程分为四个步骤:

  • 初始标记

  • 并发标记

  • 重新标记

  • 并发清理

初始标记和重新标记需要 STW(Stop The World,系统停顿)

(1) 初始标记仅是标记 GC Roots 能直接关联的对象,速度很快。

(2) 并发标记从 GC Roots 的直接关联对象开始遍历整对象图,耗时较长但不需要停顿用户线程。

(3)重新标记则是为了修正并发标记期间因用户程序运作而导致标记产生变动的那部分记录。

(4)并发清除清理标记阶段判断的已死亡对象,不需要移动存活对象,该阶段也可与用户线程并发。

STW只发生在初始标记重新标记的过程中

cms

优点:并发收集,低停顿

理由:由于在整个过程和中最耗时的并发标记和 并发清除过程收集器程序都可以和用户线程一起工作,所以总体来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的

缺点:

  1. CMS 收集器对 CPU 资源非常敏感,在并发阶段,虽然不会导致用户线程停顿,但是会因为 占用了一部分线程使应用程序变慢,总吞吐量会降低,为了解决这种情况,虚拟机提供了一种“增量式并发收集器” 的 CMS 收集器变种, 就是在并发标记和并发清除的时候让 GC 线程和用户线程交替运行,尽量减少 GC 线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,不推荐)

  2. CMS 处理器无法处理浮动垃圾,CMS 在并发清理阶段线程还在运行, 伴随着程序的运行 自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS 无法再当次过程中处理, 所以只有等到下次 gc 时候在清理掉,这一部分垃圾就称作“浮动垃圾”。

  3. CMS 是基于“标记–清除”算法实现的,所以在收集结束的时候会有大量的空间碎片产生。 空间碎片太多的时候,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象的,只能提前触发 full gc。 为了解决这个问题,CMS 提供了一个开关参数,用于在 CMS 顶不住要进行 full gc 的时候开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片没有了,但是停顿的时间变长了

Garbage First收集器(G1)

Garbage First,优先回收最有价值的垃圾区域,达到暂停时间不短的目标。2004 论文发布; 2009JDK 6u14 体验;2012 JDK 7u4 官方支持;2017 JDK 9 默认,同时废弃了 CMS 垃圾回收。 开创了收集器面向局部收集的设计思路和基于 Region 的内存布局,主要面向服务端,最初设计目标是替换 CMS。

G1 之前的收集器,垃圾收集目标要么是整个新生代,要么是整个老年代或整个堆。而 G1 可面向堆任何部分来组成回收集进行回收,衡量标准不再是分代,而是哪块内存中存放的垃圾数量最多,回收受益最大

G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region可以根据需要,扮演新生代的Eden、Survivor空间,或者老年代空间

跟踪各 Region 里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的 Region。这种方式保证了 G1 在有限时间内获取尽可能高的收集效率。也因此可以建立起可预测的停顿时间模型。

G1 运作步骤:

(1)初始标记:标记 GC Roots 能直接关联到的对象,让下一阶段用户线程并发运行时能正确地在可用 Region 中分配新对象。需要 STW 但耗时很短,在 Minor GC 时同步完成。

(2)并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。 耗时长但可与用户线程并发,扫描完成后要重新处理 SATB 记录的在并发时有变动的对象

(3)最终标记:对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量 SATB 记录。

(4)筛选回收:对各 Region 的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成。

可由用户指定期望停顿时间是 G1 的一个强大功能,但该值不能设得太低,一般设置为 100~300 ms。

初始标记、最终标记、筛选回收时需要触发STW

g1

G1 具备如下特点:

  1. 并行于并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
  2. 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次 GC 的旧对象以获取更好的收集效果。
  3. 空间整合:与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  4. 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,

安全点

定义

A point in program where the state of execution is known by the VM

程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点(Safepoint)

OpenJDK官方定义如下:

  • 安全点是在程序执行期间的所有GC Root已知并且所有堆对象的内容一致的点。
  • 从全局的角度来看,所有线程必须在GC运行之前在安全点阻塞。 (作为一种特殊情况,运行JNI代码的线程可以继续运行,因为它们只使用句柄。但在安全点期间,它们必须阻塞而不是加载句柄的内容。)
  • 从本地的角度来看,安全点是一个显着的点,它位于执行线程可能阻止GC的代码块中。 大多数调用点都能当做安全点。
  • 在每个安全点都存在强大的不变量永远保持true不变,而在非安全点可能会被忽视。 编译的Java代码和C / C ++代码都在安全点之间进行了优化,但跨安全点时却不那么优化。 JIT编译器在每个安全点发出GC映射。 VM中的C / C ++代码使用程式化的基于宏的约定(例如,TRAPS)来标记潜在的安全点。

总的来说,安全点就是指,当线程运行到这类位置时,堆对象状态是确定一致的,JVM可以安全地进行操作,如GC,偏向锁解除等。

对于java来说,JIT知晓所有的栈帧信息和寄存器的内容,当JIT编译一个方法时,对于每条指令,它都可以去保存根引用信息,保存意味着额外的存储空间,如果要存储所有的指令就显得花销太大,另外在真实的运行过程中也只有少数指令才会成为暂停点,因此JIT只需要保存这些指令点的信息就够了。而真正有机会成为暂停点的地方就称作 safe-points,即能够安全的枚举根集合的暂停点

分类

  • GC safepoint需要知道在那个程序位置上,调用栈、寄存器等一些重要的数据区域里什么地方包含了GC管理的指针;
    如果要触发一次GC,那么JVM里的所有Java线程都必须到达GC safepoint。

  • Deoptimization safepoint需要知道在那个程序位置上,原本抽象概念上的JVM的执行状态(所有局部变量、临时变量、锁,等等)到底分配到了什么地方,是在栈帧的具体某个slot还是在某个寄存器里,之类的。

    如果要执行一次deoptimization,那么需要执行deoptimization的线程要在到达deoptimization safepoint之后才可以开始deoptimize。

    HotSpot中,安全点位置主要在:

    1. 方法返回之前
    2. 调用某个方法之后
    3. 抛出异常的位置
    4. 循环的末尾

为什么把这些位置设置为jvm的安全点呢,主要目的就是避免程序长时间无法进入safepoint,比如JVM在做GC之前要等所有的应用线程进入到安全点后VM线程才能分派GC任务 ,如果有线程一直没有进入到安全点,就会导致GC时JVM停顿时间延长。比如写了一个超大的循环导致线程一直没有进入到安全点,GC前停顿了8秒。

三色标记算法

原理

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

  • 黑色:该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对象)
  • 灰色:该对象已经被标记过了,但该对象下的属性没有全被标记完。(GC需要从此对象中去寻找垃圾)
  • 白色:该对象没有被标记过。(对象垃圾)

算法流程:

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

存在问题

浮动垃圾

并发标记的过程中,若一个已经被标记成黑色或者灰色的对象,突然变成了垃圾,此时,此对象不是白色的不会被清除,重新标记也不能从GC Root中去找到,所以成为了浮动垃圾,这种情况对系统的影响不大,留给下一次GC进行处理即可。

对象漏标问题(需要的对象被回收)

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

CMS对增加引用环节进行处理(Increment Update),G1则对删除引用环节进行处理(SATB)。

增量更新

在应对漏标问题时,CMS使用了Increment Update方法来做:

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

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

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

SATB(Snapshot At The Beginning)

在应对漏标问题时,G1使用了SATB方法来做:

  1. 在开始标记的时候生成一个快照图标记存活对象
  2. 在一个引用断开后,要将此引用推到GC的堆栈里,保证白色对象(垃圾)还能被GC线程扫描到
  3. 配合Rset,去扫描哪些Region引用到当前的白色对象,若没有引用到当前对象,则回收