.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);
}
}
标记过程:
- 白色:未访问的对象(默认状态)
- 灰色:已发现但子对象未完全扫描的对象
- 黑色:已完全扫描的对象
根对象扫描
- 栈引用:线程栈上的对象引用
- 静态变量:类的静态字段
- 全局变量:应用程序域的全局引用
- 句柄表:GC句柄、弱引用等
- JIT编译器根:寄存器中的引用
5.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已分配字节数
};
分配过程:
- 快速路径:在TLAB中线性分配
- 慢速路径:TLAB耗尽时申请新的TLAB
- 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内存管理优势
- 自动管理:开发者无需手动内存管理
- 分代优化:基于对象生命周期的优化策略
- 并发执行:后台GC减少应用暂停
- 碎片控制:压缩算法减少内存碎片
- 跨代优化:卡表机制优化跨代引用扫描
10.2 设计权衡
- 暂停时间 vs 吞吐量:工作站GC vs 服务器GC
- 内存开销 vs 性能:元数据和辅助结构的开销
- 复杂性 vs 效率:复杂的算法带来更好的性能
- 可预测性 vs 自适应:固定策略vs动态调整
.NET CLR的内存管理系统是一个高度优化的、复杂的系统,它在保证内存安全的同时,通过分代回收、后台GC、卡表优化等技术提供了优秀的性能表现。理解这些机制有助于编写更高效的.NET应用程序。
本文来自博客园,作者:MadLongTom,转载请注明原文链接:https://www.cnblogs.com/madtom/p/19044682
浙公网安备 33010602011771号