Skip to content

垃圾回收器

垃圾回收类型

  1. 串行

    • 单线程

    • 适合堆内存小的时候。

    • STW

      Stop The World 的简称。这是因为串行的机制,在垃圾回收的线程运行的时候,其它工作线程都要阻塞。

      在垃圾回收过程中,对象的地址会发生改变。如果其它线程不阻塞,则可能会发生对象引用错误的问题。

  2. 吞吐量优先

    • 多线程
    • 适合堆内存较大,且多核CPU的情况。
    • 在单位时间内,STW时间最短。
  3. 响应时间优先

    • 多线程

    • 适合堆内存较大,且多核CPU的情况。

    • 尽可能让单次 STW 时间最短。

      比如 1小时,垃圾回收两次,每次10分钟。(吞吐量优先)

      和每5分种垃圾回收一次,1小时内回收4次。(响应时间优先)

垃圾收集器

Serial 收集器

  • XX:+UseSerialGC -XX:+UseSerialOldGC

使用串行回收器进行回收,这个参数会使新生代和老年代都采用串行回收器。SerialOld是Serial的老年代版本。

新生代使用复制算法,老年代使用标记-整理算法

Serial收集器是最基本、历史最悠久的收集器。它是一个单线程收集器,一旦收集器工作,系统会停止。

在垃圾回收线程运行时,其它工作线程都要阻塞。

优点是简单而高效,单线程没有线程开销,有很高的单线程收集效率。

Parallel Scavenge收集器

  • XX:+UseParallelGC(年轻代) -XX:+UseParallelOldGC(老年代)

Parallel收集器其实就是 Serial收集器的多线程版本

默认的收集线程数与 CPU 核数一致。(可以修改线程数,但是为了吞吐量不建议修改)

Parallel Scavenge 收集器关注点是吞吐量(高效率的利用CPU)。

CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。

所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。

Parallel Scavenge收集器和Parallel Old收集器 是JDK8默认的新生代和老年代收集器。

ParNew 收集器

  • XX:+UseParNewGC

ParNew收集器其实跟 Parallel 收集器很类似,区别主要在于它可以和CMS收集器配合使用

CMS收集器

  • XX:+UseConcMarkSweepGC(old)

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。

使用 标记-清除 算法。

标记-清除过程

  • 初始标记

    暂停用户线程(STW),记录下GC Roots 能直接引用的对象。

  • 并发标记

    开始遍历 GC Roots 能直接引用的对象。

    比如 GC Roots → A → B,开始从 A 遍历,遍历整个对象图。

    不需要暂停用户线程,这个过程耗时较长,用户线程可以与垃圾回收线程一起运行。

    但是因为用户线程继续运行,可能导致已标记的对象状态发生变化。

  • 重新标记

    需要暂停用户线程(STW),重新标记阶段就是为了修复并发标记阶段,对象状态产生变化的对象。

    这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法 (见下面详解) 做重新标记。

  • 并发清理

    开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。

  • 并发重置

    重置本次GC过程中的标记数据。

CMS收集器的特点

优点

  • 并发收集
  • 低停顿

缺点

  • CPU 资源敏感(GC线程会抢占资源)

  • 无法处理GC过程中新产生的垃圾

    并发标记和并发清理阶段产生的新垃圾不做操作,只能等到下次 GC。

  • 标记-清除算法会导致大量内存碎片

    可以通过参数XX:+UseCMSCompactAtFullCollection 让jvm在执行完标记清除后再做整理。

  • 执行过程不确定

    可能在本次 GC还未完成的时候,由于用户线程的原因,触发新一次的 GC。


三色标记法

垃圾回收算法判断对象是否需要被回收时,一般采用可达性分析算法。

而在以减少系统停顿时间为目的的 CMS 收集器中,标记-清除过程中存在垃圾回收线程和用户线程并发的情况,可能导致对象状态发生变化。

三色标记法将对象的状态按照三种颜色划分,不仅能解决和用户线程并发的问题,还能缩短 STW 的时间。

  • 白色

    没有被 GC 扫描到的节点都是白色,意味着可以被回收。

    在初始标记开始前,所有节点的状态都是白色。

  • 灰色

    该节点表示正在标记的节点,至少存在一个引用没有被扫描到。是一种中间状态。

    最终节点状态只会停留在黑色或者白色。

  • 黑色

    被 GC 扫描过的节点标记为黑色,表示节点是安全的。

标记过程

  1. 初始标记阶段。从虚拟机栈中 GC Roots 开始扫描直接引用对象,找到 Node1 和 Node5,将 Node1 和 Node5 标记为灰色。

  2. 并发标记阶段。

    从 Node1 和 Node5 开始遍历,查到 Node2 和 Node6,此时将 Node2 和 Node6 标记为灰色。同时将 Node1 和 Node5 标记为黑色。

  3. 并发清除阶段。

    经历过标记阶段之后,只剩下Node4 和 Node7 两个节点没有被引用,可以被回收。

    将白色阶段回收之后,剩下的就都是黑色节点。

标记问题

多标 - 浮动垃圾

  • 在并发-标记过程中,如果运行的方法被销毁。该方法作为 GC Roots 引用的对象,之前如果已经被扫描过标记为黑色了。那么本轮 GC不会再回收这些对象,这些本该被回收的垃圾对象成为浮动垃圾

  • 在并发-标记(并发-清理)过程中,如果产生了新对象,通常做法直接设置为黑色。本轮也不会进行垃圾回收,放到下一轮回收。

    如果这部分对象在本次 GC 有垃圾对象产生,也是浮动垃圾。


浮动垃圾并不会影响垃圾回收的正确性,只需要等到下一次垃圾回收就可以被清除。

漏标

应该被标记为黑色回收的对象,漏标。导致存活对象被垃圾回收,严重影响程序功能。

漏标必须满足的条件

漏标只有在以下两个条件都满足的情况下才会产生:

条件 1:有至少一个黑色对象在自己被标记之后指向了这个白色对象。(黑色对象本身不会被再次扫描)

条件 2:灰色对象在自己引用扫描完成之前删除了对白色对象的引用。

只有同时满足两个条件才会发生漏标的情况,所以只要破坏其中一个条件,就能解决漏标问题。

漏标解决方案

增量更新

破坏条件 1(有至少一个黑色对象在自己被标记之后指向了这个白色对象)。

在并发标记阶段当我们黑色对象(B)引用关联白色对象(E),记录下B黑色对象。

在重复标记阶段,将 B黑色对象变为灰色对象,然后扫描 B 的整个引用链。

记录黑色对象的过程结合了写屏障来实现的。

增量更新指的是保存引用关系,在重复标记阶段再次更新。

  • 在并发-标记阶段,当 Node5 引用 Node3 时,记录 q 指针的引用关系。
  • 在重复标记阶段,将q 指针对应的 Node5 变为灰色对象,然后扫描 Node5 的整个调用链。最终将 Node3 标记为黑色,不被清除。

原始快照

破坏条件 2(灰色对象在自己引用扫描完成之前删除了对白色对象的引用)

当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来。

在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次。这样就能扫描到白色的对象,将白色对象直接标记为黑色 (目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)。

原始快照指的是保留删除前的引用关系,将要被删除的引用对象标记为黑色,下次 GC 再重新扫描。

  • 当 p 指针引用关系要被删除时,记录下 p 指针。

  • 在重复扫描过程中,以灰色对象为根,重新扫描一次。

  • 将扫描到的白色对象直接标记为黑色。

    这样做的目的是让对象在本轮 GC 中能够存活下来,等到下次 GC时再重新扫描。

写屏障

写屏障的主要作用是确保在并发编程环境中,对共享数据的写操作能够被正确地同步和管理。

特别是在垃圾回收过程中,确保对象状态的改变能够被垃圾回收器感知

当应用程序的线程对一个对象进行写操作时,写屏障会被触发,通知垃圾回收器对象的状态变化,从而避免漏标等问题。

解决方案

  • CMS 收集器
    • 写屏障 + 增量更新
  • G1 收集器
    • 写屏障 - 原始快照(SATB)
  • ZGC:
    • 读屏障