GC 垃圾回收算法
垃圾收集(Garbage Collection)是 JVM 自动管理内存的机制,核心是识别垃圾并回收内存。
一、如何判断对象是垃圾?
引用计数法
原理:对象被引用时计数 +1,引用失效时 -1,计数为 0 即可回收。
优点:实现简单,回收及时。
缺点:无法解决循环引用问题。
// 循环引用示例
class Node {
Node next;
}
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null;
// 引用计数法下,a 和 b 无法被回收(互相引用)可达性分析(JVM 使用)
原理:从 GC Roots 开始向下搜索,不可达的对象即为垃圾。
GC Roots 包括:
- 虚拟机栈中的引用(局部变量)
- 方法区中静态变量引用
- 方法区中常量引用
- 本地方法栈中 JNI 引用
- 同步锁持有的对象
- JVM 内部引用
GC Roots
│
├── 对象 A (可达)
│ └── 对象 B (可达)
│ └── 对象 C (可达)
│
└── 对象 D (可达)
对象 E (不可达) ← 垃圾
└── 对象 F (不可达) ← 垃圾二、垃圾收集算法
1. 标记-清除算法 (Mark-Sweep)
流程:
标记阶段:标记所有可达对象
↓
清除阶段:清除未标记对象优点:实现简单。
缺点:
- 效率问题:标记和清除效率都不高
- 空间问题:产生大量内存碎片
回收前:
[对象A][ ][对象B][ ][对象C][ ]
回收后:
[对象A][____][对象B][____][对象C][________]
问题:空闲内存不连续,大对象无法分配2. 复制算法 (Copying)
流程:
将内存分为两块
↓
一块用完,将存活对象复制到另一块
↓
清空原来那块优点:
- 没有碎片
- 分配高效(指针碰撞)
缺点:
- 内存利用率低(50%)
- 复制成本与存活对象成正比
回收前:
[Eden 区:对象A 对象B 对象C 空空空]
[S0: 空]
[S1: 空]
复制后:
[Eden 区:空空空空空空空空空空]
[S0: 空]
[S1: 对象A 对象B 对象C]
新生代优化:Eden:S0:S1 = 8:1:1
内存利用率:90%适用场景:新生代(存活率低,复制效率高)
3. 标记-整理算法 (Mark-Compact)
流程:
标记阶段:标记可达对象
↓
整理阶段:将存活对象移动到一端
↓
清理边界外内存优点:
- 没有碎片
- 内存利用率高
缺点:
- 移动对象成本高
- 需要更新引用
回收前:
[对象A][ ][对象B][ ][对象C][ ]
整理后:
[对象A][对象B][对象C][___________________]
优点:连续空间,便于分配大对象适用场景:老年代(存活率高)
4. 分代收集算法
原理:根据对象存活周期将内存分代,不同代采用不同算法。
┌─────────────────────────────────────────────┐
│ JVM 堆内存 │
├─────────────────────────┬───────────────────┤
│ 新生代 (Young) │ 老年代 (Old) │
├──────────┬──────────────┤ │
│ Eden │ Survivor │ │
│ │ S0 S1 │ │
│ 复制算法 │ 复制算法 │ 标记-整理/清除 │
└──────────┴──────────────┴───────────────────┘| 区域 | 存活率 | 算法 | 原因 |
|---|---|---|---|
| 新生代 | 低 | 复制算法 | 存活少,复制成本低 |
| 老年代 | 高 | 标记-整理 | 存活多,避免复制开销 |
三、垃圾收集器
收集器分类
| 收集器 | 类型 | 算法 | 适用场景 |
|---|---|---|---|
| Serial | 串行 | 复制/标记-整理 | 客户端模式 |
| Serial Old | 串行 | 标记-整理 | 客户端模式 |
| ParNew | 并行 | 复制 | 服务端新生代 |
| Parallel Scavenge | 并行 | 复制 | 吞吐量优先 |
| Parallel Old | 并行 | 标记-整理 | 吞吐量优先 |
| CMS | 并发 | 标记-清除 | 低延迟 |
| G1 | 并发 | 标记-整理 + 复制 | 大堆、低延迟 |
| ZGC | 并发 | 读屏障 | 超低延迟 |
1. Serial 收集器
特点:单线程,GC 时暂停所有工作线程。
适用场景:客户端模式、小内存应用。
# 启用参数
-XX:+UseSerialGC用户线程: ████████████████████
GC 线程: ████
暂停: ↑ STW2. ParNew 收集器
特点:Serial 的多线程版本。
适用场景:服务端新生代,配合 CMS 使用。
-XX:+UseParNewGC
-XX:ParallelGCThreads=4 # GC 线程数3. Parallel Scavenge 收集器
特点:吞吐量优先,可控制 GC 时间比例。
参数:
-XX:+UseParallelGC # 新生代
-XX:+UseParallelOldGC # 老年代
-XX:MaxGCPauseMillis=200 # 最大暂停时间(毫秒)
-XX:GCTimeRatio=99 # 吞吐量目标(1/(1+99)=1%)
-XX:+UseAdaptiveSizePolicy # 自适应调节4. CMS 收集器 (Concurrent Mark Sweep)
目标:最短回收停顿时间。
流程:
初始标记 (STW) → 并发标记 → 重新标记 (STW) → 并发清除
快 慢 快 慢优点:并发收集,低延迟。
缺点:
- CPU 敏感(占用线程)
- 浮动垃圾(并发期间新垃圾)
- 内存碎片
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75 # 老年代占用 75% 触发
-XX:+UseCMSCompactAtFullCollection # Full GC 时压缩
-XX:CMSFullGCsBeforeCompaction=0 # 每次 Full GC 都压缩JDK 14 已移除 CMS,推荐使用 G1。
5. G1 收集器 (Garbage First)
特点:
- 面向服务端,替换 CMS
- Region 化内存布局
- 可预测停顿时间
- 无内存碎片
内存布局:
┌─────────────────────────────────────────────┐
│ G1 堆 │
├──────┬──────┬──────┬──────┬──────┬──────────┤
│Region│Region│Region│Region│Region│ ... │
│ Eden │ Eden │Surviv│ Old │ Humo │ │
│ E │ E │ S │ O │ H │ │
└──────┴──────┴──────┴──────┴──────┴──────────┘
每个 Region: 1-32MB(2的幂)
Humongous: 大对象专用 Region回收模式:
- Young GC:Eden 区满触发
- Mixed GC:老年代占用达阈值触发
- Full GC:内存不足时退化
参数:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标暂停时间
-XX:G1HeapRegionSize=4m # Region 大小
-XX:InitiatingHeapOccupancyPercent=45 # 触发阈值6. ZGC 收集器
特点:
- JDK 11 引入,JDK 15 正式可用
- 停顿时间 < 10ms
- 支持 TB 级堆
- 读屏障实现并发标记
参数:
-XX:+UseZGC
-XX:ZCollectionInterval=5 # GC 间隔(秒)四、垃圾收集器对比
| 收集器 | 停顿时间 | 吞吐量 | 堆大小 | JDK 版本 |
|---|---|---|---|---|
| Serial | 长 | 低 | 小 | 所有 |
| Parallel | 中 | 高 | 中 | 所有 |
| CMS | 短 | 中 | 中 | JDK 14- |
| G1 | 短 | 高 | 大 | JDK 7+ |
| ZGC | 极短 | 高 | 超大 | JDK 15+ |
五、GC 日志分析
启用 GC 日志
# JDK8
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc.log
# JDK9+
-Xlog:gc*:gc.log:time,level日志示例
[GC (Allocation Failure) [PSYoungGen: 65536K->8192K(76288K)] 65536K->8256K(251392K), 0.0123456 secs]解读:
- GC (Allocation Failure):GC 原因是分配失败
- PSYoungGen:Parallel Scavenge 新生代
- 65536K->8192K:新生代从 64MB 回收到 8MB
- 65536K->8256K:堆从 64MB 回收到 8MB
- 0.0123456 secs:耗时
GC 日志分析工具
- GCViewer
- GCEasy.io
- GCPlot
六、内存分配策略
对象优先在 Eden 分配
// 大多数对象在 Eden 区分配
Object obj = new Object(); // Eden 区大对象直接进老年代
// 大于 PretenureSizeThreshold 的对象直接进老年代
byte[] bigData = new byte[4 * 1024 * 1024]; // 可能直接进老年代-XX:PretenureSizeThreshold=3145728 # 3MB长期存活对象进老年代
对象在 Survivor 区每熬过一次 GC,年龄 +1
年龄 >= MaxTenuringThreshold,晋升老年代-XX:MaxTenuringThreshold=15 # 默认 15动态年龄判定
如果 Survivor 区同龄对象总和 > Survivor 区空间一半
年龄 >= 该年龄的对象直接进老年代空间分配担保
Minor GC 前,检查老年代最大可用连续空间
是否 > 新生代所有对象大小?
是 → 安全
否 → 是否允许担保失败?
是 → 尝试 Minor GC
否 → Full GC七、常见面试题
Q1: Minor GC 和 Full GC 区别?
| 类型 | 触发条件 | 范围 | 耗时 |
|---|---|---|---|
| Minor GC | Eden 区满 | 新生代 | 短 |
| Full GC | 老年代满、方法区满 | 全堆 | 长 |
Q2: 为什么新生代用复制算法?
新生代对象存活率低(约 90% 回收),复制算法效率高:
- 只需复制少量存活对象
- 没有碎片,分配高效
Q3: CMS 为什么有内存碎片?
CMS 使用标记-清除算法,不移动对象,导致碎片。
Q4: G1 如何实现可预测停顿?
G1 维护每个 Region 的回收价值(回收空间/耗时),优先回收价值高的 Region,在目标时间内回收最多垃圾。
Q5: 什么时候触发 Full GC?
- 老年代空间不足
- 方法区空间不足
- CMS GC 时老年代预留空间不足
- 显式调用 System.gc()
- 堆转储(jmap -histo:live)
小结
- 可达性分析是 JVM 判断垃圾的方法
- 新生代用复制算法,老年代用标记-整理
- Serial/Parallel 吞吐量优先,CMS/G1/ZGC 延迟优先
- G1 是当前主流,ZGC 是未来趋势
- 理解 GC 日志有助于排查内存问题