.NET CLR 内存结构与垃圾回收机制

.NET CLR 内存结构与垃圾回收机制详解

一、CLR 内存结构概述

1.1 托管堆(Managed Heap)整体架构

.NET CLR 使用分代垃圾收集器(Generational Garbage Collector)来管理托管内存,基于以下核心假设:

  • 年轻对象更容易死亡:新分配的对象通常很快就不再被引用
  • 老对象更可能存活:存活时间长的对象通常会继续存活
  • 相关对象通常一起死亡:同时分配的对象往往同时变得不可达

1.2 堆的整体布局

// 来源:gc.h
enum gc_generation_num
{
    // 小对象堆包含0-2代
    soh_gen0 = 0,           // 第0代(最年轻)
    soh_gen1 = 1,           // 第1代
    soh_gen2 = 2,           // 第2代(最老)
    max_generation = soh_gen2,

    // 大对象堆(技术上不是代,但方便表示)
    loh_generation = 3,      // 大对象堆 (LOH)

    // 固定对象堆
    poh_generation = 4,      // 固定对象堆 (POH)

    uoh_start_generation = loh_generation,
    ephemeral_generation_count = max_generation,
    total_generation_count = poh_generation + 1,
    uoh_generation_count = total_generation_count - uoh_start_generation
};

二、内存分区详解

2.1 小对象堆(SOH - Small Object Heap)

定义:存储小于85KB的对象

分代结构

// 来源:gcpriv.h - generation 类
class generation
{
public:
    alloc_context   allocation_context;    // 分配上下文
    PTR_heap_segment start_segment;        // 起始段
    heap_segment*   allocation_segment;    // 当前分配段
    heap_segment*   tail_region;          // 尾部区域
    allocator       free_list_allocator;  // 空闲列表分配器
    
    // 统计信息
    size_t          free_list_allocated;   // 空闲列表已分配
    size_t          end_seg_allocated;     // 段尾已分配
    size_t          condemned_allocated;   // 被回收的已分配
    size_t          sweep_allocated;       // 清扫已分配
    
    size_t          free_list_space;       // 空闲列表空间
    size_t          free_obj_space;        // 空闲对象空间
};

第0代(Gen 0)

  • 特点:最年轻的对象,分配频率最高
  • 大小:通常为256KB - 4MB
  • GC频率:最频繁,性能影响最小
  • 分配策略:线性分配,简单快速

第1代(Gen 1)

  • 特点:Gen 0 GC后存活的对象
  • 大小:通常为512KB - 4MB
  • 作用:作为Gen 0和Gen 2之间的缓冲
  • 优化:减少对Gen 2的压力

第2代(Gen 2)

  • 特点:长期存活的对象
  • 大小:可以非常大,受系统内存限制
  • GC成本:最昂贵,会暂停所有线程
  • 包含:可能包含LOH和POH对象

2.2 大对象堆(LOH - Large Object Heap)

// 来源:gc.h
#define LARGE_OBJECT_SIZE ((size_t)(85000))  // 85KB阈值

特征

  • 对象大小:≥ 85KB
  • 不压缩:避免移动大对象的高昂成本
  • 直接进入Gen 2:跳过Gen 0和Gen 1
  • 分配策略:使用空闲列表,可能产生碎片

典型使用场景

  • 大数组(如大型图像数据)
  • 大字符串
  • 大型缓存对象

2.3 固定对象堆(POH - Pinned Object Heap)

.NET 5.0+ 新增特性

  • 专门存储:被固定(pinned)的对象
  • 避免碎片:将固定对象从SOH中分离
  • 提高性能:减少对SOH压缩的影响
  • 互操作优化:方便与非托管代码交互

三、堆段(Heap Segment)结构

3.1 段的基本结构

// 来源:gcpriv.h
class heap_segment
{
public:
    uint8_t*        allocated;     // 已分配区域的结束位置
    uint8_t*        committed;     // 已提交的内存结束位置
    uint8_t*        reserved;      // 保留的内存结束位置
    uint8_t*        used;          // 实际使用的内存结束位置
    uint8_t*        mem;           // 段的起始位置
    size_t          flags;         // 标志位
    PTR_heap_segment next;         // 下一个段的指针
    
#ifdef MULTIPLE_HEAPS
    gc_heap*        heap;          // 所属的堆
#endif //MULTIPLE_HEAPS

    // 后台GC相关
    uint8_t*        background_allocated;
    uint8_t*        plan_allocated;
    uint8_t*        saved_allocated;
    uint8_t*        saved_bg_allocated;
    
#ifdef USE_REGIONS
    size_t          survived;      // 存活对象大小
    uint8_t         gen_num;       // 所属代数
    bool            swept_in_plan_p;  // 是否在计划阶段清扫
    int             plan_gen_num;   // 计划代数
    // ... 更多区域相关字段
#endif //USE_REGIONS
};

3.2 内存管理层次

Virtual Memory (虚拟内存)
    ↓
Reserved Memory (保留内存) - VirtualAlloc(MEM_RESERVE)
    ↓
Committed Memory (提交内存) - VirtualAlloc(MEM_COMMIT)
    ↓
Used Memory (使用内存) - 实际存储对象

四、对象布局结构

4.1 对象头(Object Header)

// 来源:object.h
#ifdef TARGET_64BIT
#define OBJHEADER_SIZE  (sizeof(DWORD) /* m_alignpad */ + sizeof(DWORD) /* m_SyncBlockValue */)
#else
#define OBJHEADER_SIZE  sizeof(DWORD) /* m_SyncBlockValue */
#endif

#define OBJECT_SIZE     TARGET_POINTER_SIZE /* m_pMethTab */
#define OBJECT_BASESIZE (OBJHEADER_SIZE + OBJECT_SIZE)

对象内存布局

64位系统:
┌─────────────────┬─────────────────┬─────────────────┐
│   对齐填充      │   同步块索引    │   方法表指针    │
│   4 bytes       │   4 bytes       │   8 bytes       │
├─────────────────┼─────────────────┼─────────────────┤
│                 对象数据区域                      │
└─────────────────────────────────────────────────────┘

32位系统:
┌─────────────────┬─────────────────┐
│   同步块索引    │   方法表指针    │
│   4 bytes       │   4 bytes       │
├─────────────────┼─────────────────┤
│          对象数据区域            │
└─────────────────────────────────────┘

4.2 数组对象布局

// 来源:object.h
#ifdef TARGET_64BIT
#define ARRAYBASE_SIZE  (OBJECT_SIZE /* m_pMethTab */ + sizeof(DWORD) /* m_NumComponents */ + sizeof(DWORD) /* pad */)
#else
#define ARRAYBASE_SIZE  (OBJECT_SIZE /* m_pMethTab */ + sizeof(DWORD) /* m_NumComponents */)
#endif

数组内存布局

64位数组:
┌─────────┬─────────┬─────────┬─────────┬─────────────────┐
│ 对象头  │ 方法表  │ 长度    │ 填充    │    数组元素     │
│ 8bytes  │ 8bytes  │ 4bytes  │ 4bytes  │    可变长度     │
└─────────┴─────────┴─────────┴─────────┴─────────────────┘

五、垃圾回收算法

5.1 分代回收策略

回收触发条件

// 来源:gc.h
enum gc_reason
{
    reason_alloc_soh = 0,          // SOH分配压力
    reason_induced = 1,            // 手动触发
    reason_lowmemory = 2,          // 内存不足
    reason_empty = 3,              // 空堆
    reason_alloc_loh = 4,          // LOH分配压力
    reason_oos_soh = 5,            // SOH空间不足
    reason_oos_loh = 6,            // LOH空间不足
    reason_induced_noforce = 7,     // 非强制手动触发
    reason_gcstress = 8,           // GC压力测试
    reason_lowmemory_blocking = 9,  // 阻塞式低内存
    reason_induced_compacting = 10, // 压缩式手动触发
    reason_lowmemory_host = 11,    // 宿主低内存
    reason_pm_full_gc = 12,        // 临时模式完整GC
    reason_lowmemory_host_blocking = 13, // 宿主阻塞低内存
    reason_bgc_tuning_soh = 14,    // 后台GC调优SOH
    reason_bgc_tuning_loh = 15,    // 后台GC调优LOH
    reason_bgc_stepping = 16,      // 后台GC步进
    reason_induced_aggressive = 17, // 激进式手动触发
    reason_max
};

5.2 标记清扫算法

三色标记法

// 标记阶段的核心逻辑(伪代码)
void mark_through_cards_for_segments(mark_object_fn mark_fn, BOOL relocating)
{
    // 遍历所有段
    for (heap_segment* seg = generation_start_segment(generation_of(condemned_gen));
         seg != NULL; seg = heap_segment_next(seg))
    {
        // 遍历卡表,标记跨代引用
        mark_through_cards_helper(seg, mark_fn, relocating);
    }
}

标记过程

  1. 白色:未访问的对象(默认状态)
  2. 灰色:已发现但子对象未完全扫描的对象
  3. 黑色:已完全扫描的对象

根对象扫描

  • 栈引用:线程栈上的对象引用
  • 静态变量:类的静态字段
  • 全局变量:应用程序域的全局引用
  • 句柄表:GC句柄、弱引用等
  • JIT编译器根:寄存器中的引用

5.3 压缩算法

压缩的必要性

  • 消除碎片:回收后的内存碎片化
  • 提高局部性:相关对象聚集在一起
  • 简化分配:线性分配器效率更高

压缩过程

  1. 计划阶段:确定对象的新位置
  2. 重定位阶段:更新所有引用
  3. 压缩阶段:实际移动对象
// 重定位引用的核心逻辑
void relocate_address(uint8_t** object_ptr)
{
    uint8_t* old_address = *object_ptr;
    if (gc_heap::reloc && (old_address != 0))
    {
        uint8_t* new_address = gc_heap::relocate_obj(old_address);
        *object_ptr = new_address;
    }
}

六、并发与后台GC

6.1 工作站GC(Workstation GC)

特点

  • 单线程:使用一个GC线程
  • 适用场景:桌面应用、小型服务
  • 延迟优化:专注于减少GC暂停时间
  • 内存开销:较小的内存占用

6.2 服务器GC(Server GC)

特点

  • 多线程:每个CPU核心一个GC线程
  • 适用场景:服务器应用、高吞吐量场景
  • 吞吐量优化:专注于整体性能
  • 内存开销:更大的堆大小和内存占用
// 多堆结构(多线程服务器GC)
#ifdef MULTIPLE_HEAPS
class gc_heap
{
    // 每个逻辑处理器一个堆实例
    static gc_heap* g_heaps[MAX_SUPPORTED_CPUS];
    static int n_heaps;
    
    // 堆间负载均衡
    void balance_heaps();
    void distribute_free_regions();
};
#endif //MULTIPLE_HEAPS

6.3 后台GC(Background GC)

.NET 4.0+ 引入

  • 并发执行:GC在后台运行,应用继续执行
  • 减少暂停:只在特定阶段暂停应用
  • 适用范围:主要用于Gen 2收集

后台GC状态机

// 来源:gc.h
enum bgc_state
{
    bgc_not_in_process = 0,    // 未进行中
    bgc_initialized,           // 已初始化
    bgc_reset_ww,              // 重置写屏障
    bgc_mark_handles,          // 标记句柄
    bgc_mark_stack,            // 标记栈
    bgc_revisit_soh,           // 重访SOH
    bgc_revisit_uoh,           // 重访UOH
    bgc_overflow_soh,          // SOH溢出处理
    bgc_overflow_uoh,          // UOH溢出处理
    bgc_final_marking,         // 最终标记
    bgc_sweep_soh,             // 清扫SOH
    bgc_sweep_uoh,             // 清扫UOH
    bgc_plan_phase,            // 计划阶段
    bgc_relocate_phase,        // 重定位阶段
    bgc_1st_sweep,             // 第一次清扫
    bgc_2nd_sweep,             // 第二次清扫
    bgc_final_sweep,           // 最终清扫
    bgc_completed              // 完成
};

七、卡表(Card Table)机制

7.1 卡表的作用

解决问题:避免在年轻代GC时扫描整个老年代

基本原理

  • 内存分块:将堆内存分为512字节的"卡片"
  • 标记修改:当老年代对象引用年轻代对象时标记对应卡片
  • 选择性扫描:GC时只扫描被标记的卡片
// 卡表相关的全局变量
extern "C" uint32_t* g_gc_card_table;      // 卡表
extern "C" uint8_t*  g_gc_lowest_address;  // 最低地址
extern "C" uint8_t*  g_gc_highest_address; // 最高地址

// 卡片大小定义
#define card_size 512
#define card_word_width ((size_t)card_size*sizeof(uint32_t))

7.2 写屏障(Write Barrier)

目的:在对象引用更新时自动维护卡表

// 写屏障的基本形式(伪代码)
void WriteBarrier(Object** dst, Object* ref)
{
    *dst = ref;  // 执行实际的写入
    
    // 如果目标在老年代,新值在年轻代
    if (IsInOldGeneration(dst) && IsInYoungGeneration(ref))
    {
        // 标记对应的卡片
        MarkCard(GetCardForAddress(dst));
    }
}

八、内存分配策略

8.1 快速分配路径

线程本地分配缓冲区(TLAB)

// 分配上下文结构
struct alloc_context
{
    uint8_t* alloc_ptr;        // 当前分配指针
    uint8_t* alloc_limit;      // 分配限制
    int64_t  alloc_bytes;      // 已分配字节数
    uint8_t* alloc_bytes_uoh;  // UOH已分配字节数
};

分配过程

  1. 快速路径:在TLAB中线性分配
  2. 慢速路径:TLAB耗尽时申请新的TLAB
  3. GC触发:无法分配时触发垃圾回收

8.2 大对象分配

空闲列表分配器

class allocator
{
    // 分桶的空闲列表
    uint8_t* freelists[num_buckets];
    
    // 分配策略
    uint8_t* allocate(size_t size)
    {
        int bucket = size_to_bucket(size);
        return allocate_from_bucket(bucket, size);
    }
};

九、性能调优与监控

9.1 GC性能指标

关键指标

  • GC暂停时间:应用暂停的时长
  • GC吞吐量:GC开销占总时间的比例
  • 内存使用效率:堆利用率和碎片化程度
  • 分配速率:单位时间内的分配量

9.2 调优参数

重要配置

<configuration>
  <runtime>
    <!-- 启用服务器GC -->
    <gcServer enabled="true"/>
    
    <!-- 启用并发GC -->
    <gcConcurrent enabled="true"/>
    
    <!-- 保留段数量 -->
    <GCRetainVM enabled="true"/>
    
    <!-- 大对象堆压缩 -->
    <GCLOHThreshold enabled="true"/>
  </runtime>
</configuration>

9.3 内存泄漏诊断

常见原因

  • 静态集合:不断增长的静态集合
  • 事件订阅:未取消的事件订阅
  • 非托管资源:未释放的非托管资源
  • 大对象:长期持有的大对象引用

十、总结

10.1 CLR内存管理优势

  1. 自动管理:开发者无需手动内存管理
  2. 分代优化:基于对象生命周期的优化策略
  3. 并发执行:后台GC减少应用暂停
  4. 碎片控制:压缩算法减少内存碎片
  5. 跨代优化:卡表机制优化跨代引用扫描

10.2 设计权衡

  1. 暂停时间 vs 吞吐量:工作站GC vs 服务器GC
  2. 内存开销 vs 性能:元数据和辅助结构的开销
  3. 复杂性 vs 效率:复杂的算法带来更好的性能
  4. 可预测性 vs 自适应:固定策略vs动态调整

.NET CLR的内存管理系统是一个高度优化的、复杂的系统,它在保证内存安全的同时,通过分代回收、后台GC、卡表优化等技术提供了优秀的性能表现。理解这些机制有助于编写更高效的.NET应用程序。

posted @ 2025-08-18 14:50  MadLongTom  阅读(28)  评论(0)    收藏  举报