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

GC 系列 #03:分代假说与分代收集:朝生夕死的工程哲学

发布于 2026-05-25 14:16 👁 27 次阅读
#GC#JVM#理论#分代 GC

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


目录

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

弱分代假说

核心观察

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

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

统计数据:

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

分代假说的推论

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

  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 GC Major GC Full GC
回收区域 Young Gen Old Gen Young + Old + Metaspace
触发条件 Eden 满 Old Gen 满 显式 System.gc() 或空间担保失败
STW 时间 短(毫秒级) 长(秒级,取决于 GC 算法) 最长
频率 高(几秒一次) 低(分钟到小时) 尽量避免

空间担保(Space Guarantee)

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


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 GC Old Gen 分配失败 -Xmx parallel/psParallelCompact.cpp
Metaspace 满 -XX:MetaspaceSize(触发扩容)
-XX:MaxMetaspaceSize(上限,满则 Full GC)
shared/metaspace.cpp
Young GC 空间担保失败(promotion failed) -XX:+HandlePromotionFailure(JDK 6u24+ 默认 true) parallel/psScavenge.cpp
CMS Old Gen 使用率超阈值(触发并发 GC,未到 Full GC) -XX:CMSInitiatingOccupancyFraction(默认 92%)
-XX:+UseCMSInitiatingOccupancyOnly
cms/concurrentMarkSweepGeneration.cpp
Concurrent Mode Failure:并发标记期间 Old Gen 耗尽,退化为 Serial Old Full GC 调低 -XX:CMSInitiatingOccupancyFraction 预留空间 同上
Promotion Failed:晋升时 Old Gen 空间不足 同上
碎片过多无法分配大对象 -XX:CMSFullGCsBeforeCompaction(默认 0,每次压缩) 同上
G1 Evacuation 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
-XX:ZAllocationSpikeTolerance
z/zDriver.cpp
Shenandoah Degenerated GC:并发期间分配压力过大,退化为 STW -XX:ShenandoahGCHeuristics(adaptive/compact/aggressive) shenandoah/shenandoahControlThread.cpp
Full GC:Degenerated GC 仍失败后的最后兜底 同上
所有收集器 System.gc() -XX:+DisableExplicitGC(禁用)
-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)

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