知识模块
☕ Java 知识模块
四、JVM 深入理解
GC 垃圾回收算法

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 线程:             ████
暂停:                 ↑ STW

2. 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所有
CMSJDK 14-
G1JDK 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 GCEden 区满新生代
Full GC老年代满、方法区满全堆

Q2: 为什么新生代用复制算法?

新生代对象存活率低(约 90% 回收),复制算法效率高:

  • 只需复制少量存活对象
  • 没有碎片,分配高效

Q3: CMS 为什么有内存碎片?

CMS 使用标记-清除算法,不移动对象,导致碎片。

Q4: G1 如何实现可预测停顿?

G1 维护每个 Region 的回收价值(回收空间/耗时),优先回收价值高的 Region,在目标时间内回收最多垃圾。

Q5: 什么时候触发 Full GC?

  1. 老年代空间不足
  2. 方法区空间不足
  3. CMS GC 时老年代预留空间不足
  4. 显式调用 System.gc()
  5. 堆转储(jmap -histo:live)

小结

  • 可达性分析是 JVM 判断垃圾的方法
  • 新生代用复制算法,老年代用标记-整理
  • Serial/Parallel 吞吐量优先,CMS/G1/ZGC 延迟优先
  • G1 是当前主流,ZGC 是未来趋势
  • 理解 GC 日志有助于排查内存问题