05-02-源码-Boehm GC标记阶段

源码深度:Boehm GC标记阶段

元数据

  • 系列:Unity和C#的GC剖析与对比
  • 章节:05-源码深度-Boehm GC与IL2CPP
  • 编号:05-02
  • 标签:Boehm GC, 保守标记, Root发现, Partial Clean, 标记栈
  • 阅读时间:~30分钟
  • 依赖知识:05-01数据结构

概述

Boehm GC的标记阶段是其最核心的组件。与SGen的精确标记不同,Boehm GC使用保守标记算法:不依赖对象的精确类型信息,而是将对象体中所有看起来像指针的word都当作潜在引用。这种设计使得Boehm GC可以与任意C/C++代码集成,但也带来了伪指针风险和扫描开销。本文深入分析保守标记算法的实现、根发现机制、Partial Clean优化以及标记栈管理。

保守标记算法

标记阶段入口

复制代码
// gc_mark.c
// 标记阶段入口
void
GC_mark_roots (void)
{
    // 1. 清除所有标记位
    GC_clear_marks_all();
    
    // 2. 初始化标记栈
    GC_mark_stack_init();
    
    // 3. 推送根集合
    GC_push_roots();
    
    // 4. 处理标记栈
    while (!GC_mark_stack_empty()) {
        GC_mark_one_step();
    }
    
    // 5. 处理弱引用和finalizer
    GC_mark_finalize();
}

清除所有标记位

复制代码
// gc_mark.c
static void
GC_clear_marks_all (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);
        
        // 清除标记位图
        memset(hdr->hb_marks, 0, sizeof(hdr->hb_marks));
    }
}

标记单个对象

复制代码
// gc_mark.c
// 保守标记:检查地址是否指向堆内对象,如果是则标记
static GC_bool
GC_mark_one (ptr_t addr)
{
    // 1. 检查是否在GC堆内
    hblkhdr *hdr = GC_get_header(addr);
    if (!hdr) {
        return FALSE;  // 不在堆内,不是对象指针
    }
    
    // 2. 计算堆块基址
    hblk *h = (hblk*)((ptr_t)addr & ~(HBLKSIZE - 1));
    
    // 3. 检查地址是否对齐到对象边界
    size_t obj_size = hdr->hb_obj_size;
    if (obj_size == 0) {
        // 大对象:整个堆块是一个对象
        // 地址必须在对象起始位置
        if ((ptr_t)addr != (ptr_t)h) {
            return FALSE;
        }
    } else {
        // 检查地址是否对齐到对象大小
        size_t offset = (ptr_t)addr - (ptr_t)h;
        if (offset % obj_size != 0) {
            // 不对齐:可能是指向对象内部的指针
            // 保守处理:找到对象起始位置
            addr = (ptr_t)h + (offset / obj_size) * obj_size;
        }
    }
    
    // 4. 检查是否已标记
    if (IS_MARKED(hdr, addr)) {
        return FALSE;  // 已标记,跳过
    }
    
    // 5. 设置标记位
    SET_MARK(hdr, addr);
    
    // 6. 如果对象可能含指针,压入标记栈
    if (hdr->hb_obj_kind != PTRFREE) {
        GC_mark_stack_push(addr, hdr);
    }
    
    return TRUE;
}

保守扫描对象体

复制代码
// gc_mark.c
// 扫描对象体中的所有word,保守地标记可能的引用
static void
GC_scan_object_body (ptr_t obj, hblkhdr *hdr)
{
    size_t obj_size = hdr->hb_obj_size;
    
    if (obj_size == 0) {
        // 大对象:获取实际大小
        obj_size = GC_get_large_obj_size(obj);
    }
    
    // 遍历对象体中每个word
    ptr_t end = obj + obj_size;
    for (ptr_t p = obj; p < end; p += sizeof(word)) {
        word value = *(word*)p;
        
        // 保守检查:是否指向堆内
        if (GC_is_heap_ptr(value)) {
            GC_mark_one((ptr_t)value);
        }
    }
}

标记栈处理

复制代码
// gc_mark.c
// 标记栈:存储待扫描的对象(灰色节点)

typedef struct mark_stack_entry {
    ptr_t obj;           // 对象地址
    hblkhdr *hdr;        // 堆块头
    size_t scan_offset;  // 已扫描到的偏移(增量标记用)
} mark_stack_entry;

static mark_stack_entry *GC_mark_stack;
static mark_stack_entry *GC_mark_stack_top;
static mark_stack_entry *GC_mark_stack_limit;

// 压入标记栈
static void
GC_mark_stack_push (ptr_t obj, hblkhdr *hdr)
{
    if (GC_mark_stack_top >= GC_mark_stack_limit) {
        GC_mark_stack_grow();
    }
    
    GC_mark_stack_top->obj = obj;
    GC_mark_stack_top->hdr = hdr;
    GC_mark_stack_top->scan_offset = 0;
    GC_mark_stack_top++;
}

// 弹出并处理一个标记栈条目
static void
GC_mark_one_step (void)
{
    GC_mark_stack_top--;
    mark_stack_entry *entry = GC_mark_stack_top;
    
    // 保守扫描对象体
    GC_scan_object_body(entry->obj, entry->hdr);
}

// 标记栈扩容
static void
GC_mark_stack_grow (void)
{
    size_t old_size = GC_mark_stack_limit - GC_mark_stack;
    size_t new_size = old_size * 2;
    
    mark_stack_entry *new_stack = 
        (mark_stack_entry*)GC_alloc_os_memory(
            new_size * sizeof(mark_stack_entry),
            GC_PROT_READ | GC_PROT_WRITE);
    
    memcpy(new_stack, GC_mark_stack, 
           old_size * sizeof(mark_stack_entry));
    
    // 更新指针
    GC_mark_stack_top = new_stack + (GC_mark_stack_top - GC_mark_stack);
    GC_mark_stack = new_stack;
    GC_mark_stack_limit = new_stack + new_size;
}

Root发现机制

根集合组成

复制代码
// gc_root.c
// Boehm GC的根集合由以下部分组成:
// 1. 线程栈(包括寄存器)
// 2. 静态数据区(.data, .bss段)
// 3. 通过GC_add_roots注册的自定义根
// 4. GC Handle表

void
GC_push_roots (void)
{
    // 1. 扫描所有线程的栈和寄存器
    GC_push_all_thread_stacks();
    
    // 2. 扫描静态数据区
    GC_push_roots_static();
    
    // 3. 扫描注册的自定义根
    GC_push_roots_custom();
    
    // 4. 扫描GC Handle表
    GC_push_roots_handles();
}

线程栈扫描

复制代码
// gc_pthread.c
// 扫描所有线程的栈
static void
GC_push_all_thread_stacks (void)
{
    // 遍历所有注册的线程
    GC_thread *thread = GC_thread_list;
    
    while (thread) {
        // 获取线程的栈范围
        ptr_t stack_bottom = thread->stack_base;
        ptr_t stack_top = thread->stack_ptr;  // 当前栈指针
        
        // 保存的寄存器上下文
        ptr_t regs = (ptr_t)&thread->context;
        size_t regs_size = sizeof(thread->context);
        
        // 1. 保守扫描寄存器
        GC_push_all(regs, regs + regs_size);
        
        // 2. 保守扫描栈
        GC_push_all_stack(stack_bottom, stack_top);
        
        thread = thread->next;
    }
}

保守栈扫描详解

复制代码
// gc_root.c
// 保守扫描一段内存区域
void
GC_push_all (ptr_t bottom, ptr_t top)
{
    ptr_t p;
    
    // 遍历区域中每个word
    for (p = bottom; p < top; p += sizeof(word)) {
        word value = *(word*)p;
        
        // 快速过滤:检查地址范围
        // 优化:先检查是否在堆段的大致范围内
        if (GC_quick_is_heap_ptr(value)) {
            // 精确检查:页表查找
            if (GC_is_heap_ptr(value)) {
                // 标记对象
                GC_mark_one((ptr_t)value);
            }
        }
    }
}

// 快速过滤:使用地址范围检查
static inline GC_bool
GC_quick_is_heap_ptr (word addr)
{
    // 检查是否在堆的大致地址范围内
    // 这是一个快速但不精确的检查
    return (addr >= GC_heap_addr_min && addr < GC_heap_addr_max);
}

静态数据区扫描

复制代码
// gc_root.c
// 扫描静态数据区(.data和.bss段)
static void
GC_push_roots_static (void)
{
    struct roots_static *root = GC_proc_instance.static_roots;
    
    while (root) {
        GC_push_all(root->start, root->end);
        root = root->next;
    }
}

// 注册静态数据区(在GC初始化时调用)
void
GC_register_static_roots (void)
{
    // .data段
    extern char __data_start[];
    extern char __data_end[];
    GC_add_roots(__data_start, __data_end);
    
    // .bss段
    extern char __bss_start[];
    extern char __bss_end[];
    GC_add_roots(__bss_start, __bss_end);
}

GC Handle扫描

复制代码
// gc_handle.c
// Boehm GC支持类似.NET的GC Handle机制
// 在IL2CPP中用于实现GCHandle、WeakReference等

typedef struct {
    void *obj;           // 指向的对象
    int type;            // Handle类型
    // 类型: NORMAL, PINNED, WEAK, WEAK_TRACK_RESURRECTION
} GC_handle_entry;

static GC_handle_entry *GC_handle_table;
static size_t GC_handle_table_size;

static void
GC_push_roots_handles (void)
{
    for (size_t i = 0; i < GC_handle_table_size; i++) {
        if (GC_handle_table[i].obj) {
            switch (GC_handle_table[i].type) {
            case GC_HANDLE_NORMAL:
            case GC_HANDLE_PINNED:
                // 强引用:标记对象
                GC_mark_one((ptr_t)GC_handle_table[i].obj);
                break;
            case GC_HANDLE_WEAK:
            case GC_HANDLE_WEAK_TRACK:
                // 弱引用:不标记,在标记完成后处理
                break;
            }
        }
    }
}

Partial Clean机制

什么是Partial Clean

Partial Clean是Boehm GC的一种优化策略:在标记阶段完成后、清扫阶段之前,对某些"明显不可达"的堆块进行快速清理,减少完整清扫的工作量。

复制代码
// gc_reclaim.c
// Partial Clean:在标记完成后立即回收某些块
void
GC_partial_clean (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);
        
        // 只处理PTRFREE类型的块
        // PTRFREE对象不含指针,可以快速判断可达性
        if (hdr->hb_obj_kind == PTRFREE) {
            GC_partial_clean_block(h, hdr);
        }
    }
}

static void
GC_partial_clean_block (hblk *h, hblkhdr *hdr)
{
    size_t obj_size = hdr->hb_obj_size;
    int obj_count = HBLKSIZE / obj_size;
    
    // 检查块中每个对象的标记状态
    int all_unmarked = TRUE;
    int all_marked = TRUE;
    
    for (int i = 0; i < obj_count; i++) {
        ptr_t obj = (ptr_t)h + i * obj_size;
        if (IS_MARKED(hdr, obj)) {
            all_unmarked = FALSE;
        } else {
            all_marked = FALSE;
            // 回收未标记的对象
            GC_reclaim_obj(obj, obj_size);
        }
    }
    
    if (all_unmarked) {
        // 整个块都不可达,可以归还给OS或加入空闲块池
        GC_free_hblk(h);
    } else if (all_marked) {
        // 整个块都存活,跳过清扫
        hdr->hb_last_gc = GC_proc_instance.gc_no;
    }
}

Partial Clean的优势

复制代码
// gc_reclaim.c
// Partial Clean减少了完整清扫的工作量
// 特别对PTRFREE对象(如byte[], int[]等)效果显著

// 性能对比:
// 不使用Partial Clean:
//   标记 -> 清扫所有块 -> 重建空闲链表
//   时间复杂度: O(堆大小)
//
// 使用Partial Clean:
//   标记 -> Partial Clean(PTRFREE块) -> 清扫剩余块
//   PTRFREE块可以快速处理(无需扫描对象体)
//   时间复杂度: O(非PTRFREE块大小) + O(PTRFREE块数)

标记阶段优化

标记位图缓存优化

复制代码
// gc_mark.c
// Boehm GC使用位图标记,与SGen的vtable低位标记不同
// 位图的优势:不修改对象体,对缓存友好
// 劣势:需要额外的内存开销

// 标记位图布局:
// 堆块(4KB):
// +---+---+---+---+---+---+---+---+---+---+
// | O0| O1| O2| O3| O4| O5| O6| O7| ...   |
// +---+---+---+---+---+---+---+---+---+---+
// 
// 标记位图(每个对象1位):
// +---+---+---+---+---+---+---+---+
// | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 |  (O0存活, O2存活, O6存活)
// +---+---+---+---+---+---+---+---+

批量标记

复制代码
// gc_mark.c
// 批量处理标记栈,减少函数调用开销
static void
GC_mark_batch (int batch_size)
{
    mark_stack_entry batch[GC_MARK_BATCH_SIZE];
    int count = 0;
    
    // 批量弹出
    while (count < batch_size && 
           !GC_mark_stack_empty()) {
        GC_mark_stack_top--;
        batch[count++] = *GC_mark_stack_top;
    }
    
    // 批量处理
    for (int i = 0; i < count; i++) {
        GC_scan_object_body(batch[i].obj, batch[i].hdr);
    }
}

并行标记

复制代码
// gc_par_mark.c
// Boehm GC支持并行标记(可选)

typedef struct {
    mark_stack_entry *local_stack;
    mark_stack_entry *local_stack_limit;
    mark_stack_entry *local_stack_top;
    
    // 全局共享标记栈(用于工作窃取)
    mark_stack_entry *global_stack;
    mono_mutex_t *global_lock;
    
    int worker_id;
    volatile int *done_flag;
} GC_mark_worker;

static void
GC_mark_parallel (void)
{
    int n_workers = GC_proc_instance.mark_parallelism;
    GC_mark_worker *workers = 
        (GC_mark_worker*)calloc(n_workers, sizeof(GC_mark_worker));
    
    // 初始化工作线程
    for (int i = 0; i < n_workers; i++) {
        workers[i].worker_id = i;
        workers[i].global_stack = GC_mark_stack;
        workers[i].global_lock = &GC_mark_stack_lock;
        workers[i].done_flag = &GC_mark_done;
        
        // 分配本地标记栈
        workers[i].local_stack = 
            (mark_stack_entry*)GC_alloc_os_memory(
                GC_MARK_STACK_INIT_SIZE * sizeof(mark_stack_entry),
                GC_PROT_READ | GC_PROT_WRITE);
        workers[i].local_stack_limit = 
            workers[i].local_stack + GC_MARK_STACK_INIT_SIZE;
        workers[i].local_stack_top = workers[i].local_stack;
    }
    
    // 启动工作线程
    for (int i = 1; i < n_workers; i++) {
        GC_start_mark_thread(&workers[i]);
    }
    
    // 主线程也参与标记
    GC_mark_worker_main(&workers[0]);
    
    // 等待所有工作线程完成
    GC_wait_mark_workers_done();
}

弱引用与Finalizer处理

弱引用处理

复制代码
// gc_mark.c
// 标记完成后处理弱引用
void
GC_mark_finalize (void)
{
    // 1. 处理弱引用
    GC_process_weak_refs();
    
    // 2. 处理finalizer队列
    GC_process_finalize_queue();
}

static void
GC_process_weak_refs (void)
{
    // 遍历所有弱引用
    for (size_t i = 0; i < GC_handle_table_size; i++) {
        if (GC_handle_table[i].type == GC_HANDLE_WEAK ||
            GC_handle_table[i].type == GC_HANDLE_WEAK_TRACK) {
            
            void *obj = GC_handle_table[i].obj;
            if (obj && !GC_is_marked(obj)) {
                // 目标不可达,清空弱引用
                GC_handle_table[i].obj = NULL;
                
                if (GC_handle_table[i].type == GC_HANDLE_WEAK_TRACK) {
                    // 加入复活队列
                    GC_add_to_resurrection_queue(obj);
                }
            }
        }
    }
}

Finalizer处理

复制代码
// gc_mark.c
static void
GC_process_finalize_queue (void)
{
    // 遍历finalizer队列
    GC_finalize_entry *entry = GC_finalize_list;
    GC_finalize_entry *prev = NULL;
    
    while (entry) {
        GC_finalize_entry *next = entry->next;
        
        if (!GC_is_marked(entry->obj)) {
            // 对象不可达,需要finalizer
            // 临时标记对象(使其在本次GC中存活)
            GC_mark_one((ptr_t)entry->obj);
            
            // 移到待执行队列
            GC_add_to_to_finalize(entry->obj, entry->fn);
            
            // 从finalizer队列中移除
            if (prev) prev->next = next;
            else GC_finalize_list = next;
        } else {
            prev = entry;
        }
        
        entry = next;
    }
}

保守标记的误标记分析

伪指针场景

复制代码
// 伪指针示例:
// 栈上有一个整数值 0x7F3A0010
// 恰好堆中有一个对象地址为 0x7F3A0010
// 保守GC会将这个整数误认为对象引用,标记该对象为存活

// 伪代码展示:
void foo() {
    int x = 0x7F3A0010;  // 一个整数值
    // ... 此时GC触发
    // GC扫描栈,发现x的值看起来像堆地址
    // GC标记地址0x7F3A0010处的对象为存活
    // 即使该对象实际上不可达
}

误标记的影响

复制代码
// 误标记导致的问题:
// 1. 内存泄漏:不可达对象被误标记为存活,无法回收
// 2. 级联效应:被误标记对象的引用也被标记
//    导致更多对象无法回收
// 3. 不可预测性:误标记取决于运行时的栈/寄存器状态
//    同一程序在不同运行中可能有不同的GC行为

// 缓解措施:
// 1. 黑名单机制:记录可疑地址,避免在这些地址分配对象
// 2. 对齐检查:只将对齐到对象边界的地址视为有效引用
// 3. 堆块隔离:不同类型的对象分配在不同区域
//    减少伪指针命中的概率

总结

复制代码
Boehm GC标记阶段流程:

GC_mark_roots()
    |
    +-- GC_clear_marks_all()           // 清除所有标记位
    |
    +-- GC_push_roots()                // 推送根集合
    |     |-- GC_push_all_thread_stacks()  // 扫描线程栈+寄存器
    |     |-- GC_push_roots_static()       // 扫描静态数据区
    |     |-- GC_push_roots_custom()       // 扫描自定义根
    |     +-- GC_push_roots_handles()      // 扫描GC Handle
    |
    +-- while (!mark_stack_empty)       // 处理标记栈
    |     +-- GC_scan_object_body()     // 保守扫描对象体
    |           +-- 遍历每个word
    |           +-- GC_is_heap_ptr()    // 检查是否指向堆
    |           +-- GC_mark_one()       // 标记对象+压栈
    |
    +-- GC_mark_finalize()              // 处理弱引用和finalizer
          |-- GC_process_weak_refs()    // 清空指向不可达对象的弱引用
          +-- GC_process_finalize_queue() // 临时标记需finalizer的对象
特性 保守标记 精确标记(SGen)
类型信息 不需要 需要描述符
扫描开销 高(扫描所有word) 低(只扫描引用字段)
误标记风险 有(伪指针) 无
对象移动 不支持 支持
适用场景 C/C++混合 纯托管代码

Boehm GC的保守标记策略是其最大的设计决策,既带来了通用性优势,也引入了性能和正确性方面的挑战。下一章将分析分配与碎片管理。

相关推荐
信可维1 小时前
AI 读 Word 表格总读错?先查合并单元格和表头这 3 处
开发语言·人工智能·c#·word·软件工程
Code_Solitude1 小时前
传智杯备赛代码整理(前4题)
数据结构·算法
Jasmine_llq2 小时前
《P8816 [CSP-J 2022] 上升点列》
数据结构·算法·贪心·排序·二维动态规划 dp·类背包状态转移
hetao17338372 小时前
2026-10-19 hetao1733837 的刷题记录
c++·算法
辛苦才能2 小时前
C++ 哈希表深度解析:删除为什么必须打标记,负载因子为什么卡 0.7
开发语言·数据结构·c++·算法
m0_639906942 小时前
HashMap:哈希扰动、扩容源码、红黑树转换、并发问题
java·算法·哈希算法
半摆烂日常3 小时前
手机表单的左滑操作:触摸事件的处理细节
大数据·开发语言·人工智能·智能手机·https·c#
开开心心就好3 小时前
白天晚上自动调蓝光,护眼工具亮度可调
服务器·开发语言·算法·pdf·ocr·intellij-idea·idea
绛橘色的日落(。・∀・)ノ4 小时前
C#学习笔记
笔记·学习·c#