MinorGC/MajorGC/FullGC 对比
三种 GC 的区别
很多初学者对 MinorGC、MajorGC、FullGC 三个概念混淆不清。这一节把它们放在一起对比,厘清概念。
MinorGC:年轻代的垃圾回收
触发条件
当 Eden 区空间不足 时触发 MinorGC。
java
public class MinorGCDemo {
public static void main(String[] args) {
// 不断创建对象,直到 Eden 区满
List<byte[]> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配 1MB
i++;
// 当 Eden 区满,触发 MinorGC
}
}
}回收过程
MinorGC 采用复制算法:
- 扫描 Eden 区和 From Survivor 区,找出所有存活对象
- 存活对象复制到 To Survivor 区
- Eden 区和 From Survivor 区清空
- From Survivor 和 To Survivor 角色互换
MinorGC 前:
┌──────────────────────────────────────────────────────────┐
│ Eden (满) │ S0 (部分存活) │ S1 (空) │
│ [ObjA][ObjB]... │ [ObjC] │ │
└──────────────────────────────────────────────────────────┘
MinorGC 后:
┌──────────────────────────────────────────────────────────┐
│ Eden (已清空) │ S0 (空) │ S1 (存活对象) │
└──────────────────────────────────────────────────────────┘
↑
ObjA, ObjB, ObjC 全部复制到这里关键特点
- 停顿时间短:年轻代通常较小,扫描和复制速度快(通常几十毫秒)
- 频率高:MinorGC 非常频繁,是日常 GC 的主力
- 对象晋升:存活超过 Survivor 区容量或年龄达到阈值 → 晋升老年代
MajorGC / OldGC:老年代的垃圾回收
触发条件
当老年代空间不足时触发 MajorGC(或 OldGC)。
java
public class MajorGCDemo {
public static void main(String[] args) {
// 模拟大量对象晋升老年代
List<byte[]> bigObjects = new ArrayList<>();
for (int i = 0; i < 100; i++) {
// 分配大对象,直接进入老年代
bigObjects.add(new byte[10 * 1024 * 1024]); // 10MB × 100 = 1GB
}
}
}回收过程
MajorGC 通常采用标记-清除算法或标记-压缩算法:
- 扫描老年代,标记所有存活对象
- 清除未标记的对象(或者压缩存活对象)
- 如果空间仍然不足,触发 FullGC
MajorGC 的速度比 MinorGC 慢得多,因为老年代的容量通常是年轻代的 2~3 倍。
FullGC:全面的垃圾回收
触发条件
FullGC 是最重量级的 GC,触发条件很多:
- 老年代空间不足:MinorGC 晋升时担保失败
- System.gc():调用系统 GC(不一定生效,取决于 JVM 实现)
- 元空间不足:JDK 8+ 元空间满了
- 代码缓存不足:JIT 编译器没有足够的空间缓存编译后的代码
- 手动触发:RMI、JMX 等定时 FullGC
java
public class FullGCDemo {
public static void main(String[] args) {
// 触发 FullGC 的几种方式:
// 1. System.gc()
System.gc(); // 建议执行 FullGC,不保证
// 2. Runtime.getRuntime().gc()
Runtime.getRuntime().gc();
// 3. JMX 手动触发
// ManagementFactory.getGarbageCollectorMXBeans()
// .forEach(gc -> gc.collect());
}
}FullGC 的全面性
FullGC 会扫描整个堆(年轻代 + 老年代)和元空间,停顿时间可能从几百毫秒到几秒不等。
MinorGC:只扫描年轻代(Eden + Survivor)
MajorGC:只扫描老年代
FullGC:扫描整个堆 + 元空间三种 GC 的对比
| 维度 | MinorGC | MajorGC | FullGC |
|---|---|---|---|
| 触发条件 | Eden 区满 | 老年代满 | 老年代满/元空间满/显式调用 |
| 回收区域 | 年轻代 | 老年代 | 整个堆 + 元空间 |
| 回收算法 | 复制算法 | 标记-清除/压缩 | 视 GC 收集器而定 |
| 停顿时间 | 短(几十毫秒) | 中等(数百毫秒) | 长(数百毫秒到秒级) |
| 发生频率 | 高(每分钟多次) | 低(每小时几次) | 低(每天几次或更少) |
| 是否 Stop-The-World | 是(但时间短) | 是(CMS 等并发时不同) | 是(且时间长) |
Stop-The-World:GC 的代价
所有 GC 都会触发 Stop-The-World(STW)——暂停所有应用线程,直到 GC 完成。
java
public class STWDemo {
public static void main(String[] args) {
// STW 期间,整个程序停顿
// GC 线程工作,所有 Java 线程暂停
// 线程无法响应请求,无法分配内存
// ...
}
}不同 GC 的 STW 表现
| GC 收集器 | STW 程度 |
|---|---|
| Serial GC | 全程 STW,最长停顿 |
| Parallel GC | FullGC 时 STW,并行缩短停顿 |
| CMS GC | 初始标记 + 最终标记 STW,并发阶段不 STW |
| G1 GC | 初始标记 + 最终标记 + cleanup 部分 STW |
| ZGC / Shenandoah | 极短 STW(亚毫秒级) |
实战建议
- MinorGC 频繁是正常的:说明 Eden 区合理利用
- MinorGC 后大量对象晋升要警惕:说明 Survivor 区太小或对象生命周期异常
- FullGC 频繁是问题:需要调优或升级 GC 收集器
- 使用 G1:JDK 11+ 默认,平衡吞吐和停顿
本节小结
三种 GC 的核心区别在于扫描范围和停顿时间:
- MinorGC:年轻代,频率高,时间短
- MajorGC:老年代,频率低,时间中等
- FullGC:全堆 + 元空间,频率最低,时间最长
理解三种 GC 的区别,是理解 GC 调优的基础。下一节,我们来看 堆空间 GC 举例与日志分析。
