专栏文章
专栏文章
GC 算法系列
1. GC 算法 #01:三色标记与写屏障 2. GC 算法 #02:分代 GC 3. GC 算法 #03:G1 GC(Region-based) 4. GC 算法 #04:ZGC(染色指针与负载屏障) 5. GC 算法 #05:引用计数与循环引用检测

GC 算法 #02:分代 GC

发布于 2026-06-02 02:33 👁 10 次阅读
#GC#JVM#算法#engineering#generational#young-gen#old-gen

分代 GC 基于"弱分代假设(Weak Generational Hypothesis)":大多数对象在生命很短后就死亡。通过将堆分为年轻代(Young Generation)和老年代(Old Generation),GC 可以频繁快速地回收短命对象,极少打扰长命对象。本文覆盖 Eden/Survivor 空间的工作机制、晋升规则、跨代引用处理,以及各类边界异常。

相关文章三色标记与写屏障 · G1 GC(Region-based) · ZGC(染色指针与负载屏障)


目录

章节 说明
弱分代假设 为什么分代有效
堆的结构 Eden / S0 / S1 / Old / Metaspace
Minor GC 流程 Eden 满时的年轻代回收
对象晋升规则 何时从 Young 升入 Old
跨代引用处理 Card Table 与 Remembered Set
完整伪代码 Minor GC + 晋升 + 跨代引用
执行追踪 对象从出生到晋升的完整生命周期
Major GC / Full GC 老年代回收的触发与代价
异常场景 晋升失败 / 大对象 / 元空间溢出
参数速查 JVM 关键 GC 参数

弱分代假设

经验性统计(IBM 和 SUN 的研究,1980s~1990s):
  约 80~98% 的对象在分配后不久即死亡
  只有 2~20% 的对象能"活"到一定年龄

推论:
  如果把新对象和老对象分开管理:
    年轻代:空间小(几十~几百 MB),GC 频繁(每秒~每几秒),速度极快
    老年代:空间大(几百 MB~几 GB),GC 极少(几分钟~几十分钟),但代价高

分代 GC 的效率来源:
  Minor GC 只扫描年轻代(1~5% 的堆),不碰老年代
  大多数垃圾在 Minor GC 就被回收
  Full GC 极少触发 → 长暂停时间极少发生

堆的结构

generational gc

JVM 堆结构(Parallel GC / Serial GC 的经典布局):

┌─────────────────────────────────────────────────────────┐
│                    Young Generation                      │
│  ┌──────────────────┐  ┌──────────┐  ┌──────────┐      │
│  │      Eden        │  │    S0    │  │    S1    │      │
│  │   (新对象分配)    │  │ Survivor │  │ Survivor │      │
│  │   约 80% Young   │  │  约 10%  │  │  约 10%  │      │
│  └──────────────────┘  └──────────┘  └──────────┘      │
└──────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│                    Old Generation                        │
│          长命对象,分配失败时晋升至此                     │
│          大小约为 Young 的 2~3 倍                        │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│   Metaspace(类元数据,JDK 8+,不在堆内,受 OS 限制)     │
└─────────────────────────────────────────────────────────┘

Survivor 空间(S0/S1):
  任意时刻只有一个 Survivor 处于活跃状态("From")
  另一个是空的("To")
  Minor GC 后 From 和 To 互换角色

Minor GC 流程

触发条件:Eden 空间耗尽(无法为新对象分配内存)

Minor GC 使用"复制算法"(Copying Collection):
  存活对象 → 复制到 To Survivor 或老年代
  Eden + From Survivor → 清空

流程:
  1. Stop-The-World(暂停所有用户线程)
  2. 扫描 GC Roots(栈、静态变量)
     标记 Eden + From Survivor 中的存活对象
  3. 将存活对象复制到 To Survivor(或直接晋升 Old)
     age(年龄)+= 1
  4. 清空 Eden 和 From Survivor
  5. 交换 From 和 To 的角色(From ↔ To)
  6. 恢复用户线程

复制算法的优势:
  只需遍历存活对象(少),不需遍历所有对象
  复制后空间紧凑,无碎片,分配速度极快(指针碰撞)
  内存利用率:Eden + 1 个 Survivor(约 90% Young 可用)

对象晋升规则

规则 1:年龄阈值晋升

每经历一次 Minor GC 存活,对象 age += 1
age 存储在对象头(Mark Word)的 4 bit 中(最大 15)

if obj.age >= MaxTenuringThreshold(默认 15):
    晋升到 Old Generation

// 参数:-XX:MaxTenuringThreshold=15

规则 2:动态年龄判断

不总是等到 age=15 才晋升,JVM 会动态计算:

if Σ(age <= N 的对象大小) > Survivor * 0.5:
    所有 age >= N 的对象直接晋升 Old
    // 防止 Survivor 空间过于拥挤

直觉:Survivor 的一半被 age <= N 的对象占满了
     说明 age > N 的对象应该"毕业"了,不要再占年轻代空间

规则 3:大对象直接进 Old

超过 PretenureSizeThreshold 的对象:
  跳过 Eden,直接分配到 Old Generation
  // 避免 Eden → Survivor → Old 的多次复制开销
  // 参数:-XX:PretenureSizeThreshold=1MB

注意:G1 GC 没有此参数,用 Region 大小的一半作为阈值

规则 4:To Survivor 空间不足

Minor GC 复制时,To Survivor 剩余空间不足:
  → 将部分存活对象直接晋升 Old(提前晋升)
  → 若 Old 也不够 → 触发 Full GC

这是导致 Full GC 的常见原因之一

跨代引用处理

问题:老年代对象引用了年轻代对象(跨代引用),Minor GC 只扫描年轻代,怎么找到这些引用?

朴素做法:扫描整个老年代找对老年代引用 → 太慢(Old 很大)

解法:Card Table(卡表)

Card Table:
  将老年代堆划分为一系列"卡片",每个卡片约 512 字节
  每个卡片对应 Card Table 中的一个 byte(dirty bit)
  当老年代中某个对象的引用字段被修改时(写屏障触发):
    对应卡片 byte = dirty(1)

Minor GC 时:
  只扫描 Card Table 中标记为 dirty 的卡片
  对这些卡片中的对象检查是否有对年轻代的引用
  找到跨代引用 → 作为 Minor GC 的额外根

效果:
  只扫描有变化的老年代卡片(通常远少于整个老年代)
  将 Minor GC 的扫描范围限制在年轻代 + dirty 卡片

写屏障触发卡表更新

# 每次写引用时(src.field = new_ref)
def write_barrier_card_table(src, field, new_ref):
    src.field = new_ref
    if is_in_old_gen(src) and is_in_young_gen(new_ref):
        # 老年代对象引用了年轻代对象 → 标记卡片
        card_index = (address(src) - old_gen_start) >> 9  # / 512
        card_table[card_index] = DIRTY

完整伪代码

def minor_gc():
    STW_begin()

    # 收集所有根(栈 + 静态 + dirty 卡片中的跨代引用)
    roots = []
    roots.extend(scan_gc_roots())             # 栈变量等
    roots.extend(scan_dirty_cards())          # 跨代引用
    reset_dirty_cards()                       # 清除 dirty 标记

    to_space   = get_empty_survivor()         # "To" Survivor
    old_gen    = get_old_gen()
    age_histo  = {}                           # 年龄直方图

    # 复制存活对象
    def copy_object(obj):
        if obj.is_forwarded():
            return obj.forwarding_ptr     # 已被复制,返回新地址

        age = obj.age + 1

        # 判断晋升到 Old 还是 Survivor
        if (age >= MaxTenuringThreshold
                or to_space.remaining() < obj.size()):
            dest = old_gen.allocate(obj.size())
            if dest is None:
                raise PromotionFailure()   # Old 也满了 → 触发 Full GC
        else:
            dest = to_space.allocate(obj.size())

        # 复制对象头 + 数据
        copy(obj, dest)
        dest.age = age
        obj.forwarding_ptr = dest         # 设置转发指针,防止重复处理
        age_histo[age] = age_histo.get(age, 0) + obj.size()

        # 递归扫描子对象
        for child_ref in dest.fields:
            if child_ref is not None and is_in_young_gen(child_ref):
                new_child = copy_object(child_ref)
                update_ref(dest, child_ref, new_child)

        return dest

    for root in roots:
        if root.ref is not None and is_in_young_gen(root.ref):
            new_ref = copy_object(root.ref)
            root.ref = new_ref

    # 清空 Eden 和 From Survivor(复制算法:直接重置指针)
    eden.reset()
    from_survivor.reset()

    # 交换 From/To
    swap(from_survivor, to_survivor)

    # 动态年龄判断:调整 MaxTenuringThreshold
    cumulative = 0
    for age in sorted(age_histo.keys()):
        cumulative += age_histo[age]
        if cumulative > to_survivor.capacity() * 0.5:
            MaxTenuringThreshold = min(age, MaxTenuringThreshold)
            break

    STW_end()

执行追踪

Eden=8MB,S0/S1=1MB,MaxTenuringThreshold=4

T1: 分配对象 A(1MB), B(0.5MB), C(2MB), D(0.5MB)... Eden 使用 8MB
    Eden: [A(age0), B(age0), C(age0), D(age0)...]

T2: Minor GC #1(Eden 满)
    存活对象:A, C(B, D 已无引用)
    A(age0) → 复制到 S0,age=1;C(age0) → 复制到 S0,age=1
    Eden 清空,From=S0,To=S1(空)

T3: 继续分配,Minor GC #2
    A 存活:S0 → S1,age=2;C 存活:S0 → S1,age=2

T4: Minor GC #3,A/C age=3;Minor GC #4,A/C age=4
    age=4 == MaxTenuringThreshold=4
    → A 和 C 晋升到 Old Generation!

T5: Old Generation: [A, C](长命对象)
    Young Generation 继续处理新对象,不影响 A/C

T6: 如果 Old Generation 积累了大量垃圾,触发 Full GC(Major GC)
    Full GC 暂停时间通常 = Minor GC × 10~100 倍

Major GC / Full GC

触发条件(JVM HotSpot):
  1. 老年代使用率 > CMSInitiatingOccupancyFraction(CMS,默认 92%)
  2. 晋升失败(Promotion Failure):Minor GC 时 Old 空间不足
  3. 显式调用 System.gc()(不建议)
  4. Metaspace 空间不足
  5. 堆内存碎片无法满足大对象分配(Parallel GC)

Full GC 的代价:
  暂停时间:几秒~几十秒(JVM 大堆场景)
  回收算法:通常是标记-压缩(Mark-Compact),消除碎片但更慢

减少 Full GC 的方法:
  1. 增大 Old Gen(-Xmx)
  2. 降低晋升速率(增大 MaxTenuringThreshold)
  3. 使用 G1/ZGC 代替 Parallel GC(更短暂停)
  4. 减少大对象分配(避免直接进 Old)

异常场景

晋升失败(Promotion Failure)

Minor GC 时,To Survivor 满 → 晋升到 Old
Old Gen 也满 → Promotion Failure
→ 触发 Full GC(此时 Full GC 在 Minor GC 中途触发,代价更高)

特征:GC 日志中看到 "Promotion failed" 或 "concurrent mode failure"

解法:
  增大 Old Gen 空间
  降低对象晋升速率(优化代码,减少长命对象)
  使用 G1 代替 Parallel GC(G1 有更好的晋升失败处理)

Survivor 空间溢出(Survivor 占比过小)

默认 Eden:S0:S1 = 8:1:1
若对象存活率高(如批量处理场景),Survivor 容纳不下
→ 大量对象提前晋升 Old → Old 快速填满 → 频繁 Full GC

诊断:-XX:+PrintTenuringDistribution 查看各年龄段对象分布
解法:-XX:SurvivorRatio=4(调为 4:1:1)或直接增大 Young Gen

Metaspace 溢出(类加载器泄漏)

现象:java.lang.OutOfMemoryError: Metaspace
原因:动态类加载(反射、CGLib、Groovy)不断创建新类,旧类没有被卸载
     类卸载条件:ClassLoader 无引用 + 对应 Class 无引用 + 对应实例无引用
     → 三个条件同时满足极难,导致 Metaspace 持续增长

解法:
  排查 ClassLoader 泄漏(通常是框架 Bug 或滥用反射)
  设置 -XX:MaxMetaspaceSize 作为安全阀,触发 Full GC 清理

参数速查

参数 默认值 作用
-Xms / -Xmx JVM 决定 堆初始/最大大小(建议相同避免扩缩容)
-Xmn 约 1/3 堆 Young Generation 大小
-XX:SurvivorRatio 8 Eden:S0:S1 = 8:1:1
-XX:MaxTenuringThreshold 15 对象晋升 Old 的年龄阈值
-XX:PretenureSizeThreshold 0(不限) 超过此大小直接进 Old
-XX:+PrintGCDetails false 打印 GC 详情
-XX:+PrintTenuringDistribution false 打印年龄分布
-XX:+UseParallelGC JDK8 默认 使用 Parallel GC
-XX:+UseG1GC JDK9+ 默认 使用 G1 GC

参考资料

  • Wilson, P.R. (1992). Uniprocessor Garbage Collection Techniques
  • Oracle JVM GC Tuning Guide: https://docs.oracle.com/en/java/javase/17/gctuning/
  • 《深入理解 Java 虚拟机》第 3 章:垃圾收集器与内存分配策略
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论