返回首页面试题

面试必备:JVM 垃圾回收算法详解

2026年03月25日9 min read

面试必备:JVM 垃圾回收算法详解

如何判断对象已死?

GC 之前要先确定"哪些对象是垃圾"。

1. 引用计数法

// 每个对象有个引用计数器
// 对象被引用 +1,引用失效 -1
// 计数器为 0 → 垃圾

Object a = new Object();  // count = 1
Object b = a;              // count = 2
a = null;                  // count = 1
b = null;                  // count = 0 → 可以回收

问题:无法解决循环引用

Object A = new Object();
Object B = new Object();
A.ref = B;  // A 引用 B
B.ref = A;  // B 引用 A
A = null;
B = null;
// 此时 A 和 B 都不用了,但相互引用,计数器都不是 0
// 引用计数法无法回收 → 被淘汰

2. 可达性分析(根搜索算法)

GC Roots 开始,向下搜索,形成"引用链"。

不在引用链上的对象 → 垃圾

        GC Roots
            │
      ┌─────┴─────┐
      │           │
     [A]        [B]
      │           │
      ▼           ▼
     [C]        [D] ←─ 图中 C、D 形成引用链
      │           │
      ▼           ▼
     [E]        [F]
      │           
      ▼           
     [G]  ←─ 这个对象没有被 GC Roots 引用
            → 可以回收

GC Roots 包括

  • 虚拟机栈中引用的对象
  • 方法区中静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈 JNI 引用的对象
  • JVM 内部引用(ClassLoader、异常对象)
  • 同步锁持有的对象

3. 四种引用类型

类型说明回收时机
强引用Object o = new Object()永远不会回收
软引用SoftReference内存不足时回收
弱引用WeakReference下次 GC 时回收
虚引用PhantomReference随时可能被回收
// 软引用:缓存场景
SoftReference<byte[]> cache = new SoftReference<>(new byte[1024*1024]);

// 弱引用:ThreadLocal
WeakReference<Object> ref = new WeakReference<>(new Object());
// 下次 GC 必定回收

垃圾回收算法

1. 标记-清除算法(Mark-Sweep)

标记阶段:标记所有存活对象
清除阶段:回收所有未标记对象

before:
┌──────────────────────────────┐
│  ○  ●  ○  ●  ●  ○  ●  ○    │
│  ↑     ↑        ↑           │
│  存活   存活      存活         │
└──────────────────────────────┘
          ↓
after:
┌──────────────────────────────┐
│  ○  ○  ○  ●  ●  ●  ○  ○    │
│  空闲   空闲   使用  空闲       │
└──────────────────────────────┘

缺点

  • 效率不稳定
  • 产生内存碎片

2. 复制算法(Copying)

把内存分成两块,每次只用一块
GC 时,把存活对象复制到另一块,然后整块清除

内存 A (使用中)          内存 B (空闲)
┌────────────────┐      ┌────────────────┐
│  ●  ○  ●  ○   │  →   │  ●  ●  ●  ●   │
│  存活复制过来   │      │  原样复制       │
└────────────────┘      └────────────────┘

然后把内存 A 全部清空

优点:没有碎片,效率高

缺点:可用内存减半(实际用 8:1:1 优化)

3. 标记-整理算法(Mark-Compact)

标记阶段:和标记-清除一样
整理阶段:让存活对象向一端移动

before:          整理中:         after:
┌──────────┐     ┌──────────┐     ┌──────────┐
│●●○●●○○●●│     │●●●●●●○○○│     │●●●●●●○○○│
└──────────┘     └──────────┘     └──────────┘
                   →→→→→            存活对象密集

优点:没有碎片,不用浪费一半内存

缺点:整理需要时间,效率略低

4. 分代收集算法

根据对象存活周期分代处理:

┌──────────────────────────────────────────────┐
│                    堆内存                     │
│                                              │
│  ┌────────────────┐   ┌──────────────────┐  │
│  │   Young Gen     │   │   Old Gen        │  │
│  │   (新生代)       │   │   (老年代)        │  │
│  │                │   │                  │  │
│  │  ┌──────────┐  │   │                  │  │
│  │  │   Eden    │  │   │                  │  │
│  │  │   80%    │  │   │                  │  │
│  │  ├──────────┤  │   │                  │  │
│  │  │ S0 │ S1 │  │   │                  │  │
│  │  │10% │10% │  │   │                  │  │
│  │  └──────────┘  │   │                  │  │
│  │                │   │                  │  │
│  │  Minor GC     │   │     Major GC     │  │
│  │  频繁、较快    │   │    /Full GC      │  │
│  │  复制算法      │   │    较慢、标记整理 │  │
│  └────────────────┘   └──────────────────┘  │
│                                              │
└──────────────────────────────────────────────┘

年轻代:对象朝生夕死,用复制算法

老年代:对象存活时间长,用标记-清除/整理算法


常见垃圾收集器

年轻代收集器:
├── Serial(单线程)
├── ParNew(多线程)
└── Parallel Scavenge(吞吐量优先)

老年代收集器:
├── Serial Old
├── Parallel Old
├── CMS(并发标记清除)
└── G1(Garbage First)

组合关系:
┌────────────────────────────────────────────────────────┐
│  Serial ──── Serial Old                               │
│  Serial ──── CMS                                      │
│  ParNew ──── CMS                                      │
│  ParNew ──── Serial Old                               │
│  Parallel Scavenge ──── Parallel Old                  │
│  G1(统一)                                            │
└────────────────────────────────────────────────────────┘

1. Serial / Serial Old

  • 单线程,最简单
  • Stop The World(STW):GC 时暂停所有用户线程
  • 桌面应用,单核服务器

2. ParNew(年轻代)

  • Serial 的多线程版
  • 默认收集线程 = CPU 核数
  • JDK 7/8 推荐与 CMS 搭配

3. Parallel Scavenge(年轻代)

  • 关注吞吐量(吞吐量 = 运行代码时间 / (运行代码时间 + GC 时间))
  • 支持自适应调节(-XX:+UseAdaptiveSizePolicy)

4. CMS(老年代)

四步走

① 初始标记(STW)→ 标记 GC Roots 直接引用的对象
② 并发标记 → 从 GC Roots 向下追踪(用户线程运行)
③ 重新标记(STW)→ 修正并发标记期间变动的对象
④ 并发清除 → 清除垃圾(用户线程运行)

优点:并发收集,低停顿

缺点

  • CPU 敏感
  • 无法处理浮动垃圾(并发清理阶段产生的垃圾)
  • 产生内存碎片

5. G1(Garbage First)

JDK 9+ 默认收集器

特点

  • 把堆分成多个大小相等的 Region
  • 跟踪各 Region 垃圾堆积价值(回收能释放多少空间)
  • 优先回收价值最高的 Region

四步

① 初始标记(STW)→ 标记 GC Roots
② 并发标记 → 追踪存活对象
③ 最终标记(STW)→ 修正标记
④ 筛选回收 → 根据价值排序,回收部分 Region

面试高频问题

Q1:Minor GC 和 Full GC 的区别?

类型触发条件频率停顿时间
Minor GCEden 满
Full GC老年代满 / Metaspace 满 / System.gc()

Q2:对象进入老年代的时机?

  1. 年龄达到阈值(-XX:MaxTenuringThreshold,默认 15)
  2. 大对象直接分配
  3. Survivor 中相同年龄对象 > Survivor 的 50%

Q3:CMS 和 G1 的区别?

维度CMSG1
目标老年代全堆
算法标记-清除标记-整理
碎片有碎片无碎片
停顿可预测可预测
JDK 版本1.5+9+默认

总结

判断对象死亡

  • 引用计数(无法处理循环引用)
  • 可达性分析(从 GC Roots 追踪)

垃圾回收算法

  • 标记-清除(碎片)
  • 复制算法(年轻代,无碎片,需浪费一半空间)
  • 标记-整理(老年代,无碎片)

分代收集:年轻代复制算法,老年代标记-整理

收集器:Serial → ParNew/Parallel Scavenge → CMS → G1

评论区