JVM 02 垃圾回收

发布于:2025-08-03 ⋅ 阅读:(15) ⋅ 点赞:(0)

JVM 的垃圾回收(Garbage Collection)主要针对堆中对象。

可达性分析

首先需要一个标准,什么时候可以删除对象。
引用计数法:对象中维护一个引用计数器,如果有引用则加一,引用失效则减一。如果引用为零则可以删除。该方法原理简单,但是 JVM 没有采用。原因是需要额外考虑特殊情况,比如:循环引用。
JVM 用的方法是可达性分析。选定一系列对象作为起始集合,称为GC Roots。从起始对象开始根据引用关系搜索对象。搜索路径称为引用链。不在引用链的对象为不可达对象,可以删除。
GC Roots包括:

  1. 虚拟机栈中引用的对象。
  2. 方法区中静态属性引用的对象。
  3. 方法区常量池引用的对象。
  4. 本地方法栈引用的对象。
  5. JVM 内部的引用,比如基本类型的 Class 对象,常驻异常对象,系统类加载器。
  6. 被同步锁持有的对象。
  7. 其他对象。

强软弱虚

引用表示指向另一个内存对象的起始地址。根据垃圾回收分为四种:强软弱虚。
强引用:垃圾回收不会收集强引用的对象。内存溢出报OOM。
软引用:如果内存溢出,这些对象可以被回收。
弱引用:下一次执行垃圾回收则回收引用对象。
虚引用:当做不存在引用,不影响垃圾回收。只是引用对象被回收时,发送一个系统通知到引用队列。

finalizer

不可达对象不会立即被删除,而是标记一次。随后判断对象是否有 finalizer() 方法,没有则回收。如果有,再判断是否执行过,是则回收,因为 finalizer() 方法只能执行一次。否就放在 F-Queue 队列中,由一个低优先级的 Finalizer 线程执行。一般不建议在对象中定义 finalizer() 方法,原因是它的运行代价高,不确定性大。

方法区回收

方法区主要回收:废弃常量,卸载类型。常量池中的对象也是用可达性分析判断是否回收。卸载类型更苛刻,需要满足三个条件:

  1. 该类的所有实例都被回收,即 JVM 不存在该类及子类实例。
  2. 类加载器已经被回收。
  3. Class 对象没有被引用。

对于反射,动态代理等动态创建类的场景,JVM 提供的类型卸载可以减小方法区的内存压力。

分代收集理论

分代假说:

  1. 弱分代假说:大多数对象朝生夕死。
  2. 强分代假说:经过更多次垃圾回收的对象更不容易被回收。
  3. 跨代引用假说:跨代引用相对于同代引用很少。

基于分代假说,JVM 将堆分为新生代区域和老年代区域。新生代区域的对象朝生夕死,可以频繁垃圾回收,只关注存活对象。老年代区域可以降低垃圾回收频率。
对于跨代引用,JVM 在新生代维护一个记忆集,标记老年代哪一块存在跨代引用。由于认为跨代引用很少,因此可以认为记忆集的维护开销比起遍历老年代很小。

垃圾收集算法

  1. 标记清除算法:标记要回收的对象,清除对象。它的缺点是:1. 执行效率不稳定,对象数量越多,标记清除时间越长。2. 内存空间碎片多。
  2. 标记复制算法:将内存区域划分为两块。某一块内存满了,把存活对象复制到另一块。适合对象存活率低的区域,不考虑内存碎片。缺点就是:空间浪费严重。
  3. 标记整理算法:标记存活对象,向内存一端移动存活对象,清理边界以外的内存。移动对象以及更新所有引用会消耗大量 CPU 资源。但是不移动对象就要维护空闲内存表来分配内存,甚至出现内存碎片太小,无法分配对象的情况。

HotSpot 根节点枚举

并非在需要垃圾回收时才开始枚举 GC Roots,而是维护 OopMap 来动态存储根节点对象。因此根节点枚举虽然需要暂停用户线程,但是停顿时间不长。
HotSpot 并不为每一条指令生成 OopMap,而是在特定的位置(安全点)记录 OopMap。也就是说,线程的字节码指令流并非随时可以停下开始垃圾回收,而是要在安全点才可以暂停。
垃圾回收器想中断线程,它会设置标记位为 true。各个线程不断主动轮询标记位,一旦发现标记位为 true 则主动在最近的安全点暂停。Hotspot 将轮询操作优化为一条汇编指令,提高效率。
对于sleep 或者 block 状态的线程,线程获取不到时间片,无法主动暂停。对于这类线程,JVM 提出安全区域概念。即安全区域内,引用关系不会发生变化,任意位置垃圾回收都是安全的。线程从 block 状态获取时间片,首先判断是否完成根节点枚举,如果没完成则等待。

Hotspot 记忆集

记忆集是从非收集区域指向收集区域的指针集合,避免回收收集区域被引用的对象。按照非收集区域的大小可以分为三种精度:

  1. 字长精度。每个记录精确到一个机器字长。
  2. 对象精度。每个记录精确到一个对象。
  3. 卡精度。每个记录精确到一块内存区域(512 字节)。

最常用的是第三种。卡表实现记忆集,卡表是字节数组,每一个元素对应一块内存区域,称为卡页。每页 512 字节。如果卡页中对象存在跨代引用,则卡页标记为Dirty。垃圾回收时,筛选变脏卡页,将跨代引用对象加入 GC Roots。

HotSpot 通过写屏障维护卡表。收集器更新引用之后在写屏障中更新卡表。
卡表可能存在伪共享:假设处理器缓存行大小64字节,64个卡表元素共享一个缓存行。不同线程同时更新同一个缓存行,影响彼此导致性能降低。解决方法是:事先判断,再更新。但是会增加判断开销。

Hotspot 并发执行可达性分析

枚举根节点很快,可以暂停用户线程。但是分析引用链时间与堆的大小正相关,暂停用户线程不可接受。

三色标记概念:将可达性分析中的对象分为三类,白色表示对象未被垃圾回收器访问过。灰色表示对象已经被访问过,但是存在至少一个引用没有被扫描。黑色表示所有引用均被扫描。

收集器标记对象期间,用户线程可以更改引用关系。如果收集器把应该消失的对象标记为存活,还可以接受。但是如果把存活对象标记为消失,比如用户线程更新黑色对象的新引用,那么就会程序错误。
理论证明:只有两个条件同时满足时,会出现对象消失。

  1. 插入黑色到某个白色对象的新引用。
  2. 删除所有从灰色对象到该白色对象的直接或间接引用。

破坏第一个条件的方法叫增量更新,破坏第二个条件的方法叫原始快照。增量更新记录黑色到白色的新引用。扫描完成后,再对新引用进行扫描。即将黑色对象变为灰色对象。原始快照记录灰色对象到白色对象的引用。扫描完成后,再对删除引用进行扫描。即按照开始扫描一瞬间的快照进行搜索。

HotSpot 的新生代收集器

Serial 是最古老的单线程串行收集器。使用标记复制算法。只有一个线程回收对象,回收对象时暂停所有工作线程。

ParNew 是 Serial 的多线程版本,即多个线程一起回收对象,回收对象时也暂停所有工作线程。使用标记复制算法。

Parallel Scavenge 强调高吞吐量,允许更长停顿时间。吞吐量=运行用户代码时间/(用户代码时间+GC时间)。最高效率利用服务器资源,尽快完成运算任务,适合不需要交互的后台任务。使用标记复制算法。

HotSpot 的老年代收集器

Serial Old 是 Serial 的老年代版本,使用标记整理算法。

Parallel Old 是 Parallel Scavenge 的老年代版本,使用标记整理算法。

CMS 收集器强调短的停顿时间,允许低吞吐量。适合需要交互的服务。它分为四个步骤:

  1. 初始标记。扫描 GC Roots,暂停所有工作线程。速度快。
  2. 并发标记。分析引用链,时间长。但是不停顿工作线程。
  3. 重新标记。使用增量更新方法解决对象消失问题。暂停所有工作线程。
  4. 并发清除。标记清除要删除的对象。留下内存碎片。不停顿工作线程。

CMS 的缺点是:

  1. 内存碎片,影响大对象分配。如果采用标记整理,那么会增加停顿时间。
  2. 在并发标记和并发清理阶段,虽然不停顿用户线程,但是占用部分 CPU 能力导致程序变慢,降低吞吐量。
  3. 为了不影响用户线程,需要预留部分空间供用户线程创建新对象。用户创建新对象期间也会产生垃圾,被称为浮动垃圾。

G1 收集器

JDK8 默认采用 Parallel Scavenge + Parallel Old。而从JDK9 开始默认使用 G1。
G1 是逻辑分代,物理不分代的收集器。将堆划分为多个 Region。每个 Region 可以扮演新生代 Eden,Survivor 以及老年代。收集器根据 Region 的角色分别处理。
每个 Region 的大小可以通过参数设置。对于大小超过 Region 一半的对象,G1 将其放在特殊 Humongous 区域。
G1 每次回收对象按照 Region 回收,而不是按照整个代回收。这也是它可以预测停顿时间的原因。具体思路是:维护优先级列表,回收价值高的 Region 优先回收。根据用户允许停顿时间,判断回收 Region 的个数。

G1 收集器四个步骤:

  1. 初始标记。标记GC Roots。停顿用户线程,时间短。
  2. 并发标记。可达性分析。与用户线程并发执行。
  3. 最终标记。停顿用户线程。用原始快照方法解决对象消失问题。
  4. 筛选回收。选择 Region 作为回收集,把存活对象复制到新 Region,清空旧 Region。停顿用户线程。

除了并发标记以外,均需要暂停用户线程。

它的目标是在延迟可控的条件下提高吞吐量。比起之前分代收集器一下子收集整代的思路,G1 少量多次地收集,维持堆内空间的平衡。

G1 的问题是:

  1. 每个 Region 维护一份卡表,卡表占据更多空间。
  2. 如果用户允许停顿时间过短,可能收集速度赶不上分配速度,垃圾堆积导致 Full GC。

回收策略

Young GC/Minor GC:
触发条件:Eden 满了。
特点:频率高,耗时短。使用复制算法。

Old GC/Major GC:
触发条件:老年代空间不足。
特点:执行频率低,耗时长。使用标记整理/清除算法。

MixedGC:G1 独有的
新生代 Region +收益较高的部分老年代 Region。

Full GC:
触发条件:老年代空间不足,方法区空间不足,空间分配担保失败。清理整个堆+方法区。