
有人以为 GC 是自动的,有人以为 GC 是无敌的。
直到有一天,内存爆了、服务挂了、老板骂了,才发现 —— 垃圾回收不是想象中的“回收站”,而是一位脾气不太好的“管家”。
JVM 中的垃圾回收(Garbage Collection,简称 GC)负责清理不再使用的对象,让内存“循环再利用”。
很多 Java 程序员初期对 GC 有个“甜美误会”:
“Java 有 GC,不用手动释放内存,太棒了!”
而现实是:
GC 的核心是分代:把对象分阶段地管理,老少有别,优待长者。
新生代回收频率高,但速度快(Minor GC)。
JVM 提供了多种 GC 实现,它们各有脾气,常见的有:
适合单核小内存机器,用得少,用错哭得早。
适合吞吐量优先的后台服务,JDK8 默认。
优点:多线程干活,清得快,吞吐量高。 缺点:STW 停顿时间不稳定,对延迟敏感的场景不太友好。
曾是“老年代明星选手”,现在退休了,JDK9 开始废弃。
优点:STW 时间短。 缺点:容易碎片化、失败后还得 Full GC。
现在的主力选手,JDK 9+ 默认使用。
把堆划分成多个小块(Region),优先回收垃圾最多的区域。 适合大内存场景,追求稳定的停顿时间。
年轻一代的“超低延迟 GC”,JDK 11+ 开始支持。
优点:停顿时间控制在 10ms 以下,适合对延迟敏感的系统。 缺点:内存占用略高,配置略复杂。
GC 设计初衷是美好的:
你写的这段代码:
public void foo() {
String s = new String("hello");
}
方法执行完后,s 引用就失效了,GC 老哥就会把这个 "hello" 给回收了(如果没被缓存)。
你要是写:
List<String> list = new ArrayList<>();
while (true) {
list.add("hello");
}
GC 可能崩溃哭泣:“你是来拉屎的吧?”

很多线上事故,看似是 GC 引发的,实则是你“喂的太多”。
常见锅甩场景:
对象不再使用却仍被引用,GC 无法清理。
static List<String> cache = new ArrayList<>();
public void doSomething() {
cache.add("user_data");
}
你以为是缓存,其实是内存炸弹💣。
for (int i = 0; i < 1000000; i++) {
String str = new String("test"); // 内存压力++
}
内存频繁抖动,GC 疯狂启动,服务不稳定。

超过 PretenureSizeThreshold 的对象会跳过新生代,直接进老年。
GC:你一个刚出生的孩子,就住进养老院,这合理吗?
根据应用需求,设置如下参数:
# 启用 G1 垃圾回收器(适合大多数服务端应用)
-XX:+UseG1GC
# 设置最大 GC 停顿时间为 200 毫秒,GC 会尽量满足这个目标
-XX:MaxGCPauseMillis=200
# 在老年代使用率达到 45% 时,触发混合 GC(Mixed GC)
-XX:InitiatingHeapOccupancyPercent=45
# 初始堆大小为 2GB(防止服务启动时频繁扩容)
-Xms2g
# 最大堆大小为 2GB(限制内存上限,避免吃光系统资源)
-Xmx2g
你总不能连房子多大都不告诉 GC,让它瞎猜。
# 开启 GC 日志,打印每次 GC 的基本信息(JDK 8 及以下)
-verbose:gc
# 更详细的 GC 日志记录(适用于 JDK 9+)
-Xlog:gc*:file=gc.log:time,uptime,level,tags
再用 GCEasy.io、GCViewer 可视化分析。
接入 Prometheus + Grafana,把:
统统接入监控,一眼识破“GC 幕后黑手”。
不懂 JVM 内存模型,只知道“Xmx越大越好”是误区。
大内存虽然延迟 GC,但 GC 一来就得很久。
它管不了你乱丢垃圾的习惯,它只能负责尽可能的善后。
如果你希望 GC 真正做到“无感、安全、高效”,记住以下几点:

GC 不可怕,可怕的是你啥也不管还怨它干得不够好。