一、垃圾回收基础原理
1.1 托管堆内存结构
C#采用分代式垃圾回收器(Generational GC),将托管堆划分为三个主要区域:
- 第0代:新创建的小对象(0-85KB)
- 回收频率最高(约每100ms检查一次)
- 占用空间约256KB-4MB(CLR自动调整)
- 第1代:从第0代晋升的对象
- 缓冲区作用,约2MB-16MB
- 回收频率中等
- 第2代:长期存活对象
- 包含大型对象堆(LOH,>85KB)
- 回收频率最低(可能几分钟一次)
// 查看对象代龄示例
var obj = new object();
Console.WriteLine(GC.GetGeneration(obj)); // 输出: 0
1.2 GC根对象类型
垃圾回收器从以下GC根开始标记:
- 活动线程栈中的局部变量
- 静态字段
- CPU寄存器中的引用
- 终结队列中的对象
- 互操作中的COM对象
二、垃圾回收触发机制
2.1 自动触发条件
触发条件 | 影响范围 | 典型场景 |
---|---|---|
第0代满 | 仅回收第0代 | 频繁创建小对象 |
第1代满 | 回收第0+1代 | 中等数量对象存活 |
第2代满/系统内存不足 | 完全回收(0+1+2代) | 大内存分配或系统压力大 |
AppDomain卸载 | 完全回收 | 应用程序域卸载 |
2.2 手动控制API
// 强制垃圾回收(谨慎使用)
GC.Collect(int generation, GCCollectionMode mode, bool blocking);
// 回收模式选项
public enum GCCollectionMode
{
Default, // 最佳平衡
Forced, // 立即执行
Optimized // 由CLR决定最佳时机
}
三、标记-压缩算法详解
3.1 三阶段工作流程
- 标记阶段:
// 伪代码表示标记过程
void MarkFromRoots()
{
foreach (var root in GC Roots)
{
Mark(root);
}
}
- 清除阶段:
- 更新空闲列表(LOH)
- 记录未标记的内存块
- 压缩阶段(仅限第0/1代):
// 移动对象后的引用更新
void UpdateReferences()
{
foreach (var ref in ObjectReferences)
{
if (ref.IsMarked)
ref.Address = new_location;
}
}
3.2 各代回收特点
特性 | 第0代 | 第1代 | 第2代 |
---|---|---|---|
压缩方式 | 完全压缩 | 完全压缩 | 空闲列表 |
暂停时间 | 短(1-5ms) | 中(5-20ms) | 长(20-100ms) |
回收频率 | 高 | 中 | 低 |
四、大型对象堆(LOH)管理
4.1 LOH特殊机制
- 分配阈值:默认85KB
- 不压缩(.NET 4.5.1+可手动压缩)
- 空闲列表管理:
// 检查LOH碎片化程度
var lohFrag = GC.GetLOHFragmentation();
if (lohFrag > 0.75)
{
GCSettings.LargeObjectHeapCompactionMode =
GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2);
}
4.2 LOH优化策略
- 避免频繁分配/释放大对象
- 使用对象池重用大对象
- 考虑使用
ArrayPool<T>
共享数组
五、终结器与Dispose模式
5.1 终结器队列机制
graph LR
A[对象被标记为垃圾] --> B{是否有终结器?}
B -->|是| C[加入终结队列]
B -->|否| D[直接回收]
C --> E[Finalizer线程调用终结器]
E --> F[从队列移除]
F --> G[下次GC时回收]
5.2 标准Dispose模式实现
class Resource : IDisposable
{
private bool _disposed = false;
~Resource() => Dispose(false);
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing) {
// 释放托管资源
}
// 释放非托管资源
CloseHandle(_handle);
_disposed = true;
}
}
六、GC工作模式配置
6.1 服务器GC vs 工作站GC
特性 | 工作站模式 | 服务器模式 |
---|---|---|
线程数量 | 1个专用GC线程 | 每个CPU核心1个GC线程 |
内存分配 | 共享堆 | 每个线程独立堆 |
延迟敏感性 | 优化UI响应 | 优化吞吐量 |
适用场景 | 客户端应用 | 服务端高并发 |
配置方法(App.config):
<configuration>
<runtime>
<gcServer enabled="true"/>
<gcConcurrent enabled="false"/>
</runtime>
</configuration>
6.2 并发GC机制
- 后台GC(.NET 4.0+):第2代回收时不阻塞应用线程
- 分片式回收:将堆分成多个区域并行回收
七、性能优化实践
7.1 减少GC压力技巧
- 对象复用:
// 使用对象池代替频繁new
var pool = new ObjectPool<MyClass>(() => new MyClass());
var obj = pool.Get();
try { /* 使用对象 */ }
finally { pool.Return(obj); }
- 值类型优化:
// 使用struct替代小class
readonly struct Point { public int X, Y; }
- 集合预分配:
var list = new List<int>(capacity: 1000);
7.2 诊断工具使用
- PerfView:分析GC暂停时间和频率
PerfView /GCCollectOnly collect
- ETW事件:
GC.RegisterForFullGCNotification(10, 10);
Task.Run(() =>
{
while (true)
{
GCNotificationStatus status = GC.WaitForFullGCApproach();
if (status == GCNotificationStatus.Succeeded)
OnGCApproaching();
}
});
八、.NET各版本GC改进
版本 | 重要改进 | 影响 |
---|---|---|
4.0 | 后台GC | 减少第2代GC停顿 |
4.5 | LOH压缩支持 | 减少大对象堆碎片 |
4.6 | 更积极的第2代回收 | 降低内存占用峰值 |
Core 2.1 | 可配置的GC模式 | 更好的容器支持 |
5.0 | 可回收的GC句柄 | 减少非托管内存泄漏风险 |
6.0 | PGO引导的分配优化 | 智能对象布局 |
九、实战问题排查
9.1 内存泄漏诊断步骤
- 使用
dotnet-dump
收集内存快照
dotnet-dump collect -p <pid>
- 分析对象根引用链
!dumpheap -stat
!gcroot <object_address>
- 检查非托管资源
!finalizequeue
9.2 高GC暂停优化
- 切换到服务器GC模式
- 减少第2代对象分配
- 使用
ArrayPool
或MemoryPool
- 考虑使用
System.Buffers
中的类型
十、特殊场景处理
10.1 非托管内存管理
unsafe struct NativeBuffer
{
public byte* Pointer;
public int Length;
public NativeBuffer(int size)
{
Pointer = (byte*)NativeMemory.Alloc((nuint)size);
Length = size;
}
public void Free()
{
NativeMemory.Free(Pointer);
Pointer = null;
Length = 0;
}
}
10.2 实时系统优化
// 禁用并发GC确保可预测性
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
try
{
// 执行关键代码
}
finally
{
GCSettings.LatencyMode = GCLatencyMode.Interactive;
}
理解C#垃圾回收机制是编写高性能应用的基础。通过合理运用分代特性、选择适当GC模式、优化对象生命周期管理,可以显著提升应用程序性能。记住:最好的GC优化就是减少分配。