图文搞懂GC Java 面试通关笔记05 | 内存结构与 GC——分代假说、G1/ZGC 的取舍

101 阅读4分钟

一、为什么要关心内存和垃圾回收?

想象一下,你在家里做饭,切菜、炒菜、洗碗。如果你不及时清理垃圾,厨房会越来越乱,最后你连锅都找不到。计算机内存就像厨房,垃圾回收(GC)就是帮你清理垃圾的“保洁员”。

程序运行时会不断创建对象(比如数字、字符串、图片),这些对象占用内存。如果不清理,内存会被塞满,程序就会“卡死”甚至崩溃。

所以,GC 的目标:自动清理不再使用的对象,让程序继续顺畅运行。

image.png


二、内存结构:程序的“房间布局”

Java 程序运行时,内存主要分成几个区域(可以类比成房间):

  • 堆(Heap):存放对象的地方,GC 的主要工作区域。
  • 栈(Stack):存放方法调用和临时变量,像工作台,用完就清理。
  • 方法区(Method Area):存放类信息、常量,像说明书。

重点是 ,因为对象都在这里,垃圾也在这里。

image.png


三、第一性原理:为什么要分代?

问题:为什么不直接把整个堆当成一个大垃圾堆,定期清理?

答案:因为这样效率太低。
观察程序的运行规律,发现一个现象:

  • 大部分对象“活得很短”(比如临时变量、临时字符串),很快就没用了。
  • 少部分对象“活得很久”(比如缓存、全局配置)。

这就是著名的 分代假说(Generational Hypothesis)

大部分对象朝生暮死,少部分对象长寿。

于是,Java 把堆分成两代:

  • 年轻代(Young Generation):存放新生对象,垃圾多,清理频繁。
  • 老年代(Old Generation):存放长寿对象,垃圾少,清理少。

这样,GC 可以针对不同区域采用不同策略,提高效率。

generational_hypothesis.png

  • 核心规律:对象的“死亡率”随年龄快速下降——大多数对象很快就被回收少数能熬过多次 GC 的对象会“晋升”到老年代
  • 左侧曲线是示意(非精确数据):展示“存活率/数量随对象年龄快速衰减”。
  • 右侧框图:
  • 年轻代内含 Eden / Survivor S0 / Survivor S1Minor GC 主要在这里发生;
  • 经多次存活后的小部分对象 Promotion 到 Old Gen,老年代会经历 Major/Full GC

四、GC 的工作方式:不同“保洁员”的风格

1. 年轻代 GC(Minor GC)

  • 频繁发生,速度快。
  • 复制算法:把活着的对象搬到另一块区域,剩下的直接清空。

2. 老年代 GC(Major GC / Full GC)

  • 不常发生,但耗时长。
  • 标记-清除标记-整理:先标记活对象,再清理或压缩。

image.png

  1. Copying(半空间)
  • 左侧 From-space:包含“存活对象(绿色)”与“死亡对象(灰色)”。
  • 右侧 To-space:箭头表示把存活对象复制过去,随后 From-space 整体清空
  • 适用于年轻代的 Minor GC(复制算法快、碎片少)。
  1. Mark-Sweep(标记-清除)
  • 在一个堆框内先对可达对象做“打勾(Mark)”,
  • 再进行 Sweep,把未标记的死亡对象空间回收。
  • 简单直接,但可能遗留内存碎片
  1. 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 是为了在大内存场景下减少停顿。