资讯动态

04-05-源码-Mono GC分代与晋升机制

发布时间:2026/10/19 4:14:22 来源:尧图企业网站定制
源码深度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 GCMinor GC触发条件// sgen-gc.c static gboolean sgen_should_collect_nursery (void) { SgenGC *gc sgen_gc; // 条件1Nursery分配指针接近上限 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 | (0clean, 1dirty) ------------------------------ 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中被标记为PinnedMinor 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 GCMajor GC触发条件// sgen-gc.c static gboolean sgen_should_collect_major (void) { SgenGC *gc sgen_gc; // 条件1Major堆使用率过高 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; } // 条件3Minor 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 GCMajor GC回收范围Nursery (2-4MB)全堆暂停时间1-5ms10-100ms触发频率高低策略CopyingMark-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的基础。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑