你是否遇到过 Java 应用突然卡顿,或深夜收到系统内存溢出的告警?这些问题往往与 Java 垃圾回收机制息息相关。作为 Java 开发者,理解垃圾回收不仅能让你深入把握 JVM 底层运行原理,还能帮你解决性能瓶颈,提高系统稳定性。好的 GC 配置可能是系统性能优化的关键一步。
什么是垃圾回收?
Java 垃圾回收(Garbage Collection,简称 GC)是 JVM 自动管理内存的机制,负责识别和回收不再使用的对象所占用的内存空间。
在 Java 中,程序员不需要像 C/C++那样手动释放内存,JVM 会自动判断哪些对象是"垃圾",并在适当的时机回收它们。
JVM 主要通过可达性分析来判断对象是否存活。从一系列 GC Roots 出发,通过引用关系搜索,能被搜索到的对象标记为存活,其余的则是垃圾。
GC Roots 主要包括:
- 虚拟机栈中引用的对象(线程栈帧中的本地变量表)
- 方法区中静态属性引用的对象(静态变量)
- 方法区中常量引用的对象(如字符串常量池)
- 本地方法栈中 JNI 引用的对象
- JVM 内部引用(如类加载器、异常对象、同步锁等)
除了强引用外,Java 还提供了软引用、弱引用和虚引用:
- 软引用:内存不足时才会被回收,适合缓存场景
- 弱引用:下一次 GC 时无论内存是否充足都会回收
- 虚引用:不影响对象生命周期,用于跟踪对象被回收的状态
graph TD
GCRoots[GC Roots] --> A[对象A]
A --> B[对象B]
A --> C[对象C]
B --> D[对象D]
E[对象E] --> F[对象F]
style E fill:#f99,stroke:#333
style F fill:#f99,stroke:#333
上图中,对象 E 和 F 无法从 GC Roots 到达,因此会被标记为垃圾。
Java 中的垃圾回收算法与回收器
以下内容既包括基础算法(如标记-清除、复制等基础理论),也包括分代策略和现代垃圾回收器实现(G1、ZGC 等)。后者是前者的工程化组合与优化,通过不同算法的组合和改进来满足不同场景的需求。
1. 标记-清除算法(Mark-Sweep)
标记-清除是最基础的垃圾回收算法,分为两个阶段:
- 标记阶段:遍历所有 GC Roots 可达的对象,标记为存活对象
- 清除阶段:遍历整个堆,回收未被标记的对象
graph LR
subgraph 标记前
A1[对象A] --- B1[对象B]
C1[对象C]
D1[对象D]
end
subgraph 标记后
A2["对象A ✓"] --- B2["对象B ✓"]
C2[对象C]
D2["对象D ✓"]
end
subgraph 清除后
A3[对象A] --- B3[对象B]
空白C[" "]
D3[对象D]
end
优点:
- 实现简单,基本思路清晰
缺点:
- 效率低:标记和清除过程效率都不高
- 空间问题:清除后会产生大量不连续的内存碎片
案例分析: 当应用创建大量短生命周期的小对象时,使用标记-清除算法会导致严重的内存碎片问题。例如,Web 应用在处理 HTTP 请求时创建的临时对象:
public void processRequest(HttpRequest request) {
String data = request.getParameter("data"); // 临时字符串
JsonObject json = new JsonParser().parse(data); // 临时JSON对象
// 处理逻辑
// 方法结束后,data和json都会变成垃圾
}
若频繁创建/销毁这类小对象,碎片会越来越多,最终可能导致OutOfMemoryError(需要分配较大对象时找不到足够的连续空间,即使总空闲内存充足)。
2. 标记-整理算法(Mark-Compact)
标记-整理算法是对标记-清除算法的改进,解决了内存碎片的问题。
- 标记阶段:与标记-清除算法相同
- 整理阶段:将所有存活对象向内存一端移动,然后清除内存端边界以外的所有空间
graph LR
subgraph 标记前
A1[对象A] --- B1[对象B]
C1[对象C]
D1[对象D]
end
subgraph 标记后
A2["对象A ✓"] --- B2["对象B ✓"]
C2[对象C]
D2["对象D ✓"]
end
subgraph 整理后
A3[对象A] --- B3[对象B] --- D3[对象D] --- 空白[" "]
end
优点:
- 解决了内存碎片问题
- 可以分配大对象
缺点:
- 移动对象需要暂停应用线程(Stop-The-World,STW):必须在没有应用线程运行的情况下更新所有引用,确保引用一致性
- 整理过程比清除更复杂,STW 时间更长
STW 会在安全点(Safe Point)发生,这些是程序执行中的特定位置,如方法调用、循环回跳和异常处理点,此时线程状态一致且可以安全暂停。想象成高速公路的收费站,车辆只能在特定点停下检查。
实际应用: 在老年代垃圾回收中经常使用,因为老年代对象存活率高,复制算法会有较大开销。
3. 复制算法(Copying)
复制算法将内存分为两个相等的区域:From 空间和 To 空间。每次只使用其中一个区域,回收时将存活对象复制到另一个区域,然后清空当前区域。
优点:
- 解决了内存碎片问题
- 分配效率高(只在一侧分配)
- 回收效率高
缺点:
- 内存利用率低,相当于只使用了一半的内存
- 对象存活率高时,复制开销大
这就像搬家时,租两套完全相同的房子,却只住其中一套。当需要大扫除时,把有用的东西搬到空房子,然后把原来的房子彻底清空。虽然浪费空间,但整理起来特别高效。
实际运用:
Java 的新生代通常采用复制算法。以 HotSpot 虚拟机为例,新生代被分为 Eden 和两个 Survivor 区域,默认比例为 8:1:1,即 Eden 占 80%,每个 Survivor 占 10%,通过-XX:SurvivorRatio=8配置。
// 示例:新生代对象分配与年龄增长
byte[] buffer = new byte[1024]; // 通常在Eden区分配
// 小对象(<Eden剩余空间)在Eden分配,大对象(如超过-XX:PretenureSizeThreshold,默认3MB)直接进入老年代
// 经过一次Minor GC后,如果buffer仍存活
// 会被复制到一个Survivor区,并增加年龄计数
// 可通过以下参数查看对象年龄分布
// -XX:+PrintTenuringDistribution
/*
Desired survivor size 8388608 bytes, new threshold 15 (max 15)
- age 1: 5172024 bytes, 5172024 total
- age 2: 871376 bytes, 6043400 total
- age 3: 212000 bytes, 6255400 total
*/
当对象在 Survivor 区经过一定次数的 GC 后(默认 15 次,由-XX:MaxTenuringThreshold控制),会被提升到老年代。此外,动态年龄判定也会影响晋升:当 Survivor 空间中相同年龄对象大小总和超过 Survivor 空间的一半,年龄大于或等于该年龄的对象将直接进入老年代。
4. 分代收集算法(Generational Collection)
分代收集算法基于"弱分代假说",即大多数对象生命周期很短,少数对象存活时间长。根据对象的生命周期,将堆分为新生代和老年代:
- 新生代:存放新创建的对象,大多数对象在这里创建和回收
- 老年代:存放经过多次 GC 仍然存活的对象
这就像一个学校系统:幼儿园(新生代)孩子来来去去变化快,而大学(老年代)学生相对稳定,对两者采用不同的管理方式更有效率。
graph TD
subgraph Java堆
subgraph 新生代
Eden区 --- S0[Survivor 0]
Eden区 --- S1[Survivor 1]
end
老年代
end
Eden区 -.Minor GC后存活.-> S0
S0 -.下次Minor GC后存活.-> S1["Survivor 1 (年龄+1)"]
S1 -."年龄≥阈值".-> 老年代
不同代采用不同的回收算法:
- 新生代:复制算法(因为大多数对象都"死"了,复制少量存活对象更高效)
- 老年代:标记-整理或标记-清除算法(存活对象多,复制成本高)
分代策略奠定了现代 GC 的基础,但随着硬件发展(大内存、多核 CPU),出现了更高效的垃圾回收器实现,如 G1(区域化分代)、ZGC(并发低延迟)等,它们在分代思想基础上进行了进一步优化。
案例: 当我们开发一个缓存系统时,缓存对象通常会长期存活,这些对象会逐渐进入老年代:
public class CacheManager {
private static final Map<String, Object> CACHE = new ConcurrentHashMap<>();
public static void put(String key, Object value) {
CACHE.put(key, value); // 这些对象可能会长期存活,最终进入老年代
}
public static Object get(String key) {
return CACHE.get(key);
}
}
5. CMS 收集器(Concurrent Mark Sweep)
CMS 是一种以获取最短回收停顿时间为目标的老年代收集器,基于标记-清除算法实现。它的工作流程:
- 初始标记:标记 GC Roots 直接关联对象(STW,很短)
- 并发标记:并发追踪引用关系
- 重新标记:修正并发标记期间变动的部分(STW,较短)
- 并发清除:清理死亡对象,与应用线程并发
sequenceDiagram
participant A as 应用线程
participant G as CMS线程
G->>G: 初始标记(STW)
A->>A: 运行
G->>G: 并发标记
A->>A: 运行
G->>G: 重新标记(STW)
A->>A: 运行
G->>G: 并发清除
A->>A: 运行
优点:
- 并发收集,低延迟(STW 通常在 10-200ms,远低于 Parallel 的 1 秒以上)
- 适合交互式应用
缺点:
- 内存碎片问题(基于标记-清除,没有整理)
- 对 CPU 资源敏感,与应用争抢 CPU
- "并发模式失败"风险:并发清理赶不上应用分配速度时触发 Full GC
适用场景: 对响应速度有要求的中小型应用,JDK9 前使用较多。
6. G1 垃圾回收器(Garbage-First)
G1 是一种面向服务端应用的垃圾回收器,目标是替代 CMS。G1 将堆划分为多个大小相等的区域(Region),不要求物理上连续。
graph TD
subgraph G1堆内存
E1[Eden] --- E2[Eden]
E2 --- E3[Eden]
S1[Survivor] --- S2[Survivor]
O1[Old] --- O2[Old]
O2 --- O3[Old]
O3 --- O4[Old]
H[Humongous] --- H2[Humongous]
end
G1 回收过程:
- 初始标记:标记 GC Roots 直接关联的对象(STW)
- 并发标记:遍历整个堆的对象图
- 最终标记:处理并发标记阶段的变动(STW)
- 筛选回收:对各个 Region 回收价值和成本进行排序,优先回收垃圾最多的 Region(价值=Region 内垃圾占比,即 Garbage-First 的由来)。这个阶段需要 STW,会将存活对象复制到空 Region 中。
G1 像城市规划者,把内存分成很多小区域,先重点清理"垃圾率"最高的区域,而不是全城大扫除。这样既能保证效率,又能控制停顿时间。
优点:
- 可预测的停顿时间(可通过
-XX:MaxGCPauseMillis设置期望停顿时间,G1 会尽量满足,但非绝对保证) - 区域化设计,避免全堆扫描
- 适合大内存应用
使用场景: 在需要较低 GC 延迟的大内存服务器应用中,G1 是不错的选择,JDK 9 后的默认收集器。常用于 Spring Boot 微服务应用。
// 启用G1垃圾回收器
// java -XX:+UseG1GC -jar myapp.jar
// 设置期望的最大GC停顿时间(目标,非硬性限制)
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -jar myapp.jar
// 设置Region大小(默认根据堆大小自动计算,公式为 2048 * 2^n,范围1MB~32MB)
// java -XX:+UseG1GC -XX:G1HeapRegionSize=4m -jar myapp.jar
// 注意:设置过大会导致大对象分配效率降低,设置过小会增加内存管理开销
G1 的关键内部机制包括:
- 记忆集(Remembered Set):每个 Region 都有一个 RemSet,记录其他 Region 中的对象引用本 Region 中对象的情况,避免全堆扫描
- Humongous 区域:专门用于存储大对象(大于等于 Region 大小 50%的对象),若频繁创建大对象,可能导致性能下降
下面是一个简单的例子,说明 Region 大小对大对象处理的影响:
// 假设G1HeapRegionSize=1MB
byte[] largeObj = new byte[600 * 1024]; // 约600KB,正常对象
byte[] hugeObj = new byte[1500 * 1024]; // 约1.5MB,被视为Humongous对象
// 如果设置G1HeapRegionSize=4MB
// 则1.5MB对象不再被视为Humongous对象,可能提高性能
7. ZGC(Z Garbage Collector)
ZGC 是 JDK 11 引入的低延迟垃圾回收器,目标是在任何堆大小下都能将 STW 时间控制在 10ms 以内。
ZGC 的核心特性:
- 并发处理:标记、整理、复制等阶段几乎都是并发的
- 基于区域:堆内存分为大小动态变化的区域
- 内存压缩:并发整理内存,保证内存连续
- 染色指针(Colored Pointers):利用 64 位系统指针的低 4 位存储对象状态(Marked、Remapped 等),实现并发移动对象
染色指针的工作原理可以类比交通灯:
- 当对象需要移动时,JVM 设置指针中的特定位(如红灯)
- 应用线程在访问对象时检查这些标志位
- 如果发现对象正在移动,会执行特殊逻辑找到新位置
- 这样对象就能在不停止应用线程的情况下移动
graph LR
subgraph ZGC工作流程
A["并发标记:通过染色指针标记存活对象"] --> B["并发整理:移动存活对象到新位置"]
B --> C["并发重映射:更新所有指向移动后对象的引用"]
end
优点:
- 极低的延迟(<10ms),不会随堆增大而增加
- 支持 TB 级别堆
- 对吞吐量影响小(通常比 Parallel GC 低 5-15%)
适用场景: 对延迟极其敏感的大内存应用,如交易系统、实时分析平台、在线游戏服务器。
// 启用ZGC
// java -XX:+UseZGC -jar myapp.jar
// 设置ZGC区域大小(默认为8MB,当堆超过4TB时自动增大)
// java -XX:+UseZGC -XX:ZGCHeapRegionSize=4m -jar myapp.jar
8. Shenandoah 收集器
Shenandoah 是一款与 ZGC 目标类似的低延迟垃圾回收器,由 Red Hat 开发。它通过并发的标记、复制、整理来减少 STW 时间。
Shenandoah 的独特之处在于它的并发复制算法,使用转发指针(Brooks Pointers)来实现对象移动与应用并发执行。每个对象头中都有一个额外的引用字段,指向对象移动后的新位置。当对象被移动时,旧对象的转发指针会指向新位置,允许应用线程在对象移动过程中继续访问,无需 STW。
想象一下搬家的场景:
- 普通搬家:所有人必须停止活动,等家具搬完才能继续(STW)
- Shenandoah 方式:在每件家具上贴便利贴指向新位置,大家可以边搬家边使用
graph TD
subgraph Shenandoah工作流程
A[初始标记] --> B[并发标记]
B --> C[最终标记]
C --> D[并发清理]
D --> E["并发复制:通过Brooks指针实现并发移动"]
E --> F[最终引用更新]
F --> G[并发清理]
end
优点:
- 低延迟,STW 通常<10ms
- 适用于不同大小的堆
- 对吞吐量影响较小(约 85-95%的 Parallel GC 吞吐量,视工作负载而定)
适用场景: 同样适用于对延迟敏感的应用,如响应式微服务、交互式应用。
// 启用Shenandoah
// java -XX:+UseShenandoahGC -jar myapp.jar
// 启用被动GC模式(Shenandoah 1.0+特性,降低CPU使用,只在内存压力大时GC)
// java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=passive -jar myapp.jar
G1、ZGC 和 Shenandoah 的关键实现对比
这三个现代回收器采用了不同的并发策略和内存管理方案,它们都着力解决跨区域/跨代引用问题,但实现方式不同:
| 特性 | G1 | ZGC | Shenandoah |
|---|---|---|---|
| 并发移动对象 | 否(Evacuation 阶段 STW 复制对象) | 是 | 是 |
| 指针技术 | 普通指针+RemSet | 染色指针 | Brooks 指针 |
| 内存开销 | 中等 | 较低 | 较高 |
| 区域管理 | 固定大小 Region | 动态区域 | 固定区域 |
| 适用场景 | Spring 微服务 | 金融交易系统 | 流媒体服务 |
9. Serial 与 Parallel 回收器
这两个是相对传统的垃圾回收器,分别面向单线程和多线程环境:
- Serial 回收器:单线程执行 GC,简单高效,适合客户端应用和小内存场景
- 在小内存场景下(<4GB),单线程回收器的上下文切换开销更低,适合嵌入式或客户端应用
- Parallel 回收器:多线程并行执行 GC,注重吞吐量(应用运行时间/(应用运行时间+GC 时间)),适合后台批处理应用
Serial 就像一个清洁工打扫整栋楼,Parallel 则像一个清洁团队并行工作,效率更高但需要更多资源协调。
// 启用Serial GC
// java -XX:+UseSerialGC -jar myapp.jar
// 启用Parallel GC
// java -XX:+UseParallelGC -jar myapp.jar
// 设置并行GC线程数
// java -XX:+UseParallelGC -XX:ParallelGCThreads=4 -jar myapp.jar
常见 GC 问题诊断流程
对于 Java 应用中的 GC 问题,我们可以按照以下流程进行诊断:
graph TD
A[应用卡顿/内存溢出] --> B[开启GC日志]
B --> C{是否频繁Full GC?}
C -->|是| D{内存泄漏?}
C -->|否| E{Minor GC时间长?}
D -->|是| F[使用堆转储分析工具查找泄漏]
D -->|否| G[检查老年代空间]
G --> H[调整堆大小或回收器]
E -->|是| I[检查对象过早晋升]
I --> J[调整新生代大小]
全量 GC 触发条件与排查
Full GC(或 Major GC)会对整个堆进行回收,包括新生代和老年代,通常会导致较长的 STW 时间。以下是常见的触发条件:
- 老年代空间不足:这是最常见的原因
- 永久代/元空间不足:JDK 8 前的 PermGen 或之后的 Metaspace 空间不足(可通过
-XX:MaxMetaspaceSize限制大小,尤其对使用大量动态类加载的 Spring Boot 等应用重要) - 显式调用:
System.gc()或 JMX 触发(可通过-XX:+DisableExplicitGC禁用) - CMS 的并发模式失败:CMS 回收时内存分配速度过快
- 分配担保失败:Minor GC 前,老年代剩余空间不足以容纳新生代所有存活对象(动态年龄判定后的可能晋升集合),触发 Full GC
分配担保失败类似于银行资金不足的场景:
- 银行(老年代)必须确保有足够资金(空间)
- 如果客户(新生代对象)可能同时取款(晋升)的总额大于准备金
- 银行必须先筹集资金(触发 Full GC)再处理业务
如何排查频繁 Full GC 问题:
- 使用工具监控 GC:
# JDK 8及以下
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# JDK 9+统一日志格式
-Xlog:gc*=info:file=gc.log:time,uptime,level,tags
# 使用jstat实时监控
jstat -gcutil <pid> 1000
# 生成堆转储
jmap -dump:format=b,file=heap.bin <pid>
- 分析内存占用:
# 查看对象分布
jmap -histo <pid> | head -20
# 使用MAT/VisualVM分析堆转储文件
3. 常见原因与解决方案:
- 内存泄漏:对象无法被回收(如静态集合持续增长)
- 内存过小:增加堆大小(-Xmx)
- 大对象分配:避免频繁创建大对象,考虑对象池
- 过早晋升:增加新生代大小(-Xmn)或调整晋升阈值
如何选择合适的垃圾回收器?
选择垃圾回收器需要考虑以下因素:
- 应用特点:是否对延迟敏感?内存使用模式如何?
- 硬件资源:CPU 核心数、可用内存
- 性能需求:吞吐量 vs 延迟
- 吞吐量:应用程序运行时间占总时间的比例
- 延迟:单次 GC 造成的停顿时间
各回收器性能对比(大致参考值):
- Parallel GC:吞吐量 95%+,STW 100-1000ms
- CMS:吞吐量 85-90%,STW 10-200ms
- G1:吞吐量 80-90%,STW 50-200ms(可预测)
- ZGC/Shenandoah:吞吐量 85-95%(视负载而定),STW <10ms
选择建议:
- 对于需要高吞吐量的批处理系统,可以选择 Parallel GC(允许较长 STW 换取更少 GC 次数)
- 对于需要低延迟的交互式应用,可以选择 G1、ZGC 或 Shenandoah(拆分 GC 任务,减少单次 STW)
- 对于内存受限的环境,可能 Serial GC 是更好的选择(小内存场景下单线程回收器的上下文切换开销更低)
graph TD
开始 --> A{内存大小?}
A -->|小于4GB| B{对延迟要求?}
A -->|大于4GB| C{对延迟要求?}
B -->|不敏感| D[Serial GC]
B -->|敏感| E[CMS]
C -->|高吞吐量优先| F[Parallel GC]
C -->|低延迟优先| G{JDK版本?}
G -->|JDK8/9| H[G1]
G -->|JDK11+| I[ZGC/Shenandoah]
JVM 参数最佳实践
不同回收器的推荐参数组合:
G1 回收器参数
# 基本设置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m
# 内存调优
-XX:G1ReservePercent=10 # 预留空间,防止晋升失败
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用阈值
ZGC 参数
# 基本设置
-XX:+UseZGC
-XX:ZGCHeapRegionSize=8m # 区域大小
# 内存调优
-XX:+UnlockExperimentalVMOptions
-XX:ZCollectionInterval=300 # 主动GC间隔
Shenandoah 参数
# 基本设置
-XX:+UseShenandoahGC
# 工作模式
-XX:ShenandoahGCMode=passive # 被动模式(另有normal/aggressive)
前沿技术与发展趋势
随着 JDK 的演进,GC 技术也在持续发展:
-
JDK 17+的结构化并发:通过虚拟线程和结构化并发 API,减少线程栈内存占用,间接影响 GC 行为
-
GraalVM 与垃圾回收:GraalVM 的 Substrate VM 通过提前编译(AOT)减少运行时动态对象分配,降低 GC 压力
-
项目 Leyden:OpenJDK 未来计划,旨在提供更好的静态化能力,可能革新 Java 内存模型和 GC 机制
GC 调优关键原则
- 优先优化应用程序,而非盲目调整 JVM 参数
- 减少临时对象创建
- 使用对象池管理大对象
- 避免频繁的装箱/拆箱操作
- 明确性能目标
- 是优化响应时间还是吞吐量?
- 能接受的最大停顿时间是多少?
- 监控实际表现
- 定期查看 GC 日志
- 持续监控内存使用趋势
实际案例:一个 Web 应用服务层频繁创建 JSON 对象,修改代码重用对象比调整 GC 参数效果更好:
// 优化前:每次请求创建新对象
public Response process() {
ObjectMapper mapper = new ObjectMapper(); // 每次都创建新实例
return mapper.readValue(data, Response.class);
}
// 优化后:使用共享实例
private static final ObjectMapper MAPPER = new ObjectMapper(); // 单例重用
public Response process() {
return MAPPER.readValue(data, Response.class);
}
GC 日志分析
在生产环境中,通过 GC 日志可以分析垃圾回收行为,帮助调优:
# JDK 8及以下
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# JDK 9+统一日志格式
-Xlog:gc*=info:file=gc.log:time,uptime,level,tags
GC 日志示例解读:
[2022-03-15T10:15:30.123+0800] [GC (Allocation Failure) [PSYoungGen: 65536K->8192K(76800K)] 65536K->8200K(251904K), 0.0234700 secs]
这条日志表明:
- 发生在 2022-03-15 10:15:30
- 因为"分配失败"触发的 Young GC
- PSYoungGen 表示使用 Parallel Scavenge 收集器
- 新生代内存从 65536K 减少到 8192K
- 总堆内存从 65536K 减少到 8200K
- GC 耗时 23.47ms
G1 GC 日志示例:
[2022-03-15T10:20:45.678+0800] [GC pause (G1 Evacuation Pause) (young), 0.0345700 secs]
[Eden: 112.0M(112.0M)->0.0B(96.0M) Survivors: 16.0M->32.0M Heap: 246.8M(512.0M)->198.8M(512.0M)]
通过分析 GC 频率、停顿时间和内存使用模式,可以判断是否需要调整堆大小、回收器类型或参数。关键指标包括:
- GC 频率(太频繁表示内存压力大)
- 停顿时间(超过预期可能需要更换回收器)
- 内存回收效率(回收比例低表示对象大多长期存活)
- 晋升情况(过早晋升可能导致 Full GC)
总结
下表对 Java 垃圾回收算法和回收器进行了系统分类:
| 类型 | 算法/回收器 | 核心原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| 基础算法 | 标记-清除 | 标记存活对象,清除未标记对象 | 实现简单 | 碎片多、效率低 | 早期 JVM、概念验证 |
| 复制 | 存活对象复制到空白区域,清空原区域 | 无碎片、分配高效 | 内存利用率低、高存活对象低效 | 新生代 | |
| 标记-整理 | 标记后压缩对象到内存一端 | 无碎片、支持大对象 | 移动对象开销、STW | 老年代 | |
| 分代策略 | 分代收集 | 按生命周期分代,新生代复制+老年代整理 | 针对性优化 | 实现复杂 | 所有现代 JVM |
| 现代回收器 | Serial | 单线程 GC | 简单高效 | STW 长 | 客户端、小内存场景 |
| Parallel | 多线程并行 GC | 高吞吐量 | STW 仍较长 | 批处理系统、Kafka 后台 | |
| CMS | 并发标记清除 | 低延迟 | 内存碎片、CPU 敏感 | 交互式应用 | |
| G1 | 区域化+筛选回收 | 可预测停顿 | 内存占用略高 | Spring Boot 微服务 | |
| ZGC | 染色指针+并发移动 | 极低延迟(<10ms) | 吞吐量略低、JDK11+ | 金融交易、游戏服务器 | |
| Shenandoah | Brooks 指针+并发复制 | 低延迟(<10ms) | 内存开销较大 | 流媒体、实时分析 |