源码深度:Mono GC分代与晋升机制
元数据
- 系列:Unity和C#的GC剖析与对比
- 章节:04-源码深度-Mono GC
- 编号:04-05
- 标签:Mono, SGen, 分代GC, 晋升, Nursery, Major, Remembered Set
- 阅读时间:~35分钟
- 依赖知识:04-01至04-04章节
概述
分代GC基于"弱分代假设"(Weak Generational Hypothesis):绝大多数对象朝生夕灭,老对象倾向于保持存活。SGen将堆分为Nursery(年轻代)和Old Generation(老年代),对Nursery执行频繁但低成本的Minor Collection,对老年代执行低频但全面的Major Collection。本文深入分析SGen的分代策略、Nursery收集、Major收集、对象晋升机制以及Remembered Set的维护。
分代架构总览
堆布局
SGen堆布局: +--------------------------------------------------+ | Nursery (年轻代) | | +--------+--------+--------+--------+--------+ | | | TLAB_A | TLAB_B | TLAB_C | ... | 未分配 | | | +--------+--------+--------+--------+--------+ | | 大小: 2-4MB (可配置) | | 策略: Bump-pointer + TLAB | | 回收: Minor GC (频繁, 快速) | +--------------------------------------------------+ | Major Heap (老年代) | | +--------+--------+--------+--------+--------+ | | | Seg_1 | Seg_2 | Seg_3 | ... | Seg_N | | | +--------+--------+--------+--------+--------+ | | 策略: Mark-Sweep / Compact / Copying | | 回收: Major GC (低频, 全面) | +--------------------------------------------------+ | LOS (大对象空间) | | +--------+--------+--------+ | | | LOS_1 | LOS_2 | LOS_3 | ... | | +--------+--------+--------+ | | 策略: 独立链表管理 | | 回收: 随Major GC一起 | +--------------------------------------------------+分代配置
// sgen-gc.c void sgen_generations_init (void) { SgenGC *gc = &sgen_gc; // Nursery配置 gc->nursery_size = sgen_config_get_int( "MONO_GC_NURSERY_SIZE", SGEN_DEFAULT_NURSERY_SIZE); // 默认2MB gc->nursery_size = MIN(gc->nursery_size, SGEN_MAX_NURSERY_SIZE); // 最大4MB // Major收集器类型 const char *collector = sgen_config_get_string( "MONO_GC_MAJOR_COLLECTOR", "marksweep"); // 默认mark-sweep if (strcmp(collector, "marksweep") == 0) { gc->major_collector = sgen_major_ms_new(); } else if (strcmp(collector, "marksweep-compact") == 0) { gc->major_collector = sgen_major_msc_new(); } else if (strcmp(collector, "copying") == 0) { gc->major_collector = sgen_major_copying_new(); } // LOS阈值 gc->los->threshold = sgen_config_get_int( "MONO_GC_LOS_THRESHOLD", SGEN_LARGE_OBJ_SIZE); // 默认8000字节 // 晋升阈值 gc->promotion_threshold = sgen_config_get_int( "MONO_GC_PROMOTION_AGE", 15); // 默认15次存活后晋升 }Nursery收集(Minor GC)
Minor GC触发条件
// sgen-gc.c static gboolean sgen_should_collect_nursery (void) { SgenGC *gc = &sgen_gc; // 条件1:Nursery分配指针接近上限 size_t used = gc->nursery_alloc_ptr - gc->nursery_start; size_t total = gc->nursery_size; if (used > total * 9 / 10) { // 90%满 return TRUE; } // 条件2:分配请求失败 if (gc->alloc_failed_count > 0) { return TRUE; } // 条件3:手动触发 if (gc->forced_collection) { return TRUE; } return FALSE; }Minor GC完整流程
// sgen-gc.c void sgen_gc_collect_nursery (void) { SgenGC *gc = &sgen_gc; gint64 start_time = mono_100ns_ticks(); // === 1. 请求STW === sgen_stw_request(); // 设置当前收集模式 gc->current_collection_mode = SGEN_GC_MODE_NURSERY; gc->collection_count++; // === 2. 标记阶段 === // 2a. 重置标记栈 sgen_mark_stack_clear(gc->mark_stack); // 2b. 扫描根集合(只标记Nursery中的对象) sgen_mark_scan_roots(); // 2c. 扫描Remembered Set // 老年代中可能有引用指向Nursery sgen_mark_scan_remset(); // 2d. 处理标记栈 sgen_mark_serial(); // 或并行标记 // === 3. 清扫与晋升阶段 === // 对于Nursery,使用Copying策略 // 存活对象被复制(晋升)到Major堆 sgen_nursery_collect(); // === 4. 重置Nursery === sgen_nursery_reset(); // === 5. 恢复线程 === sgen_stw_release(); // 统计 gc->stats.nursery_collections++; gc->stats.nursery_time += mono_100ns_ticks() - start_time; }Nursery收集的Copying策略
Nursery收集使用Copying策略:将存活对象从Nursery复制到Major堆,然后整个Nursery变为空闲。
// sgen-nursery.c static void sgen_nursery_collect (void) { SgenGC *gc = &sgen_gc; // 遍历标记栈中的存活对象 // 这些对象需要被复制到Major堆 // 方式1:从标记结果获取存活对象列表 // 方式2:遍历Nursery,检查mark bit char *p = gc->nursery_start; char *end = gc->nursery_alloc_ptr; while (p < end) { if (sgen_is_valid_object(p)) { MonoVTable *vt = SGEN_OBJECT_VTABLE(p); size_t obj_size = sgen_object_size_from_vt(p, vt); if (SGEN_OBJECT_IS_MARKED(p)) { // 存活对象:晋升到Major堆 sgen_promote_object(p, obj_size, vt); } // 不可达对象:直接丢弃(Nursery整体重置) p += obj_size; } else { // TLAB边界或未分配区域,跳过 p += sizeof(gpointer); } } }对象晋升机制
晋升策略
SGen的Nursery使用Copying策略,所有在Minor GC中存活的对象都会被晋升到Major堆。这与某些GC的"年龄晋升"策略不同——SGen不跟踪对象在Nursery中存活的次数,而是直接晋升所有存活对象。
// sgen-nursery.c static void sgen_promote_object (gpointer obj, size_t size, MonoVTable *vt) { SgenGC *gc = &sgen_gc; // 检查对象是否被Pinned if (SGEN_OBJECT_IS_PINNED(obj)) { // Pinned对象不能移动,留在Nursery中 // 在Nursery重置后,这些对象成为"孤岛" // 需要特殊处理 sgen_handle_pinned_nursery_object(obj, size); return; } // 在Major堆中分配空间 gpointer new_obj = sgen_major_alloc(vt, size); if (!new_obj) { // Major堆空间不足,触发Major GC sgen_gc_collect_major(); new_obj = sgen_major_alloc(vt, size); if (!new_obj) { sgen_handle_oom(size); return; } } // 复制对象内容 memcpy(new_obj, obj, size); // 在旧对象上设置forwarding指针 sgen_set_forwarding_addr(obj, new_obj); // 更新统计 gc->stats.promoted_objects++; gc->stats.promoted_bytes += size; }Forwarding指针
// sgen-nursery.c // 在Nursery对象被晋升后,旧位置需要记录新位置 // 以便更新所有指向旧位置的引用 // 使用vtable的低位标记forwarding #define SGEN_FORWARDING_BIT 0x4 // bit 2 static inline void sgen_set_forwarding_addr (gpointer old_obj, gpointer new_obj) { SgenObjectHeader *header = (SgenObjectHeader*)old_obj; // 设置forwarding位 header->vtable_and_flags = (gpointer)( ((gsize)header->vtable_and_flags) | SGEN_FORWARDING_BIT); // 在对象体中存储新地址 // 使用对象头后的第一个字段 gpointer *forwarding_slot = (gpointer*)((char*)old_obj + sizeof(gpointer)); *forwarding_slot = new_obj; } static inline gpointer sgen_get_forwarding_addr (gpointer obj) { SgenObjectHeader *header = (SgenObjectHeader*)obj; if (((gsize)header->vtable_and_flags) & SGEN_FORWARDING_BIT) { // 已晋升,获取新地址 gpointer *forwarding_slot = (gpointer*)((char*)obj + sizeof(gpointer)); return *forwarding_slot; } return NULL; // 未晋升 }引用更新
晋升后需要更新所有指向Nursery中已移动对象的引用:
// sgen-nursery.c static void sgen_nursery_update_refs (void) { SgenGC *gc = &sgen_gc; // 1. 更新根集合中的引用 sgen_update_root_refs_nursery(); // 2. 更新Major堆中对象的引用 // 这些引用可能指向Nursery中的已晋升对象 SgenHeapSegment *seg; for (seg = gc->major_collector->segments; seg; seg = seg->next) { char *p = seg->start; char *end = seg->alloc_ptr; while (p < end) { if (SGEN_OBJECT_IS_MARKED(p)) { sgen_update_object_refs_nursery(p); } p += sgen_object_size(p); } } // 3. 更新Remembered Set中的引用 sgen_update_remset_refs_nursery(); // 4. 更新已晋升对象内部的引用 // 晋升后的对象可能引用其他Nursery对象 for (seg = gc->major_collector->segments; seg; seg = seg->next) { char *p = seg->start; char *end = seg->alloc_ptr; while (p < end) { // 检查是否是刚晋升的对象 if (sgen_is_promoted_in_this_gc(p)) { sgen_update_object_refs_nursery(p); } p += sgen_object_size(p); } } } static void sgen_update_object_refs_nursery (gpointer obj) { MonoVTable *vt = SGEN_OBJECT_VTABLE(obj); SgenDescriptor desc = vt->gc_descr; if ((desc & DESC_TYPE_MASK) == DESC_TYPE_SMALL) { guint32 bitmap = sgen_descriptor_get_bitmap(desc); gpointer *fields = (gpointer*)((char*)obj + sizeof(SgenObjectHeader)); while (bitmap) { int bit = __builtin_ctz(bitmap); gpointer ref = fields[bit]; if (ref && sgen_is_in_nursery(ref)) { // 引用指向Nursery,检查是否已晋升 gpointer new_addr = sgen_get_forwarding_addr(ref); if (new_addr) { fields[bit] = new_addr; // 如果新地址在Major堆,需要更新Remembered Set // 因为这个对象现在在Major堆中,引用了Nursery对象 // (如果被引用对象也在这次GC中晋升了,则不需要) } else { // 被引用对象未晋升(不可达),清空引用 fields[bit] = NULL; } } bitmap &= (bitmap - 1); } } // 其他描述符类型类似 }Remembered Set维护
写屏障
Remembered Set的维护依赖写屏障(Write Barrier)。每当老年代对象的引用字段被写入新值,且新值指向Nursery对象时,需要记录这个引用关系。
// sgen-remembered-set.c // JIT生成的写屏障调用 void sgen_write_barrier (gpointer obj, gpointer *slot, gpointer value) { // 先写入值 *slot = value; // 检查是否需要记录 // 条件:写入者在Major堆,被引用对象在Nursery if (sgen_is_in_major(obj) && sgen_is_in_nursery(value)) { sgen_remset_record(slot); } } // 卡片表记录 static inline void sgen_remset_record (gpointer *slot) { SgenRememberedSet *remset = sgen_gc.remset; // 计算slot属于哪张卡片 // 卡片表覆盖整个堆(包括Major和Nursery) size_t card_index = ((char*)slot - sgen_gc.heap_start) / remset->card_size; // 标记卡片为脏 remset->card_table[card_index] = CARD_DIRTY; }卡片表布局
堆内存: +---+---+---+---+---+---+---+---+---+---+ | C0| C1| C2| C3| C4| C5| C6| C7| C8| C9| (每张卡片128字节) +---+---+---+---+---+---+---+---+---+---+ 卡片表: +---+---+---+---+---+---+---+---+---+---+ | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | (0=clean, 1=dirty) +---+---+---+---+---+---+---+---+---+---+ Minor GC时只需扫描脏卡片(C1, C4, C8)Remembered Set扫描
// sgen-remembered-set.c void sgen_mark_scan_remset (void) { SgenRememberedSet *remset = sgen_gc.remset; // 遍历卡片表 for (size_t i = 0; i < remset->card_table_size; i++) { if (remset->card_table[i] == CARD_DIRTY) { // 计算卡片对应的内存范围 char *card_start = sgen_gc.heap_start + i * remset->card_size; char *card_end = card_start + remset->card_size; // 扫描卡片内的所有对象 sgen_scan_card(card_start, card_end); // 清除脏标记(Minor GC后卡片不再脏) remset->card_table[i] = CARD_CLEAN; } } } static void sgen_scan_card (char *card_start, char *card_end) { char *p = card_start; while (p < card_end) { if (sgen_is_valid_object(p)) { // 扫描对象中的引用字段 // 这些字段可能指向Nursery中的对象 sgen_mark_scan_object_refs(p); p += sgen_object_size(p); } else { p += sizeof(gpointer); } } }Pinned对象处理
Nursery中的Pinned对象
某些对象不能被移动(如被P/Invoke传递给非托管代码的对象)。这些对象在Nursery中被标记为Pinned,Minor GC时不会被晋升。
// sgen-nursery.c // Pinned对象在Nursery中的管理 // 使用pin位标记 #define SGEN_PIN_BIT 0x2 // vtable低位bit 1 static inline gboolean sgen_object_is_pinned (gpointer obj) { return ((gsize)((SgenObjectHeader*)obj)->vtable_and_flags) & SGEN_PIN_BIT; } static void sgen_handle_pinned_nursery_object (gpointer obj, size_t size) { // Pinned对象不能移动 // 在Nursery重置后,这些对象成为"存活孤岛" // 策略1:将Pinned对象周围的Nursery区域标记为不可分配 // 策略2:将Pinned对象复制到Major堆的特殊Pinned区域 // (但这需要更新外部引用,复杂度高) // SGen采用策略1:在Nursery中保留Pinned对象 // 重置Nursery时跳过Pinned区域 SgenPinnedRegion *region = g_new(SgenPinnedRegion, 1); region->start = obj; region->end = (char*)obj + size; region->next = sgen_gc.pinned_regions; sgen_gc.pinned_regions = region; } // Nursery重置时跳过Pinned区域 static void sgen_nursery_reset (void) { SgenGC *gc = &sgen_gc; // 重置分配指针 gc->nursery_alloc_ptr = gc->nursery_start; // 跳过Pinned区域 SgenPinnedRegion *region = gc->pinned_regions; while (region) { // 在Pinned区域之前设置可分配区域 if (region->start > gc->nursery_alloc_ptr) { // 分配指针可以推进到Pinned区域之前 gc->nursery_alloc_ptr = region->start; } // Pinned区域之后继续分配 // 但需要记录下一个可分配位置 region = region->next; } // 清理Pinned区域列表(下次GC时重新计算) g_list_free(gc->pinned_regions); gc->pinned_regions = NULL; }Major收集(Major GC)
Major GC触发条件
// sgen-gc.c static gboolean sgen_should_collect_major (void) { SgenGC *gc = &sgen_gc; // 条件1:Major堆使用率过高 size_t major_used = gc->major_collector->total_allocated; size_t major_total = 0; SgenHeapSegment *seg; for (seg = gc->major_collector->segments; seg; seg = seg->next) { major_total += seg->end - seg->start; } if (major_used > major_total * 8 / 10) { // 80%满 return TRUE; } // 条件2:碎片率过高 size_t fragmentation = sgen_calculate_fragmentation(); if (fragmentation > major_used * 3 / 10) { // 30%碎片 return TRUE; } // 条件3:Minor GC后仍无法分配 if (gc->alloc_failed_after_minor) { return TRUE; } // 条件4:手动触发 if (gc->forced_major_collection) { return TRUE; } return FALSE; }Major GC流程
// sgen-gc.c void sgen_gc_collect_major (void) { SgenGC *gc = &sgen_gc; gint64 start_time = mono_100ns_ticks(); // === 1. STW === sgen_stw_request(); gc->current_collection_mode = SGEN_GC_MODE_MAJOR; gc->collection_count++; // === 2. 标记阶段 === sgen_mark_stack_clear(gc->mark_stack); // 2a. 扫描所有根集合 sgen_mark_scan_roots(); // 2b. Major GC不需要扫描Remembered Set // 因为整个堆都在回收范围内 // 2c. 处理标记栈 if (gc->gc_thread_count > 1) { sgen_mark_parallel(); } else { sgen_mark_serial(); } // === 3. 清扫阶段 === switch (gc->major_collector->type) { case COLLECTOR_TYPE_MAJOR_MARK_SWEEP: sgen_sweep_mark_sweep(gc->major_collector); break; case COLLECTOR_TYPE_MAJOR_MARK_SWEEP_COMPACT: sgen_sweep_mark_sweep(gc->major_collector); sgen_compact_major(gc->major_collector); break; case COLLECTOR_TYPE_MAJOR_COPYING: sgen_copying_collect(gc->major_collector); break; } // === 4. 清扫LOS === sgen_los_sweep(); // === 5. 处理Finalizer队列 === sgen_process_finalize_queue(); // === 6. 恢复线程 === sgen_stw_release(); // 统计 gc->stats.major_collections++; gc->stats.major_time += mono_100ns_ticks() - start_time; }分代策略的权衡
Minor GC vs Major GC对比
| 特性 | Minor GC | Major GC |
|---|---|---|
| 回收范围 | Nursery (2-4MB) | 全堆 |
| 暂停时间 | 1-5ms | 10-100ms |
| 触发频率 | 高 | 低 |
| 策略 | Copying | Mark-Sweep/Compact |
| 对象移动 | 晋升到Major | 可能压缩 |
| Remembered Set | 需要扫描 | 不需要 |
| 标记范围 | Nursery + RemSet | 全堆 |
晋升开销
// sgen-stats.c // 晋升是Minor GC的主要开销 void sgen_print_promotion_stats (void) { SgenGCStats *s = &sgen_gc.stats; g_print("=== Promotion Stats ===\n"); g_print("Promoted objects: %lld\n", s->promoted_objects); g_print("Promoted bytes: %lld\n", s->promoted_bytes); g_print("Promotion rate: %.1f%%\n", (double)s->promoted_bytes / (sgen_gc.nursery_size) * 100); // 高晋升率意味着Minor GC效率低 // 大量对象存活导致复制开销大 // 可能需要增大Nursery或调整分配策略 }总结
分代GC完整流程: 分配路径: new obj -> TLAB (Nursery) -> bump-pointer分配 new large obj -> LOS Minor GC (频繁): 1. STW 2. 标记: 扫描根 + Remembered Set 3. 晋升: 存活对象复制到Major堆 4. 更新引用: 修复forwarding指针 5. 重置Nursery (Pinned对象除外) 6. 恢复线程 Major GC (低频): 1. STW 2. 标记: 扫描所有根 (全堆) 3. 清扫: 回收未标记对象 4. [可选] 压缩: 移动对象消除碎片 5. 清扫LOS 6. 处理Finalizer 7. 恢复线程 Remembered Set维护: 写屏障 -> 卡片表标记 -> Minor GC时扫描脏卡片SGen的分代策略通过将高频低成本的Minor GC与低频高成本的Major GC分离,在大多数场景下实现了良好的吞吐量与暂停时间平衡。核心设计决策包括:Nursery使用Copying策略直接晋升所有存活对象、卡片表实现的高效Remembered Set、以及Pinned对象的特殊处理。这些机制共同构成了SGen作为现代分代GC的基础。