目录
正在加载目录…
专栏文章
专栏文章
GC 垃圾回收系列
1. GC 总览与历史:自动内存管理的起源与演化 2. GC 基础算法:标记-清除、标记-整理、复制收集、引用计数 3. 分代假说与分代收集:朝生夕死的工程哲学 4. CMS 收集器:Java 第一个并发收集器的设计与缺陷 5. G1 收集器:Region 化堆与可预测停顿 6. ZGC 与 Shenandoah:亚毫秒级停顿的并发移动收集器 7. Go 的 GC:并发三色标记与混合写屏障 8. Python 的内存管理:引用计数与分代循环 GC 9. GC 调优实践:JVM 日志解读与 G1/ZGC 参数策略

分代假说与分代收集:朝生夕死的工程哲学

发布于 2026-05-25 14:16 · 最后编辑于 2026-07-31 15:53 · 字数 3,688 👁 103 次阅读

分代收集是现代主流 GC 的核心设计哲学,建立在一个关键经验观察上:大多数对象"朝生夕死"。本文梳理分代假说的理论基础、分代堆的结构设计,以及 Minor GC 和 Major GC 的触发机制。

目录

章节说明
弱分代假说统计规律与理论依据
JVM 分代堆结构Eden、Survivor、Old、Metaspace
对象的晋升路径从 Eden 到 Old 的完整流程
跨代引用问题卡表与记忆集
Minor GC vs Major GC触发条件与影响

弱分代假说

核心观察

1984 年 David Ungar 在研究 Smalltalk 程序时发现:

弱分代假说(Weak Generational Hypothesis): 绝大多数对象都在年轻时死亡。

统计数据:

  • 典型 Java 程序中,98% 的对象在第一次 Minor GC 时就已经死亡
  • 只有约 2% 的对象能"熬过"第一次 GC 进入老年代
  • 长期存活的对象(如缓存、连接池)只占堆的一小部分

这个观察的深层原因:大量临时对象(方法调用时创建的局部对象、循环体内的中间结果、日志调试对象)在方法返回后就没有任何引用了。

分代假说的推论

如果大多数对象很快死亡,那么:

  1. 年轻代 GC 频繁但快速: 存活对象少,复制开销小
  2. 老年代 GC 稀少: 晋升率低,不需要频繁扫描
  3. 按代分配收集策略,整体效率远高于全堆 GC

JVM 分代堆结构

逻辑结构

gc generational heap

区域作用GC 算法
Eden新对象分配(大多数对象在此诞生)复制收集
Survivor 0/1存活过一次 Minor GC 的对象复制收集
Old Generation多次 GC 存活的长期对象标记-整理
Metaspace类元数据(JDK 8+ 替代 PermGen)独立管理

为什么有两个 Survivor 区

复制收集要求 To Space 始终为空,才能保证存活对象被复制到连续内存、没有碎片。

为什么不是两个 Eden?

两个 Eden 无法保持"始终有一块空区域":第一次 GC 后 Eden1 的存活对象进了 Eden2,第二次 GC 时 Eden2 满了,要复制回 Eden1,但 Eden1 里还有上次的存活对象——无法整体清空,碎片问题无法解决。

两个 Survivor 交替使用解决了这个问题:每次 Minor GC 后 To Space 一定是空的,下次 GC 时它仍然可以作为干净的目标区域接收存活对象。

Minor GC 过程:Eden + S0(From)中的存活对象复制到 S1(To)→ 清空 Eden 和 S0 → S0/S1 角色互换。

为什么 Survivor 不需要很大?

HotSpot 默认 Eden : S0 : S1 = 8 : 1 : 1,因为 98% 的对象在 Eden 就死亡,只有约 2% 能存活一次 GC 进入 Survivor。如果是两个 Eden 各占 50%,每次用一半来接收约 2% 的存活对象,内存利用率极低。

对象的晋升路径

完整生命周期

  1. 对象创建 → Eden 区(TLAB 快速分配)
  2. Eden 满 → 触发 Minor GC,存活对象复制到当前 To Space(Survivor 之一),age = 1
  3. 再次 Minor GC → Eden + 当前 From Space 中存活对象复制到当前 To Space,age++;每次 GC 后 S0/S1 角色互换
  4. age 达到阈值(默认 15)→ 晋升到 Old Generation
  5. Old Generation 满 → 触发 Major GC / Full GC

年龄阈值与动态调整

-XX:MaxTenuringThreshold=15   # 默认年龄阈值

动态年龄判定(HotSpot 优化):如果 Survivor 中年龄 N 的对象总大小 > Survivor 空间的 50%,则年龄 ≥ N 的对象直接晋升,不等到阈值。

大对象直接进入老年代

-XX:PretenureSizeThreshold=3145728   # 3MB 以上直接进 Old

大对象复制代价高,且通常存活时间较长(典型:大数组、大字符串),直接分配到老年代可以避免反复复制。

跨代引用问题

问题描述

分代 GC 在做 Minor GC 时只扫描年轻代,但老年代的对象可能引用年轻代对象:

如果只扫描 Young Gen,LongLivedObjYoungObj 的引用就被忽略了,YoungObj 可能被错误回收。

解决方案:卡表(Card Table)

将老年代划分为若干 512 字节的"卡",如果某张卡内的对象引用了年轻代对象,就把这张卡标记为"脏"(dirty)。卡表本身是 JVM 堆外维护的 byte 数组,通过对象地址右移 9 位(÷ 512)即可定位到对应卡号:

gc card table

Minor GC 时:只扫描年轻代 + 所有 dirty 卡,跳过干净的卡,大量节省时间。

记忆集(Remembered Set) 是卡表的抽象表示,G1 中每个 Region 维护一个 Remembered Set,记录哪些外部 Region 引用了自己。

Minor GC vs Major GC

Minor GCMajor GCFull GC
回收区域Young GenOld GenYoung + Old + Metaspace
触发条件Eden 满Old Gen 满显式 System.gc() 或空间担保失败
STW 时间短(毫秒级)长(秒级,取决于 GC 算法)最长
频率高(几秒一次)低(分钟到小时)尽量避免

空间担保(Space Guarantee)

Minor GC 前 JVM 检查:老年代剩余空间是否大于历次晋升的平均大小?

  • 老年代剩余 < 历次平均晋升量 → 担保失败,直接触发 Full GC(保守策略)
  • 若允许担保失败(-XX:+HandlePromotionFailure)→ 先做 Minor GC,若晋升量仍超过老年代剩余再做 Full GC

Full GC 触发条件(OpenJDK HotSpot,JDK 8-21)

Minor GC 只回收年轻代;Full GC 回收整个堆(Young + Old + Metaspace)。触发条件因收集器而异。

公共触发路径

分配失败 → Young GC → 空间担保失败 ──→ Full GC
                                        ↑
Metaspace 满 ────────────────────────→ Full GC
System.gc() ─────────────────────────→ Full GC(-XX:+DisableExplicitGC 可禁用)
并发 GC 失败(CMS/G1/ZGC/Shenandoah)→ 退化为 Full GC

各收集器触发条件

收集器触发场景关键参数HotSpot 源码(src/hotspot/share/gc/
Serial / Parallel GCOld Gen 分配失败-Xmxparallel/psParallelCompact.cpp
Metaspace 满-XX:MetaspaceSize(触发扩容)<br>-XX:MaxMetaspaceSize(上限,满则 Full GC)shared/metaspace.cpp
Young GC 空间担保失败(promotion failed)-XX:+HandlePromotionFailure(JDK 6u24+ 默认 true)parallel/psScavenge.cpp
CMSOld Gen 使用率超阈值(触发并发 GC,未到 Full GC)-XX:CMSInitiatingOccupancyFraction(默认 92%)<br>-XX:+UseCMSInitiatingOccupancyOnlycms/concurrentMarkSweepGeneration.cpp
Concurrent Mode Failure:并发标记期间 Old Gen 耗尽,退化为 Serial Old Full GC调低 -XX:CMSInitiatingOccupancyFraction 预留空间同上
Promotion Failed:晋升时 Old Gen 空间不足同上
碎片过多无法分配大对象-XX:CMSFullGCsBeforeCompaction(默认 0,每次压缩)同上
G1Evacuation Failure:疏散失败,退化为 Full GC-XX:G1ReservePercent(预留空间,默认 10%)g1/g1CollectedHeap.cpp
Humongous 对象分配频繁导致 Mixed GC 来不及-XX:G1HeapRegionSize(调大减少 Humongous 比例)g1/g1Policy.cpp
并发标记速度跟不上分配速度-XX:InitiatingHeapOccupancyPercent(IHOP,默认 45%)g1/g1Policy.cpp
ZGC分配速率过快,并发 GC 来不及(触发阻塞 GC,非传统 Full GC)-XX:ZCollectionInterval<br>-XX:ZAllocationSpikeTolerancez/zDriver.cpp
ShenandoahDegenerated GC:并发期间分配压力过大,退化为 STW-XX:ShenandoahGCHeuristics(adaptive/compact/aggressive)shenandoah/shenandoahControlThread.cpp
Full GC:Degenerated GC 仍失败后的最后兜底同上
所有收集器System.gc()-XX:+DisableExplicitGC(禁用)<br>-XX:+ExplicitGCInvokesConcurrent(改为并发)shared/gcCause.hpp
Heap dump / JVMTI 请求jmap -dump:...services/heapDumper.cpp

判断 Full GC 的实用方法

# GC 日志中 Full GC 关键字
-Xlog:gc*:file=gc.log   # JDK 9+,统一日志
-XX:+PrintGCDetails      # JDK 8,旧式

# 触发原因会出现在日志中,例如:
# [GC cause: Allocation Failure]
# [GC cause: Metadata GC Threshold]
# [GC cause: System.gc()]

参考资料

  • David Ungar 1984 年论文:"Generation Scavenging"
  • 《深入理解 Java 虚拟机》第 3 章 — 周志明
  • OpenJDK 源码:src/hotspot/share/gc/(各收集器触发逻辑)
  • JEP 333(ZGC)、JEP 189(Shenandoah)、JEP 173(G1 成为默认)官方设计文档
  • CMS 收集器
  • G1 收集器
  • ZGC 与 Shenandoah
  • GC 调优实践
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论