一、💡 一句话理解
核心结论
GC(Garbage Collection,垃圾回收)是 JVM 的自动内存管理机制。对象变得不可达时,只是具备回收资格;通常还要等 JVM 因内存分配压力、代空间占用等条件触发相应的 GC,内存才可能真正被回收。
如果线上表现为 GC 线程活跃并伴随 CPU 升高,可继续阅读:../工作实践/线上服务 CPU 使用率过高排查。
二、🧭 理论:GC 是什么
2.1 GC 的职责
Java 对象通常创建在堆中。对象越来越多,堆空间会逐渐被占满;GC 会判断哪些对象还活着,回收无用对象占用的内存,必要时整理内存空间。
可以把堆想成仓库、对象想成货物,GC 就是清理仓库的管理员:还在使用的货物保留,已经不用的货物清走。
类比边界
真实的 GC 由 JVM 执行,并依据对象之间的引用关系判断对象是否仍然存活。
2.2 GC 解决的问题
- 开发者通常不需要手动释放对象内存。
- 无用对象占用的内存可以再次使用。
- 降低忘记释放或重复释放内存的风险。
2.3 GC 管理哪些内存
启动一个 Java 程序后,操作系统会给这个 JVM 进程分配内存。这些内存不全属于 Java 堆,可以简单分成下面几块:
| 内存区域 | 大白话理解 | GC 是否主要负责 |
|---|---|---|
| Java 堆 | 存放 new 出来的对象 | 是,GC 的主要工作区域 |
| Metaspace | 存放已经加载的类的信息 | 特定条件下可以卸载无用的类 |
| 线程栈 | 每个线程执行方法时使用的工作空间 | 线程结束后释放,不靠普通对象回收 |
| 直接内存和本地内存 | Java 堆外的缓冲区、本地库等使用的内存 | 不属于普通的堆对象回收 |
可以把 JVM 进程想成一套房子:
JVM 进程总内存
├── Java 堆:主要放对象,GC 重点清理这里
├── Metaspace:放类的信息
├── 线程栈:每个线程自己的工作空间
└── 直接内存等:堆外使用的其他空间例如,操作系统显示 Java 进程用了 2 GB,但 Java 堆只用了 800 MB,剩下的内存可能被线程栈、Metaspace 或直接内存占用。因此:
记住这一句
进程内存高,不一定是 Java 堆高;堆不高时,频繁执行 GC 也不一定能把进程内存降下来。
三、⚙️ 对象不可达后会立即回收吗
3.1 不会立即回收
“不可达”和“已经回收”是两个不同状态:
对象仍可达
↓ 引用断开
对象不可达,只具备回收资格
↓ JVM 触发并完成相应 GC
对象占用的内存被回收或整理后复用JVM 通常不会因为某一个对象刚变得不可达,就立刻单独清理它。GC 是按一批内存区域统一处理的,具体触发条件和执行方式取决于垃圾收集器。
对象如何被判定为不可达,继续看:16.GC如何判断对象可以被回收。
3.2 一次 GC 的简化过程
- 业务线程不断创建对象,消耗堆空间。
- 某个内存区域接近分配极限,或达到收集器设定的启动条件。
- JVM 执行 GC;部分阶段会暂停业务线程,也就是 Stop-The-World(STW)。
- GC 找出存活对象,回收不可达对象占用的空间。
- 存活对象可能被复制、晋升或移动,应用随后继续运行。
注意
System.gc()只是建议 JVM 尽力执行垃圾回收,规范不保证回收某个对象、释放多少内存或何时完成。业务代码通常不应依赖它保证回收时机。
四、📦 YGC、FGC 和分代回收
4.1 为什么要分年轻代和老年代
大多数对象存活时间很短。HotSpot 利用这个特点,把堆按对象年龄划分为年轻代和老年代:
| 区域 | 常见对象 | 回收特点 |
|---|---|---|
| 年轻代 | 刚创建、生命周期较短的对象 | 回收频繁,通常回收比例高 |
| 老年代 | 多次回收后仍存活或较大的对象 | 回收频率较低,处理成本通常更高 |
4.2 YGC 是什么
YGC 通常指 Young GC,也常被称为 Minor GC。年轻代没有足够空间继续分配对象时,JVM 会收集年轻代;存活对象可能进入 Survivor 区,达到一定年龄或满足其他条件后晋升到老年代。
YGC 通常会产生短暂 STW。YGC 多不一定有问题,关键要看频率、停顿时间、回收比例和对象晋升速度。
4.3 FGC 是什么
FGC 通常指 Full GC,会处理整个堆,往往还伴随压缩整理和类卸载等工作,因此停顿通常比 YGC 更长。
常见诱因包括:
- 老年代空间不足,存活对象无法及时安置。
- 对象分配或晋升失败,普通回收无法腾出足够空间。
- G1 的并发标记和空间回收赶不上对象分配速度。
- 大对象需要连续空间,但当前布局无法满足。
- Metaspace 达到压力阈值,JVM 尝试通过 GC 和类卸载释放空间;具体日志类型取决于收集器。
- 应用、框架或外部工具请求
System.gc()。
术语边界
Minor GC、Major GC、Old GC 和 Full GC 并不是所有收集器都以完全相同的方式定义。G1 还会出现 Young、Concurrent Start、Mixed 和 Full 等阶段,排查时应以实际 GC 日志为准。
4.4 以 G1 为例理解触发条件
现代 HotSpot 服务端环境通常使用 G1。它不是简单地“年轻代满了就 YGC、老年代满了就 FGC”:
- 年轻代分配压力会触发 Young GC。
- 老年代占用达到 IHOP 启动条件后,G1 会启动并发标记。
- 标记完成后,通过 Mixed GC 同时回收部分年轻代和老年代 Region。
- 如果应用分配太快,G1 来不及完成并发回收,最坏情况下才退化为 Full GC。
五、📊 CPU、内存和 GC 的关联
5.1 常见现象怎么判断
| 现象 | 可能原因 | 重点观察 |
|---|---|---|
| 堆内存呈锯齿状,上升后又明显下降 | 正常分配和回收 | GC 后内存能否回到稳定水平 |
| YGC 很频繁,但每次停顿短、回收后很低 | 对象创建速度快,或年轻代偏小 | 分配速率、YGC 总耗时、晋升量 |
| YGC 频繁且老年代持续上涨 | 存活对象较多并不断晋升 | GC 后老年代占用、对象生命周期 |
| FGC 频繁,但每次释放很少 | 活跃数据过大、内存泄漏或堆太小 | FGC 后占用、堆转储中的引用链 |
| CPU 很高,同时 GC 时间占比很高 | 回收过于频繁或并发 GC 抢占 CPU | GC 线程 CPU、分配速率、停顿频率 |
| 进程内存很高,但 Java 堆稳定 | 可能是直接内存、线程栈或本地内存 | Native Memory、线程数、DirectBuffer |
| 多次 FGC 后仍无法分配 | 有效空间不足 | OutOfMemoryError 类型和堆转储 |
5.2 CPU 为什么会被 GC 拉高
GC 要扫描引用、复制或移动对象、更新引用关系。并行 GC 会使用多个线程;G1 的并发标记也会在业务运行时占用 CPU。
因此可能出现两种影响:
- 停顿变长:业务线程被 STW 暂停,请求延迟上升。
- CPU 竞争:并发 GC 没有完全暂停业务,但会与业务线程争抢 CPU,吞吐量下降。
5.3 内存越大越好吗
不一定。堆太小会导致 GC 频繁;堆过大虽然可能减少触发次数,但每次需要处理的存活对象可能更多,也会占用更多机器内存。
真正需要平衡的是:
- 吞吐量:业务真正工作的时间占比。
- 延迟:一次 GC 停顿多久。
- 内存占用:堆和整个 JVM 进程用了多少内存。
六、🚀 实践:打开 GC 日志并判断问题
6.1 前置准备
下面以 JDK 9 及以上版本的统一日志参数为例。先确认运行环境和实际使用的垃圾收集器,不要直接照搬调优参数。
6.2 最小实践:制造分配压力
新建 GcLogDemo.java:
import java.util.ArrayList;
import java.util.List;
public class GcLogDemo {
// volatile 字段保留当前批次,避免对象在使用前被过早优化掉。
private static volatile Object currentBatch;
public static void main(String[] args) throws InterruptedException {
for (int round = 0; round < 30; round++) {
List<byte[]> batch = new ArrayList<>();
// 每轮创建约 10 MB 对象,持续给年轻代制造分配压力。
for (int i = 0; i < 40; i++) {
batch.add(new byte[256 * 1024]);
}
// 新批次会替换旧批次;旧批次随后具备被回收的条件。
currentBatch = batch;
Thread.sleep(20);
}
System.out.println("分配完成");
}
}编译并使用 64 MB 堆运行:
javac GcLogDemo.java
java -Xms64m -Xmx64m -Xlog:gc* GcLogDemo通常可以看到多条 Young GC 日志;具体次数、内存数值和停顿时间由 JDK、收集器与运行环境决定。这个例子验证的是“对象先失去引用,之后在内存分配压力触发的 GC 中被批量处理”。
6.3 在实际应用中记录 GC 日志
java \
-Xms512m \
-Xmx512m \
-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags \
-jar app.jar-Xms512m:设置初始堆大小。-Xmx512m:设置最大堆大小。-Xlog:gc*,safepoint...:把 GC 和安全点信息写入gc.log。app.jar:替换为实际应用文件。
6.4 看懂一条日志
下面是用于理解格式的简化示例,不是固定输出:
GC(12) Pause Young (Normal) (G1 Evacuation Pause) 400M->80M(512M) 8.5ms它表示:
- 这是第 12 次 GC。
- 类型是 Young GC。
- 堆占用从 400 MB 降到 80 MB。
- 当前堆容量为 512 MB。
- 本次停顿约 8.5 毫秒。
不要只看“发生了多少次 GC”,至少同时关注:
- GC 频率和累计耗时。
- 单次最大停顿时间。
- GC 前后释放了多少内存。
- GC 后老年代占用是否持续上升。
- CPU 升高的时间是否与 GC 日志重合。
6.5 推荐排查顺序
确认 JDK 与收集器
↓
查看 GC 类型、频率和停顿
↓
查看 GC 后堆与老年代占用
↓
区分分配过快、堆太小和对象无法释放
↓
再决定优化代码、调整堆或更换收集器常见误区
不要看到 CPU 高就先增大堆,也不要看到 FGC 就立刻调整 JVM 参数。先用日志确认触发原因;如果对象一直被错误引用,单纯扩大堆只会推迟问题出现。
七、🎤 面试时怎么流利地说
7.1 回答路线
GC 的作用 → 不可达不等于立即回收 → 分代与触发条件 → YGC/FGC → CPU 和内存影响。
7.2 30 秒标准回答
GC 是 JVM 的自动内存管理机制,负责识别无用对象并回收堆内存。对象通过可达性分析被判断为不可达后,只是具备回收资格,不会立即回收;一般要等年轻代空间不足、老年代达到收集器阈值或出现分配压力时,JVM 才触发相应的 GC。YGC 主要处理年轻代,通常频繁但停顿较短;FGC 会处理整个堆,成本和停顿通常更高。排查问题时要结合 GC 频率、停顿时间、回收后的内存以及 CPU 占用一起看。
7.3 1—2 分钟展开回答
Java 对象通常先分配在年轻代。年轻代空间不足时会触发 Young GC,大部分短命对象在这里被回收,存活对象可能进入 Survivor 区或晋升老年代。以 G1 为例,老年代占用达到 IHOP 条件后会先启动并发标记,再通过 Mixed GC 回收部分老年代;如果回收赶不上分配、对象晋升或大对象分配失败,才可能退化成 Full GC。GC 会使用 CPU,STW 阶段还会直接造成请求停顿,所以不能只看次数:YGC 频繁但回收效果好未必异常;FGC 频繁且回收后内存仍很高,才更像活跃数据过大、堆过小或内存泄漏。
7.4 高频追问与短答
问:对象不可达后为什么不立即回收?
因为逐个对象立即回收的成本太高。JVM 会等内存区域达到收集条件后批量判断、复制、清理或整理对象。
问:YGC 很频繁一定有问题吗?
不一定。还要看每次停顿是否很短、回收比例是否足够高,以及总 GC 时间是否影响业务。
问:FGC 后内存为什么降不下来?
说明大部分对象仍然可达,可能是正常活跃数据太大,也可能是缓存、静态集合或线程等长期持有了不该保留的对象。
八、📌 总结
- 对象不可达,只代表具备回收资格,不代表立即回收。
- YGC 主要回收年轻代;FGC 通常处理整个堆,成本更高。
- GC 一般由内存分配压力和收集器阈值驱动,具体规则取决于收集器。
- CPU 高、内存高和 GC 频繁需要结合日志判断,不能单看一个指标。
- 记忆链:对象分配 → 空间承压 → 触发 GC → 判断存活 → 回收或晋升 → 继续分配。