Card Table详解

JVM Card Table详解:垃圾收集器的加速利器

前言

在JVM垃圾收集的世界里,有一个默默无闻但极其重要的技术——Card Table(卡片表)。它就像垃圾收集器的”加速器”,让分代垃圾收集器能够高效地处理跨代引用。本文将深入解析Card Table的工作原理、实现细节,以及它在不同垃圾收集器中的应用。

一、什么是Card Table

基本概念

Card Table(卡片表)是垃圾收集器用来记录跨代引用跨Region引用的数据结构,用于加速垃圾收集过程中的引用扫描。

解决的核心问题

在分代垃圾收集中,一个关键挑战是如何高效地找到跨代引用

1
2
3
4
5
6
问题场景:
老年代对象 → 引用 → 新生代对象

挑战:
如果新生代GC时需要扫描整个老年代来找到这些引用,
那么GC效率会极低!

基本原理

分代内存结构

1
2
3
4
5
6
7
8
堆内存布局:
┌─────────────────┬─────────────────┐
│ 老年代 │ 新生代 │
│ │ │
│ 老对象A ──────→ ─→ 新对象B │ ← 跨代引用
│ │ │
│ 老对象C │ 新对象D │
└─────────────────┴─────────────────┘

Card Table结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
卡片表(内存中的位图):
每1个Card对应512字节的堆内存

┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ ← 脏卡标记
└───┴───┴───┴───┴───┴───┴───┴───┘
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ └── Card 7:有引用变化
│ │ │ │ │ │ └─────── Card 6:有引用变化
│ │ │ │ │ └─────────── Card 5:干净
│ │ │ │ └─────────────── Card 4:有引用变化
│ │ │ └─────────────────── Card 3:干净
│ │ └─────────────────────── Card 2:有引用变化
│ └─────────────────────────── Card 1:干净
└─────────────────────────────── Card 0:干净

关键概念

  • Card(卡片):对应堆内存中的512字节区域
  • Dirty Card(脏卡):标记为1的卡片,表示该区域有引用变化
  • Clean Card(干净卡):标记为0的卡片,表示该区域无引用变化

二、Card Table工作机制

工作流程可视化

sequenceDiagram
    participant App as 应用线程
    participant WB as 写屏障
    participant CT as Card Table
    participant GC as GC线程

    Note over App,GC: 写屏障标记过程
    App->>WB: 老年代对象引用新生代对象
    WB->>WB: 检测跨代引用
    WB->>CT: 标记对应Card为脏
    Note over CT: Card状态: 0→1

    Note over App,GC: GC扫描过程
    GC->>CT: 遍历Card Table
    CT->>GC: 返回脏卡列表
    GC->>GC: 只扫描脏卡对应内存区域
    GC->>GC: 查找跨代引用
    GC->>CT: 清理脏卡标记

Card Table工作流程图

flowchart TD
    START[应用运行] --> WRITE[对象引用更新]
    WRITE --> CHECK{是否跨代引用?}
    CHECK -->|是| MARK[标记Card为脏]
    CHECK -->|否| NORMAL[正常执行]
    MARK --> CONTINUE[继续应用执行]
    NORMAL --> CONTINUE

    CONTINUE --> GC_TRIGGER[GC触发]
    GC_TRIGGER --> SCAN[扫描Card Table]
    SCAN --> FILTER[只处理脏卡区域]
    FILTER --> RECOVER[回收垃圾对象]
    RECOVER --> CLEAR[清理Card Table]
    CLEAR --> END[GC完成]

    style MARK fill:#ffeb3b
    style SCAN fill:#add8e6
    style FILTER fill:#90ee90

写屏障实现

1. 基本写屏障

1
2
3
4
5
6
7
8
9
10
11
12
13
// 伪代码:写屏障在引用更新时的工作
void write_field(Object obj, Object new_value) {
if (is_in_old_gen(obj) && is_in_young_gen(new_value)) {
// 老年代对象引用新生代对象
mark_card_dirty(obj); // 标记对应的Card为脏
}
obj.field = new_value; // 执行实际的写操作
}

void mark_card_dirty(Object obj) {
int card_index = get_card_index(obj);
card_table[card_index] = DIRTY; // 标记为脏卡
}

2. 精确标记优化

1
2
3
4
5
6
7
8
9
10
11
12
// 优化:更精确的脏卡判断
void precise_mark_card(Object obj, Object new_value) {
if (should_mark_card(obj, new_value)) {
// 只在真正需要时才标记
int card_index = get_card_index(obj);
card_table[card_index] = DIRTY;
}
}

boolean should_mark_card(Object old_obj, Object new_obj) {
return is_cross_generation_reference(old_obj, new_obj);
}

GC扫描过程

1
2
3
4
5
6
7
8
9
10
老年代回收扫描过程:

1. 枚举老年代GC Roots
2. 遍历Card Table:
├── 找到脏卡(标记为1的卡片)
├── 扫描对应内存区域的所有对象
├── 查找指向新生代的引用
└── 将新生代对象加入根集合

3. 扫描完成,清理Card Table

三、不同垃圾收集器中的Card Table实现

不同GC中的跨代引用处理对比

graph TD
    subgraph "Parallel GC"
        P1[传统Card Table]
        P2[全局共享]
        P3[串行更新]
        P4[简单可靠]
    end

    subgraph "CMS GC"
        C1[并发Card Table]
        C2[原子操作]
        C3[预清理阶段]
        C4[处理并发冲突]
    end

    subgraph "G1 GC"
        G1[Remembered Set]
        G2[Region级别]
        G3[精细化记录]
        G4[高效查询]
    end

    subgraph "ZGC"
        Z1[染色指针]
        Z2[读屏障]
        Z3[无Card Table]
        Z4[全并发]
    end

    style P1 fill:#add8e6
    style C1 fill:#ffeb3b
    style G1 fill:#90ee90
    style Z1 fill:#ff6b6b

Card Table vs Remembered Set 对比

特性 Card Table Remembered Set
粒度 粗粒度(512字节) 细粒度(对象级)
存储方式 全局共享位图 Region独立存储
更新开销 中等
查询效率 中等
内存占用 低(约1%堆) 高(约10-20%堆)
并发支持 需要特殊处理 原生支持并发

跨代引用处理演进图

graph LR
    subgraph "演进路径"
        A[全局Card Table] --> B[并发Card Table]
        B --> C[Remembered Set]
        C --> D[染色指针]
    end

    subgraph "技术特点"
        A1[简单] --> A2[串行]
        B1[原子操作] --> B2[预清理]
        C1[Region级] --> C2[精细化管理]
        D1[无额外结构] --> D2[指针编码]
    end

    style A fill:#add8e6
    style B fill:#ffeb3b
    style C fill:#90ee90
    style D fill:#ff6b6b

四、各种GC的具体实现

1. Parallel GC中的Card Table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
特点:
- 传统分代模型
- 单个Card Table
- 简单的脏卡标记

Card Table布局:
[Young Gen Cards] [Old Gen Cards]
0-1024 1024-2048

优势:
- 实现简单
- 内存开销小
- 串行GC无并发问题

劣势:
- 粒度较粗
- 扫描效率一般

2. CMS GC中的Card Table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
特点:
- 针对并发优化
- 支持并发更新
- 需要处理并发问题

并发更新策略:
- 使用原子操作标记脏卡
- 可能出现"脏卡延迟清理"问题
- 需要额外的预清理阶段

问题:并发标记过程中的脏卡清理
┌─────────────────────────────────┐
│ 并发标记阶段 │
├─────────────────────────────────┤
│ 1. 清理脏卡 │
│ 2. 应用线程继续执行,产生新脏卡 │
│ 3. 标记线程处理新脏卡 │
│ 4. 循环处理... │
└─────────────────────────────────┘

解决方案:
- 预清理阶段:提前处理新脏卡
- 重新标记阶段:最终确认所有引用

3. G1 GC中的Remembered Set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
G1不是使用传统Card Table,而是使用Remembered Set:

每个Region有自己的Remembered Set:
Region 1: [Card Set 1]
Region 2: [Card Set 2]
Region 3: [Card Set 3]

Remembered Set结构:
├── Hash Table:快速查找
├── Per-Card Table:精细记录
└── Bitmap:空间优化

G1 GC改进:
1. 每个Region独立的Remembered Set
2. 更精细的引用记录
3. 支持并发更新

与传统Card Table对比:
┌─────────────┬─────────────┬─────────────┐
│ 特性 │ Card Table │ Remembered │
├─────────────┼─────────────┼─────────────┤
│ 粒度 │ 粗粒度 │ 细粒度 │
│ 更新开销 │ 低 │ 中等 │
│ 扫描效率 │ 中等 │ 高 │
│ 内存占用 │ 低 │ 高 │
└─────────────┴─────────────┴─────────────┘

4. ZGC中的替代方案

1
2
3
4
5
6
7
8
9
ZGC不使用Card Table,而是:
- 染色指针包含引用信息
- 读屏障处理引用更新
- 无需额外的Card Table开销

优势:
- 无额外内存开销
- 完全并发处理
- 指针本身携带信息

五、Card Table的性能影响

优点

1. 减少扫描范围

1
2
3
4
5
6
7
8
9
10
传统扫描:扫描整个老年代
Card Table:只扫描脏卡对应的内存区域

性能提升:扫描范围可能减少90%+

示例:
4GB老年代:
- 传统扫描:扫描4GB全部内容
- Card Table:扫描100MB脏卡区域
- 性能提升:40倍

2. 提高GC效率

  • 新生代GC时间显著缩短
  • 老年代GC的根扫描时间减少
  • 整体GC吞吐量提升

缺点

1. 写屏障开销

  • 每次引用更新都要检查是否需要标记Card
  • 增加应用程序的执行时间
  • 对写密集型应用影响较大

2. 内存开销

  • Card Table占用额外内存(约堆大小的1%)
  • 在大堆上开销显著

3. 并发复杂性

  • 并发GC需要处理Card Table的并发更新
  • 可能产生伪脏卡

六、Card Table的优化技术

1. 批量处理

1
2
3
4
5
6
7
8
9
// 优化:批量清理脏卡
void batch_clear_dirty_cards() {
// 批量清理,减少系统调用
for (int i = 0; i < batch_size; i++) {
if (card_table[current_batch + i] == DIRTY) {
clear_and_scan_card(current_batch + i);
}
}
}

2. 卡片大小优化

1
2
3
4
5
6
7
8
9
10
11
12
13
不同GC的卡片大小:
- Parallel GC:512字节(默认)
- CMS GC:512字节
- G1 GC:使用Remembered Set,逻辑类似但更精细

选择考虑:
- 太小:Card Table开销大
- 太大:扫描粒度粗,效率低

512字节是一个经验平衡点:
- 内存开销合理(堆的1/1024)
- 扫描粒度适中
- 缓存友好

3. 并发优化

原子操作

1
2
3
4
5
// 并发安全的脏卡标记
void atomic_mark_dirty(int card_index) {
// 使用原子操作避免并发问题
atomic_compare_and_swap(card_table[card_index], CLEAN, DIRTY);
}

延迟清理

1
2
3
4
5
6
7
// 延迟清理策略,减少清理频率
void lazy_clear_cards() {
if (dirty_card_count > threshold) {
batch_clear_dirty_cards();
dirty_card_count = 0;
}
}

七、实际应用案例

案例1:电商网站的GC优化

问题描述
某电商网站在高峰期出现频繁的Young GC,每次耗时较长。

分析过程

  1. 使用GC日志分析发现每次Young GC的根扫描时间过长
  2. 发现老年代到新生代的跨代引用较多
  3. Card Table的脏卡比例很高(80%+)

优化方案

1
2
3
4
5
# 优化JVM参数
-XX:+UseParallelGC # 使用并行GC
-XX:ParallelGCThreads=8 # 设置GC线程数
-XX:+UseParallelOldGC # 老年代并行回收
-XX:ParallelCMSThreads=4 # 并发标记线程数

效果

  • Young GC时间减少40%
  • 系统吞吐量提升25%
  • 用户体验明显改善

案例2:微服务架构的GC调优

场景
微服务应用使用G1 GC,但Mixed GC频率过高。

问题诊断

1
2
3
- Remembered Set占用内存过多(15%堆内存)
- 跨Region引用更新频繁
- Card Table维护开销大

解决方案

1
2
3
4
5
6
# G1 GC优化参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标停顿时间
-XX:G1HeapRegionSize=16m # Region大小
-XX:G1RSetUpdatingPauseTimePercent=5 # RSet更新时间占比
-XX:G1RSetSparseRegionEntries=8 # 稀疏RSet配置

结果

  • Remembered Set内存占用减少到8%
  • Mixed GC频率降低50%
  • 整体GC效率提升30%

八、Card Table相关JVM参数

通用参数

1
2
3
4
# 显示Card Table相关信息
-XX:+PrintGCApplicationStoppedTime # 显示应用暂停时间
-XX:+PrintGCDetails # 详细GC信息
-XX:+PrintGCApplicationConcurrentTime # 应用执行时间

CMS相关参数

1
2
3
4
5
-XX:+UseCMSInitiatingOccupancyOnly     # 仅根据占用率启动CMS
-XX:CMSInitiatingOccupancyFraction=70 # 老年代占用70%时启动CMS
-XX:+UseCMSInitiatingOccupancyOnly # 只根据占用率触发CMS
-XX:+CMSParallelRemarkEnabled # 并行重新标记
-XX:+CMSScavengeBeforeRemark # Remark前进行Young GC

G1相关参数

1
2
3
4
-XX:G1RSetUpdatingPauseTimePercent=5  # RSet更新时间占比
-XX:G1RSetSparseRegionEntries=8 # 稀疏RSet条目数
-XX:G1RSetRegionEntries=64 # RSet条目数
-XX:G1MixedGCCountTarget=8 # 混合GC目标次数

九、总结

Card Table作为JVM垃圾收集器的重要优化技术,通过空间换时间的策略显著提高了垃圾收集的效率。

关键价值

  1. 性能提升:大幅减少GC扫描范围,提高GC效率
  2. 分代支持:使分代垃圾收集器成为可能
  3. 可扩展性:支持大内存堆的垃圾收集

技术演进

从传统的Card Table到现代的Remembered Set和染色指针,跨代引用处理技术在不断演进,目标是:

  • 更少的内存开销
  • 更高的处理效率
  • 更好的并发支持

最佳实践

  1. 监控Card Table状态:定期检查脏卡比例和维护开销
  2. 合理配置参数:根据应用特点调整JVM参数
  3. 选择合适的GC:根据场景选择最适合的垃圾收集器
  4. 持续优化:结合监控数据持续调优

Card Table虽然只是JVM垃圾收集中的一个组件,但理解它的工作原理对于进行JVM性能调优和问题诊断具有重要意义。它是连接理论与实践的桥梁,也是深入理解JVM内存管理的关键。