一、为什么要关心内存和垃圾回收?
想象一下,你在家里做饭,切菜、炒菜、洗碗。如果你不及时清理垃圾,厨房会越来越乱,最后你连锅都找不到。计算机内存就像厨房,垃圾回收(GC)就是帮你清理垃圾的“保洁员”。
程序运行时会不断创建对象(比如数字、字符串、图片),这些对象占用内存。如果不清理,内存会被塞满,程序就会“卡死”甚至崩溃。
所以,GC 的目标:自动清理不再使用的对象,让程序继续顺畅运行。
二、内存结构:程序的“房间布局”
Java 程序运行时,内存主要分成几个区域(可以类比成房间):
- 堆(Heap):存放对象的地方,GC 的主要工作区域。
- 栈(Stack):存放方法调用和临时变量,像工作台,用完就清理。
- 方法区(Method Area):存放类信息、常量,像说明书。
重点是 堆,因为对象都在这里,垃圾也在这里。
三、第一性原理:为什么要分代?
问题:为什么不直接把整个堆当成一个大垃圾堆,定期清理?
答案:因为这样效率太低。
观察程序的运行规律,发现一个现象:
- 大部分对象“活得很短”(比如临时变量、临时字符串),很快就没用了。
- 少部分对象“活得很久”(比如缓存、全局配置)。
这就是著名的 分代假说(Generational Hypothesis):
大部分对象朝生暮死,少部分对象长寿。
于是,Java 把堆分成两代:
- 年轻代(Young Generation):存放新生对象,垃圾多,清理频繁。
- 老年代(Old Generation):存放长寿对象,垃圾少,清理少。
这样,GC 可以针对不同区域采用不同策略,提高效率。
- 核心规律:对象的“死亡率”随年龄快速下降——大多数对象很快就被回收,少数能熬过多次 GC 的对象会“晋升”到老年代。
- 左侧曲线是示意(非精确数据):展示“存活率/数量随对象年龄快速衰减”。
- 右侧框图:
- 年轻代内含 Eden / Survivor S0 / Survivor S1,Minor GC 主要在这里发生;
- 经多次存活后的小部分对象 Promotion 到 Old Gen,老年代会经历 Major/Full GC。
四、GC 的工作方式:不同“保洁员”的风格
1. 年轻代 GC(Minor GC)
- 频繁发生,速度快。
- 用 复制算法:把活着的对象搬到另一块区域,剩下的直接清空。
2. 老年代 GC(Major GC / Full GC)
- 不常发生,但耗时长。
- 用 标记-清除 或 标记-整理:先标记活对象,再清理或压缩。
- Copying(半空间)
- 左侧
From-space:包含“存活对象(绿色)”与“死亡对象(灰色)”。 - 右侧
To-space:箭头表示把存活对象复制过去,随后 From-space 整体清空。 - 适用于年轻代的 Minor GC(复制算法快、碎片少)。
- Mark-Sweep(标记-清除)
- 在一个堆框内先对可达对象做“打勾(Mark)”,
- 再进行 Sweep,把未标记的死亡对象空间回收。
- 简单直接,但可能遗留内存碎片。
- Mark-Compact(标记-整理/压缩)
- 先 Mark 出存活对象;
- 再 Compact,把存活对象移动到一侧,消除碎片,得到连续空间。
- 适用于老年代,减少碎片对大对象分配的影响。
五、G1 与 ZGC:两种现代“保洁员”
随着内存越来越大、应用越来越复杂,传统 GC(比如 CMS)开始吃力,于是出现了新一代 GC:
G1(Garbage First)
- 把堆切成很多小块(Region),按垃圾多少优先清理。
- 优点:可预测停顿时间,适合大内存应用。
- 缺点:算法复杂,调优难。
ZGC(Z Garbage Collector)
- 目标:超低停顿时间(<10ms),即使内存非常大(TB 级)。
- 用 并发标记和重定位,几乎不影响应用运行。
- 缺点:对硬件要求高,调试复杂。
六、面试常问:为什么要分代?为什么选择 G1 或 ZGC?
-
为什么分代?
因为对象生命周期不同,分代可以提高 GC 效率。 -
什么时候用 G1?
大内存应用,需要可预测停顿。 -
什么时候用 ZGC?
超大内存、对延迟极度敏感的场景。
七、总结:一句话记住核心逻辑
GC 的本质是清理垃圾,分代是假设对象生命周期不同,G1/ZGC 是为了在大内存场景下减少停顿。