源码深度:Boehm GC分配与碎片管理
元数据
- 系列:Unity和C#的GC剖析与对比
- 章节:05-源码深度-Boehm GC与IL2CPP
- 编号:05-03
- 标签:Boehm GC, Size-Class分配, 空闲链表, 碎片合并, Lazy Free
- 阅读时间:~30分钟
- 依赖知识:05-01数据结构、05-02标记阶段
概述
Boehm GC的分配系统采用分级空闲链表(Size-Class Free Lists)策略,将对象按大小分组,每组维护独立的空闲链表。由于Boehm GC不支持压缩和对象移动,碎片管理是其核心挑战。本文深入分析Size-Class分配机制、空闲链表管理、碎片合并策略以及Lazy Free优化。
Size-Class分配机制
大小分级
// gc_alloc.c // Boehm GC将对象大小分为多个等级(Size Class) // 每个等级对应一个空闲链表 // 小对象分级(word为单位) // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56, 64, ... // 使用近似2的幂次分组,减少分级数量 #define GC_TINY_FREELISTS 64 // 小对象分级数 // 大小到Size-Class的映射 static inline int GC_size_to_class (size_t size) { // 转换为word数 size_t words = (size + sizeof(word) - 1) / sizeof(word); if (words <= GC_TINY_FREELISTS) { // 小对象:直接使用words作为索引 return words - 1; } // 大对象:使用对数分组 int log = 0; size_t s = words; while (s > 1) { s >>= 1; log++; } return GC_TINY_FREELISTS + log; } // Size-Class到实际大小的映射 static size_t GC_class_to_size[] = { 1 * sizeof(word), // class 0 2 * sizeof(word), // class 1 3 * sizeof(word), // class 2 // ... 64 * sizeof(word), // class 63 128 * sizeof(word), // class 64 (大对象) 256 * sizeof(word), // class 65 // ... };分配入口
// gc_inline.h // 内联分配函数(性能关键路径) static inline void * GC_malloc_inline (size_t size) { // 1. 对齐大小 size = GC_align_size(size); // 2. 计算Size-Class int size_class = GC_size_to_class(size); size_t actual_size = GC_class_to_size[size_class]; // 3. 从空闲链表分配 obj_head *obj = GC_free_lists[size_class]; if (obj) { // 快路径:空闲链表非空 GC_free_lists[size_class] = obj->next; return obj; } // 慢路径:空闲链表为空 return GC_malloc_slow(size, size_class, actual_size); }快路径分配
// gc_inline.h // 快路径:从空闲链表取出一个块 static inline void * GC_malloc_fast (int size_class) { obj_head *obj = GC_free_lists[size_class]; if (GC_LIKELY(obj != NULL)) { // 空闲链表非空,取出头节点 GC_free_lists[size_class] = obj->next; // 清零对象(如果需要) // Boehm GC默认不清零,由调用方负责 return obj; } return NULL; // 触发慢路径 }慢路径分配
// gc_alloc.c static void * GC_malloc_slow (size_t size, int size_class, size_t actual_size) { // 1. 检查是否需要GC if (GC_proc_instance.used_heap_size > GC_proc_instance.gc_threshold) { GC_try_to_collect(GC_never_stop_func); } // 2. 尝试从现有堆块中获取空闲块 void *obj = GC_alloc_from_blocks(size_class, actual_size); if (obj) return obj; // 3. 分配新的堆块 hblk *new_hblk = GC_alloc_heap_sect(HBLKSIZE); if (!new_hblk) { // OOM return GC_oom_fn(size); } // 4. 将新堆块切分为对象大小的块 GC_init_hblk(new_hblk, actual_size, NORMAL); // 5. 从新堆块中分配 obj = GC_alloc_from_hblk(new_hblk, actual_size); return obj; }从堆块中分配
// gc_alloc.c // 从堆块的空闲链表中分配 static void * GC_alloc_from_hblk (hblk *h, size_t obj_size) { hblkhdr *hdr = GC_get_header((ptr_t)h); // 检查块是否有空闲块 if (hdr->hb_free_list == NULL) { return NULL; } // 取出空闲链表头 obj_head *obj = hdr->hb_free_list; hdr->hb_free_list = obj->next; // 更新统计 GC_proc_instance.used_heap_size += obj_size; return obj; } // 从多个堆块中查找可用块 static void * GC_alloc_from_blocks (int size_class, size_t obj_size) { // 遍历所有堆块,查找匹配大小的块 for (int i = 0; i < GC_proc_instance.heap_sects_count; i++) { hblk *h = GC_proc_instance.heap_sects[i]; hblkhdr *hdr = GC_get_header((ptr_t)h); if (hdr->hb_obj_size == obj_size && hdr->hb_free_list != NULL) { return GC_alloc_from_hblk(h, obj_size); } } return NULL; }空闲链表管理
空闲链表初始化
// gc_alloc.c // 将新堆块切分为相同大小的空闲块 static void GC_init_hblk (hblk *h, size_t obj_size, int obj_kind) { hblkhdr *hdr = GC_get_header((ptr_t)h); // 设置堆块头 hdr->hb_obj_size = obj_size; hdr->hb_obj_kind = obj_kind; hdr->hb_block_kind = BLK_FREE; hdr->hb_last_gc = GC_proc_instance.gc_no; // 清除标记位 memset(hdr->hb_marks, 0, sizeof(hdr->hb_marks)); // 初始化空闲链表 int obj_count = HBLKSIZE / obj_size; obj_head *prev = NULL; for (int i = 0; i < obj_count; i++) { obj_head *obj = (obj_head*)((char*)h + i * obj_size); obj->next = prev; prev = obj; } hdr->hb_free_list = prev; // 更新全局统计 GC_proc_instance.free_bytes += obj_count * obj_size; }空闲链表分配可视化
堆块(4KB), obj_size=64B: 初始化后: +--------+--------+--------+--------+--------+--------+ | Free_0 | Free_1 | Free_2 | Free_3 | ... | Free_63| +--------+--------+--------+--------+--------+--------+ | | | | | v v v v v NULL Free_0 Free_1 Free_2 Free_62 (链表头) 分配3个对象后: +--------+--------+--------+--------+--------+--------+ | Obj1 | Obj2 | Obj3 | Free_3 | ... | Free_63| +--------+--------+--------+--------+--------+--------+ | v Free_4 -> Free_5 -> ... -> NULL (链表头)碎片管理
碎片问题
由于Boehm GC不支持压缩和对象移动,长期运行后堆中会产生碎片:
碎片示例: +----+----+----+----+----+----+----+----+----+----+ |Live|Free|Live|Free|Free|Live|Free|Live|Free|Free| | 64 | 64 | 64 | 64 | 64 | 64 | 64 | 64 | 64 | 64 | +----+----+----+----+----+----+----+----+----+----+ 总空闲: 6 * 64 = 384B 但最大连续空闲: 2 * 64 = 128B 无法满足256B的分配请求(即使总空闲足够)碎片合并策略
// gc_reclaim.c // Boehm GC的碎片合并策略 // 由于不支持对象移动,只能在清扫时合并相邻空闲块 static void GC_coalesce_free_blocks (void) { // 遍历所有堆块 for (int i = 0; i < GC_proc_instance.heap_sects_count; i++) { hblk *h = GC_proc_instance.heap_sects[i]; hblkhdr *hdr = GC_get_header((ptr_t)h); // 只处理可回收类型的块 if (hdr->hb_obj_kind == NORMAL || hdr->hb_obj_kind == PTRFREE) { GC_coalesce_block(h, hdr); } } } static void GC_coalesce_block (hblk *h, hblkhdr *hdr) { size_t obj_size = hdr->hb_obj_size; int obj_count = HBLKSIZE / obj_size; // 检查块中所有对象的标记状态 int live_count = 0; int free_count = 0; for (int i = 0; i < obj_count; i++) { ptr_t obj = (ptr_t)h + i * obj_size; if (IS_MARKED(hdr, obj)) { live_count++; } else { free_count++; } } if (free_count == obj_count) { // 整个块都空闲:可以归还给OS或加入大块池 GC_free_hblk(h); } else if (live_count == obj_count) { // 整个块都存活:无需处理 hdr->hb_block_kind = BLK_USED; } else { // 部分空闲:重建空闲链表 GC_rebuild_free_list(h, hdr); } } static void GC_rebuild_free_list (hblk *h, hblkhdr *hdr) { size_t obj_size = hdr->hb_obj_size; int obj_count = HBLKSIZE / obj_size; // 清空现有空闲链表 hdr->hb_free_list = NULL; // 遍历块中所有对象 obj_head *prev = NULL; for (int i = 0; i < obj_count; i++) { ptr_t obj = (ptr_t)h + i * obj_size; if (!IS_MARKED(hdr, obj)) { // 未标记的对象加入空闲链表 obj_head *free_obj = (obj_head*)obj; free_obj->next = prev; prev = free_obj; } } hdr->hb_free_list = prev; }块级合并
// gc_reclaim.c // 相邻堆块的合并(如果两个相邻块都完全空闲) static void GC_coalesce_adjacent_hblks (void) { // 按地址排序堆块 GC_sort_heap_sects(); for (int i = 0; i < GC_proc_instance.heap_sects_count - 1; i++) { hblk *h1 = GC_proc_instance.heap_sects[i]; hblk *h2 = GC_proc_instance.heap_sects[i + 1]; // 检查是否地址相邻 if ((ptr_t)h1 + HBLKSIZE == (ptr_t)h2) { hblkhdr *hdr1 = GC_get_header((ptr_t)h1); hblkhdr *hdr2 = GC_get_header((ptr_t)h2); // 两个块都必须完全空闲 if (hdr1->hb_block_kind == BLK_FREE && hdr2->hb_block_kind == BLK_FREE) { // 合并为一个大块 GC_merge_hblks(h1, h2); // 从数组中移除h2 GC_remove_heap_sect(i + 1); i--; // 重新检查当前位置 } } } }Lazy Free机制
什么是Lazy Free
Lazy Free(懒惰释放)是Boehm GC的一种优化策略:在GC清扫阶段,不立即回收所有未标记对象,而是延迟到分配时按需回收。这减少了GC暂停时间,将清扫工作分摊到后续的分配操作中。
// gc_reclaim.c // Lazy Free:标记完成后不立即清扫,而是在分配时按需回收 // 标记完成后设置"需要清扫"标志 void GC_start_reclaim (void) { if (GC_proc_instance.lazy_free) { // Lazy Free模式:只标记需要清扫的块 GC_mark_blocks_for_reclaim(); // 不立即清扫 } else { // 立即清扫模式 GC_reclaim_all(); } } // 标记需要清扫的块 static void GC_mark_blocks_for_reclaim (void) { for (int i = 0; i < GC_proc_instance.heap_sects_count; i++) { hblk *h = GC_proc_instance.heap_sects[i]; hblkhdr *hdr = GC_get_header((ptr_t)h); // 检查块中是否有未标记对象 GC_bool needs_reclaim = FALSE; size_t obj_size = hdr->hb_obj_size; int obj_count = HBLKSIZE / obj_size; for (int j = 0; j < obj_count; j++) { ptr_t obj = (ptr_t)h + j * obj_size; if (!IS_MARKED(hdr, obj)) { needs_reclaim = TRUE; break; } } if (needs_reclaim) { hdr->hb_block_kind = BLK_NEEDS_RECLAIM; } } }分配时Lazy Free
// gc_alloc.c // 分配时检查块是否需要清扫 static void * GC_alloc_from_blocks_lazy (int size_class, size_t obj_size) { for (int i = 0; i < GC_proc_instance.heap_sects_count; i++) { hblk *h = GC_proc_instance.heap_sects[i]; hblkhdr *hdr = GC_get_header((ptr_t)h); if (hdr->hb_obj_size != obj_size) continue; // 检查块是否需要清扫 if (hdr->hb_block_kind == BLK_NEEDS_RECLAIM) { // Lazy Free:在分配前清扫这个块 GC_reclaim_block_now(h, hdr); } // 从清扫后的块中分配 if (hdr->hb_free_list != NULL) { return GC_alloc_from_hblk(h, obj_size); } } return NULL; } // 立即清扫单个块 static void GC_reclaim_block_now (hblk *h, hblkhdr *hdr) { size_t obj_size = hdr->hb_obj_size; int obj_count = HBLKSIZE / obj_size; // 重建空闲链表 obj_head *prev = NULL; for (int i = 0; i < obj_count; i++) { ptr_t obj = (ptr_t)h + i * obj_size; if (!IS_MARKED(hdr, obj)) { // 未标记对象:加入空闲链表 obj_head *free_obj = (obj_head*)obj; free_obj->next = prev; prev = free_obj; } else { // 已标记对象:清除标记位(为下次GC准备) CLEAR_MARK(hdr, obj); } } hdr->hb_free_list = prev; hdr->hb_block_kind = BLK_FREE; // 更新统计 int free_count = 0; obj_head *p = prev; while (p) { free_count++; p = p->next; } GC_proc_instance.free_bytes += free_count * obj_size; }Lazy Free的优势
// gc_reclaim.c // Lazy Free的性能优势 // 不使用Lazy Free: // GC暂停时间 = 标记时间 + 全部清扫时间 // 标记: O(存活对象数) // 清扫: O(堆总大小) <-- 可能很长 // // 使用Lazy Free: // GC暂停时间 = 标记时间 + 标记块时间 // 标记: O(存活对象数) // 标记块: O(堆块数) <-- 很快 // // 清扫工作分摊到后续分配: // 每次分配时清扫一个块 // 分配开销增加少量清扫时间 // 但GC暂停时间显著减少 // 伪代码对比 void gc_without_lazy() { mark_phase(); // 暂停 sweep_all_blocks(); // 暂停(长) // 总暂停: mark + sweep_all } void gc_with_lazy() { mark_phase(); // 暂停 mark_blocks_for_reclaim(); // 暂停(短) // 总暂停: mark + mark_blocks(短) // 清扫在后续分配中完成 void* alloc() { if (block_needs_reclaim) sweep_block_now(); // 分摊到分配 return alloc_from_block(); } }大对象分配
// gc_alloc.c // 大对象(超过一个堆块大小的对象)的特殊分配 static void * GC_alloc_large (size_t size) { // 计算需要的堆块数 size_t hblks = (size + HBLKSIZE - 1) / HBLKSIZE; size_t total = hblks * HBLKSIZE; // 从OS分配连续堆块 hblk *h = (hblk*)GC_alloc_os_memory( total, GC_PROT_READ | GC_PROT_WRITE); if (!h) { // OOM:尝试GC后重试 GC_try_to_collect(GC_never_stop_func); h = (hblk*)GC_alloc_os_memory(total, GC_PROT_READ | GC_PROT_WRITE); if (!h) return NULL; } // 设置堆块头 hblkhdr *hdr = GC_get_header((ptr_t)h); hdr->hb_obj_size = 0; // 0表示大对象 hdr->hb_obj_kind = NORMAL; hdr->hb_block_kind = BLK_USED; // 大对象大小存储在堆块头的额外字段中 GC_set_large_obj_size(h, size); // 注册到堆段数组 GC_register_large_hblk(h, hblks); // 更新统计 GC_proc_instance.heap_size += total; GC_proc_instance.used_heap_size += total; return (void*)h; }分配性能分析
分配路径性能
// gc_alloc.c // 分配路径的性能统计 typedef struct { int fast_path_count; // 快路径命中次数 int slow_path_count; // 慢路径命中次数 int gc_trigger_count; // GC触发次数 int new_block_count; // 新块分配次数 int64_t total_alloc_bytes; int64_t total_alloc_time_ns; } GC_alloc_stats; static GC_alloc_stats GC_alloc_statistics; // 输出统计 void GC_print_alloc_stats (void) { GC_alloc_stats *s = &GC_alloc_statistics; printf("=== Boehm GC Alloc Stats ===\n"); printf("Fast path: %d (%.1f%%)\n", s->fast_path_count, (double)s->fast_path_count / (s->fast_path_count + s->slow_path_count) * 100); printf("Slow path: %d (%.1f%%)\n", s->slow_path_count, (double)s->slow_path_count / (s->fast_path_count + s->slow_path_count) * 100); printf("GC triggers: %d\n", s->gc_trigger_count); printf("New blocks: %d\n", s->new_block_count); printf("Avg alloc time: %.1f ns\n", (double)s->total_alloc_time_ns / (s->fast_path_count + s->slow_path_count)); }分配路径总结
GC_malloc(size) | +-- GC_align_size(size) // 对齐 +-- GC_size_to_class(size) // 计算Size-Class | +-- 快路径: free_lists[class] != NULL? | YES -> 取出头节点, 返回 [~80%] | NO -> 慢路径 | +-- 慢路径: |-- 检查是否需要GC |-- 遍历现有堆块找空闲块 |-- 如果找到: 返回 [~15%] |-- 如果没找到: |-- 分配新堆块 |-- 初始化空闲链表 |-- 从新块分配 [~5%]总结
| 机制 | 说明 | 优势 | 劣势 |
|---|---|---|---|
| Size-Class分级 | 按大小分组空闲链表 | 减少搜索时间 | 内部碎片 |
| 空闲链表 | 每个Size-Class独立链表 | O(1)分配 | 无法合并跨块 |
| 碎片合并 | 清扫时合并相邻空闲块 | 减少碎片 | 无法消除碎片 |
| Lazy Free | 延迟清扫到分配时 | 减少GC暂停 | 分配延迟增加 |
| 大对象 | 直接OS分配 | 避免碎片化 | 无法压缩 |
Boehm GC的分配系统在"简单"和"效率"之间取得了平衡。Size-Class分级使大多数分配在O(1)时间内完成,Lazy Free将清扫工作分摊到分配中减少了暂停时间。但由于不支持对象移动和压缩,碎片问题是Boehm GC的根本局限——这将在下一章IL2CPP集成中进一步讨论。