.NET 内存泄漏诊断与预防
大约 16 分钟约 4728 字
.NET 内存泄漏诊断与预防
简介
.NET 的垃圾回收器(GC)自动管理内存分配和释放,但这并不意味着 .NET 应用不会发生内存泄漏。内存泄漏在 .NET 中通常表现为:对象的引用被意外保持,导致 GC 无法回收它们。随着时间推移,内存占用持续增长,最终导致OutOfMemoryException、GC 暂停时间增长、应用响应变慢。
理解内存泄漏的模式、掌握诊断工具、建立预防性的编码习惯,是 .NET 开发者的必备技能。本文将从常见泄漏模式到生产环境诊断,全面覆盖 .NET 内存泄漏的诊断与预防。
特点
- 隐蔽性:内存泄漏通常不会立即暴露,在长时间运行后才显现
- 累积性:每次泄漏的量可能很小,但会随时间累积
- 难定位:泄漏的根因可能与代码表面逻辑无关
- 环境相关:某些泄漏只在特定负载或配置下出现
- 可预防:通过规范编码和工具检测,大部分泄漏可以预防
常见内存泄漏模式
1. 事件处理器泄漏
事件处理器是最常见的内存泄漏来源。订阅事件会创建从发布者到订阅者的引用,如果忘记取消订阅,订阅者对象就无法被 GC 回收。
// 泄漏示例:忘记取消事件订阅
public class OrderService
{
public event EventHandler<OrderEventArgs>? OrderCreated;
public void CreateOrder(Order order)
{
// 处理订单逻辑...
OrderCreated?.Invoke(this, new OrderEventArgs(order));
}
}
public class OrderDashboard // 这个类会泄漏!
{
private readonly OrderService _orderService;
public OrderDashboard(OrderService orderService)
{
_orderService = orderService;
// 订阅事件 - 创建了 OrderService -> OrderDashboard 的引用
_orderService.OrderCreated += OnOrderCreated;
// 如果 OrderService 是单例,OrderDashboard 永远不会被回收!
}
private void OnOrderCreated(object? sender, OrderEventArgs e)
{
// 更新仪表盘显示
}
// 问题:没有实现 IDisposable,也没有取消订阅
}// 修复方案1:实现 IDisposable
public class OrderDashboardFixed : IDisposable
{
private readonly OrderService _orderService;
private bool _disposed;
public OrderDashboardFixed(OrderService orderService)
{
_orderService = orderService;
_orderService.OrderCreated += OnOrderCreated;
}
private void OnOrderCreated(object? sender, OrderEventArgs e)
{
// 更新仪表盘显示
}
public void Dispose()
{
if (!_disposed)
{
_orderService.OrderCreated -= OnOrderCreated; // 取消订阅!
_disposed = true;
}
}
}// 修复方案2:使用弱事件模式
public class WeakEventManager
{
private readonly List<WeakReference<Delegate>> _handlers = new();
public void Subscribe<TEventArgs>(
object publisher, string eventName, EventHandler<TEventArgs> handler)
{
// 使用 WeakReference 避免强引用
_handlers.Add(new WeakReference<Delegate>(handler));
// 通过反射订阅(简化示例)
var eventInfo = publisher.GetType().GetEvent(eventName);
eventInfo?.AddEventHandler(publisher, handler);
}
public void Cleanup()
{
_handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
}
}2. 静态引用泄漏
静态字段的生命周期等于整个应用程序域的生命周期,通过静态字段引用的对象永远不会被 GC 回收。
// 泄漏示例:静态集合无限增长
public static class Cache
{
// 静态字典会无限增长!
private static readonly Dictionary<string, object> _cache = new();
public static void Add(string key, object value)
{
_cache[key] = value; // 对象被静态字典引用,永远不会被回收
}
public static object? Get(string key)
{
return _cache.TryGetValue(key, out var value) ? value : null;
}
}// 修复方案:使用 MemoryCache 或 WeakReference
using Microsoft.Extensions.Caching.Memory;
public class SafeCache : IDisposable
{
private readonly MemoryCache _cache = new(new MemoryCacheOptions
{
SizeLimit = 10000 // 限制缓存大小
});
public void Add(string key, object value, TimeSpan? expiration = null)
{
var options = new MemoryCacheEntryOptions()
.SetSize(1)
.SetSlidingExpiration(expiration ?? TimeSpan.FromMinutes(30));
_cache.Set(key, value, options);
}
public object? Get(string key)
{
return _cache.TryGetValue(key, out var value) ? value : null;
}
public void Dispose()
{
_cache.Dispose();
}
}3. 定时器泄漏
// 泄漏示例:System.Timers.Timer 未被正确停止
public class DataRefresher
{
private readonly System.Timers.Timer _timer;
private List<Report> _reports = new();
public DataRefresher()
{
_timer = new System.Timers.Timer(1000);
_timer.Elapsed += OnTimerElapsed;
_timer.Start();
// Timer 持有对 OnTimerElapsed 的引用
// OnTimerElapsed 是实例方法,持有对 this 的引用
// 如果 _timer 不停止,this 对象永远不会被回收
}
private void OnTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
// 每次刷新都创建新的报告对象
_reports.Add(GenerateReport());
// _reports 无限增长!
}
private Report GenerateReport() => new();
}// 修复方案:正确实现 IDisposable
public sealed class DataRefresherFixed : IDisposable
{
private readonly System.Timers.Timer _timer;
private readonly List<Report> _reports = new();
private readonly object _lock = new();
private bool _disposed;
public DataRefresherFixed()
{
_timer = new System.Timers.Timer(1000);
_timer.Elapsed += OnTimerElapsed;
_timer.AutoReset = true;
_timer.Start();
}
private void OnTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
lock (_lock)
{
if (_reports.Count > 100) // 限制列表大小
_reports.RemoveRange(0, _reports.Count - 100);
_reports.Add(GenerateReport());
}
}
private Report GenerateReport() => new();
public void Dispose()
{
if (!_disposed)
{
_timer.Stop(); // 停止定时器
_timer.Elapsed -= OnTimerElapsed; // 取消事件订阅
_timer.Dispose(); // 释放定时器资源
_disposed = true;
}
}
}4. WPF 绑定泄漏
// 泄漏示例:WPF 数据绑定导致的内存泄漏
public class MainWindow : Window
{
// WPF 属性变更通知会创建从控件到 ViewModel 的引用
// 如果不注意,这些引用会阻止 ViewModel 被回收
private ObservableCollection<OrderViewModel> _orders = new();
public MainWindow()
{
InitializeComponent();
// 绑定到集合 - OK
OrderList.ItemsSource = _orders;
// 泄漏:匿名方法捕获了 this
this.DataContextChanged += (s, e) =>
{
// 此匿名方法捕获了 this (MainWindow)
// 如果外部也持有对此事件的引用...
};
}
}
// 修复:使用 WeakReference 或显式取消订阅
public class WeakBindingHelper
{
public static void BindOneWay<T>(
INotifyPropertyChanged source,
string propertyName,
Action<T> updateTarget)
{
var weakRef = new WeakReference<Action<T>>(updateTarget);
source.PropertyChanged += (s, e) =>
{
if (e.PropertyName == propertyName)
{
if (weakRef.TryGetTarget(out var target))
{
var value = (T?)typeof(T).GetProperty(propertyName)?.GetValue(source);
if (value != null)
target(value);
}
}
};
}
}5. CancellationToken 注册泄漏
// 泄漏示例:CancellationToken 回调注册
public class RequestProcessor
{
private readonly List<string> _processedRequests = new();
public void ProcessWithCancellation(string requestId, CancellationToken cancellationToken)
{
// 泄漏:每次注册回调都会创建一个新的 Closure 对象
cancellationToken.Register(() =>
{
// 此闭包捕获了 requestId 和 this
_processedRequests.Add($"Cancelled: {requestId}");
});
}
}// 修复:管理 CancellationToken 注册
public class RequestProcessorFixed
{
private readonly ConcurrentDictionary<string, CancellationTokenRegistration> _registrations = new();
public void ProcessWithCancellation(string requestId, CancellationToken cancellationToken)
{
var state = (requestId, this);
var registration = cancellationToken.Register(
static state =>
{
var (reqId, self) = state!;
// 使用静态 lambda 避免闭包
}, state);
_registrations[requestId] = registration;
}
public void CompleteRequest(string requestId)
{
if (_registrations.TryRemove(requestId, out var registration))
{
registration.Dispose(); // 释放注册!
}
}
}诊断工具
dotMemory
# JetBrains dotMemory 使用
# 1. 启动应用并附加 dotMemory
dotmemory launch MyApplication.exe
# 2. 命令行模式抓取快照
dotmemory snap MyApplication.exe --save-to=snapshot.dmwp
# 3. 对比两个快照
dotmemory compare snapshot1.dmwp snapshot2.dmwpdotMemory 分析步骤:
1. 获取快照
├── 在应用启动时获取基准快照
├── 模拟负载后获取对比快照
└── 等待 GC 后获取最终快照
2. 对比分析
├── 选择 "Compare Snapshots"
├── 查看新增对象(New Objects)
├── 查看存活对象(Surviving Objects)
└── 按类型分组排序
3. 查找 GC Root
├── 选择疑似泄漏的对象类型
├── 查看 "Retention Path"
├── 找到阻止回收的引用链
└── 定位到具体代码位置
4. 常见泄漏特征
├── 某类型对象数量持续增长
├── 字节数组占用量异常
├── String 对象数量异常
├── 事件处理器委托数量多
└── Thread/Task 对象累积dotTrace
# JetBrains dotTrace 性能分析
dottrace launch MyApplication.exe
# 分析 GC 暂停时间
dottrace inspect snapshot.dtt --filter=GCPerfView
# PerfView(免费,微软出品)
# 1. 收集 GC 数据
PerfView.exe /GCCollectOnly collect
# 2. 收集完整 ETW 跟踪
PerfView.exe collect /MaxCollectSec:300
# 3. 分析内存
PerfView.exe MemoryAnalysis snapshot.etl
# 查看 GC 统计
# 在 PerfView 中打开 GCStats 视图
# 关注:
# - Gen 2 GC 频率(太高说明有长寿命对象问题)
# - LOH 分配(大对象堆碎片化)
# - GC 暂停时间(影响延迟)
# - Promotion Rate(晋升率,越大说明泄漏越严重)dotnet-dump
# .NET CLI 诊断工具
# 安装工具
dotnet tool install --global dotnet-dump
dotnet tool install --global dotnet-gcdump
dotnet tool install --global dotnet-counters
# 查看进程列表
dotnet-dump ps
# 收集内存 dump
dotnet-dump collect -p <pid>
dotnet-dump collect -p <pid> --type Full # 完整 dump
dotnet-dump collect -p <pid> --type Heap # 仅托管堆
# 分析 dump
dotnet-dump analyze core_20240115.dmp
# SOS 命令
> dumpheap -stat # 按类型统计堆上的对象
> dumpheap -mt <MT> # 查看指定类型的所有实例
> gcroot <address> # 查找对象的 GC Root
> dumpobj <address> # 查看对象的详细信息
> clrstack # 查看托管调用栈
> clrthreads # 查看托管线程
> eeheap -gc # 查看 GC 堆信息
> gcheapstat # GC 堆统计
# GC Dump(更轻量)
dotnet-gcdump collect -p <pid>
dotnet-gcdump report gcdump.dmpdotnet-counters 实时监控
# 实时监控 .NET 运行时指标
dotnet-counters monitor -p <pid>
# 关注的计数器:
# Runtime counters:
# gc-heap-size - GC 堆总大小
# gen-0-gc-count - Gen 0 GC 次数
# gen-1-gc-count - Gen 1 GC 次数
# gen-2-gc-count - Gen 2 GC 次数(关注!)
# gen-0-size - Gen 0 大小
# gen-1-size - Gen 1 大小
# gen-2-size - Gen 2 大小(持续增长=泄漏)
# loh-size - 大对象堆大小
# alloc-rate - 分配速率
# gc-fragmentation - GC 碎片率
# 自定义计数器
dotnet-counters monitor -p <pid> \
--counters System.Runtime[gc-heap-size,gen-2-size,loh-size]GC 压力分析
// GC 压力监控工具
public class GCMonitor
{
private readonly ILogger _logger;
private Timer? _monitorTimer;
private long _lastGen2Count;
private long _lastLOHSize;
public GCMonitor(ILogger<GCMonitor> logger)
{
_logger = logger;
}
public void StartMonitoring(TimeSpan interval)
{
_monitorTimer = new Timer(_ => CheckGCPressure(), null,
interval, interval);
}
private void CheckGCPressure()
{
var gcInfo = GC.GetGCMemoryInfo();
long gen2Collections = GC.CollectionCount(2);
long totalMemory = GC.GetTotalMemory(false);
long lohSize = gcInfo.GenerationInfo[3].SizeAfterBytes; // LOH
long heapSize = gcInfo.TotalCommittedBytes;
double fragmentation = gcInfo.FragmentationBytes > 0
? (double)gcInfo.FragmentationBytes / heapSize * 100
: 0;
// 检测 Gen 2 GC 频率异常
long gen2Delta = gen2Collections - _lastGen2Count;
if (gen2Delta > 5) // 每10秒超过5次 Gen2 GC
{
_logger.LogWarning(
"Gen2 GC 频率异常: {Count} 次在最近间隔内",
gen2Delta);
}
// 检测内存持续增长
_logger.LogInformation(
"GC 监控: 总内存={TotalMB:F1}MB, Gen2大小={Gen2MB:F1}MB, " +
"LOH={LohMB:F1}MB, 碎片率={Frag:F1}%",
totalMemory / 1024.0 / 1024.0,
gcInfo.GenerationInfo[2].SizeAfterBytes / 1024.0 / 1024.0,
lohSize / 1024.0 / 1024.0,
fragmentation);
_lastGen2Count = gen2Collections;
}
public void StopMonitoring()
{
_monitorTimer?.Dispose();
}
}// 使用 dotnet-counters API 自定义监控
public class CustomMetrics
{
private readonly Meter _meter;
private readonly Counter<long> _gcCollections;
private readonly Histogram<double> _gcPauseDuration;
public CustomMetrics(IMeterFactory meterFactory)
{
_meter = meterFactory.Create("MyApp.GC");
_gcCollections = _meter.CreateCounter<long>(
"myapp.gc.collections",
description: "GC 回收次数");
_gcPauseDuration = _meter.CreateHistogram<double>(
"myapp.gc.pause.duration",
unit: "ms",
description: "GC 暂停时间");
}
public void RecordGCInfo()
{
for (int gen = 0; gen <= 2; gen++)
{
_gcCollections.Add(
GC.CollectionCount(gen),
new KeyValuePair<string, object?>("generation", gen));
}
}
}IDisposable 模式
标准 Dispose 模式
/// <summary>
/// 标准 IDisposable 实现模式
/// </summary>
public class ResourceManager : IDisposable
{
private readonly object _lock = new();
private bool _disposed;
// 托管资源
private readonly HttpClient _httpClient = new();
private readonly CancellationTokenSource _cts = new();
private readonly Timer _timer;
// 非托管资源(罕见)
private IntPtr _unmanagedBuffer;
private bool _ownsUnmanagedBuffer;
public ResourceManager()
{
_timer = new Timer(OnTimer, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));
_unmanagedBuffer = Marshal.AllocHGlobal(4096);
_ownsUnmanagedBuffer = true;
}
public void DoWork()
{
ObjectDisposedException.ThrowIf(_disposed, this);
// 业务逻辑...
}
private void OnTimer(object? state)
{
ObjectDisposedException.ThrowIf(_disposed, this);
// 定时任务...
}
// 公共 Dispose 方法
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// 受保护的 Dispose 方法
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
lock (_lock)
{
if (_disposed) return;
if (disposing)
{
// 释放托管资源
_timer.Dispose();
_cts.Cancel();
_cts.Dispose();
_httpClient.Dispose();
}
// 释放非托管资源
if (_ownsUnmanagedBuffer && _unmanagedBuffer != IntPtr.Zero)
{
Marshal.FreeHGlobal(_unmanagedBuffer);
_unmanagedBuffer = IntPtr.Zero;
_ownsUnmanagedBuffer = false;
}
_disposed = true;
}
}
// 析构函数(仅当有非托管资源时才需要)
~ResourceManager()
{
Dispose(false);
}
}异步 Dispose 模式
/// <summary>
/// IAsyncDisposable 实现
/// </summary>
public class AsyncResourceManager : IAsyncDisposable, IDisposable
{
private readonly SqlConnection _connection;
private readonly StreamReader _reader;
private bool _disposed;
public AsyncResourceManager(string connectionString)
{
_connection = new SqlConnection(connectionString);
_reader = new StreamReader("data.log");
}
public async Task InitializeAsync()
{
ObjectDisposedException.ThrowIf(_disposed, this);
await _connection.OpenAsync();
}
// 异步 Dispose
public async ValueTask DisposeAsync()
{
if (_disposed) return;
// 异步释放资源
await _connection.CloseAsync();
await _connection.DisposeAsync();
await _reader.DisposeAsync();
_disposed = true;
GC.SuppressFinalize(this);
}
// 同步 Dispose(调用异步)
public void Dispose()
{
DisposeAsync().GetAwaiter().GetResult();
}
}
// 使用 await using
public class UsageExample
{
public async Task ProcessData()
{
await using var resource = new AsyncResourceManager(connectionString);
await resource.InitializeAsync();
// 使用完毕自动异步释放
}
}弱引用(WeakReference)
/// <summary>
/// 弱引用缓存 - 不阻止 GC 回收
/// </summary>
public class WeakReferenceCache<TKey, TValue>
where TValue : class
where TKey : notnull
{
private readonly ConcurrentDictionary<TKey, WeakReference<TValue>> _cache = new();
private readonly TimeSpan _cleanupInterval;
private readonly Timer _cleanupTimer;
public WeakReferenceCache(TimeSpan? cleanupInterval = null)
{
_cleanupInterval = cleanupInterval ?? TimeSpan.FromMinutes(5);
_cleanupTimer = new Timer(Cleanup, null, _cleanupInterval, _cleanupInterval);
}
public bool TryGet(TKey key, [MaybeNullWhen(false)] out TValue value)
{
if (_cache.TryGetValue(key, out var weakRef))
{
if (weakRef.TryGetTarget(out value))
return true;
// 对象已被回收,移除缓存条目
_cache.TryRemove(key, out _);
}
value = default;
return false;
}
public void Set(TKey key, TValue value)
{
_cache[key] = new WeakReference<TValue>(value);
}
private void Cleanup(object? state)
{
foreach (var kvp in _cache)
{
if (!kvp.Value.TryGetTarget(out _))
{
_cache.TryRemove(kvp.Key, out _);
}
}
}
}终结器队列分析
/// <summary>
/// 终结器队列监控
/// 大量对象在终结器队列中是内存泄漏的信号
/// </summary>
public class FinalizerQueueMonitor
{
public static void MonitorFinalizerQueue(ILogger logger)
{
// 注意:以下方法在 .NET 6+ 可用
long finalizerQueueLength = GC.GetGCMemoryInfo().FinalizationPendingCount;
if (finalizerQueueLength > 1000)
{
logger.LogWarning("终结器队列积压: {Count} 个对象等待终结", finalizerQueueLength);
}
}
// 检测终结器延迟问题
public static void CheckFinalizerThreadHealth(ILogger logger)
{
var thread = System.Diagnostics.Process.GetCurrentProcess().Threads
.Cast<System.Diagnostics.ProcessThread>()
.FirstOrDefault(t => t.ThreadState == System.Diagnostics.ThreadState.Wait);
if (thread != null)
{
// 如果终结器线程长时间被阻塞,对象无法被回收
logger.LogWarning("终结器线程可能被阻塞,线程ID: {ThreadId}", thread.Id);
}
}
}// 终结器延迟示例和解决方案
public class FinalizerBlockingExample : IDisposable
{
private readonly Stream _stream;
public FinalizerBlockingExample(string filePath)
{
_stream = File.OpenRead(filePath);
}
// 错误:在终结器中执行耗时操作会阻塞终结器线程
~FinalizerBlockingExample()
{
// 不要这样做!
// _stream.Close(); // 可能在终结器线程上阻塞
// Thread.Sleep(1000); // 绝对不要在终结器中等待
}
public void Dispose()
{
_stream?.Dispose();
GC.SuppressFinalize(this); // 已经手动释放,不需要终结器
}
}生产环境内存 Dump 分析
/// <summary>
/// 生产环境自动内存监控服务
/// </summary>
public class MemoryMonitorService : BackgroundService
{
private readonly ILogger<MemoryMonitorService> _logger;
private readonly long _memoryThresholdBytes;
private readonly string _dumpPath;
public MemoryMonitorService(
ILogger<MemoryMonitorService> logger,
IConfiguration configuration)
{
_logger = logger;
_memoryThresholdBytes = configuration.GetValue<long>(
"MemoryMonitor:ThresholdMB", 2048) * 1024 * 1024;
_dumpPath = configuration.GetValue<string>(
"MemoryMonitor:DumpPath", "/tmp/dumps");
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
long totalMemory = GC.GetTotalMemory(false);
var gcInfo = GC.GetGCMemoryInfo();
// 记录内存指标
_logger.LogInformation(
"内存监控: 已用={UsedMB:F1}MB, 提交={CommittedMB:F1}MB, " +
"可用={AvailableMB:F1}MB",
totalMemory / 1024.0 / 1024.0,
gcInfo.TotalCommittedBytes / 1024.0 / 1024.0,
gcInfo.TotalAvailableMemoryBytes / 1024.0 / 1024.0);
// 超过阈值自动抓取 dump
if (totalMemory > _memoryThresholdBytes)
{
_logger.LogWarning("内存超过阈值!当前: {CurrentMB:F1}MB, 阈值: {ThresholdMB:F1}MB",
totalMemory / 1024.0 / 1024.0,
_memoryThresholdBytes / 1024.0 / 1024.0);
await CaptureMemoryDump();
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
private async Task CaptureMemoryDump()
{
try
{
Directory.CreateDirectory(_dumpPath);
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string dumpFile = Path.Combine(_dumpPath, $"memorydump_{timestamp}.dmp");
_logger.LogInformation("正在抓取内存 dump: {Path}", dumpFile);
var processId = Environment.ProcessId;
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "dotnet-dump",
Arguments = $"collect -p {processId} -o {dumpFile}",
RedirectStandardOutput = true,
UseShellExecute = false
};
using var process = System.Diagnostics.Process.Start(startInfo);
if (process != null)
{
await process.WaitForExitAsync();
_logger.LogInformation("内存 dump 已保存: {Path} ({SizeMB:F1}MB)",
dumpFile,
new FileInfo(dumpFile).Length / 1024.0 / 1024.0);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "抓取内存 dump 失败");
}
}
}预防性编码实践
对象池模式
using Microsoft.Extensions.ObjectPool;
/// <summary>
/// 对象池减少 GC 压力
/// </summary>
public class ExpensiveObject
{
public byte[] Buffer { get; } = new byte[1024 * 1024]; // 1MB 缓冲区
public StringBuilder Builder { get; } = new(1024);
public void Reset()
{
Builder.Clear();
Array.Clear(Buffer);
}
}
public class ExpensiveObjectPoolPolicy : IPooledObjectPolicy<ExpensiveObject>
{
public ExpensiveObject Create() => new();
public bool Return(ExpensiveObject obj)
{
obj.Reset();
return true;
}
}
// 注册对象池
public static class ObjectPoolExtensions
{
public static IServiceCollection AddObjectPool(this IServiceCollection services)
{
services.AddSingleton<ObjectPool<ExpensiveObject>>(provider =>
{
var policy = new ExpensiveObjectPoolPolicy();
return new DefaultObjectPool<ExpensiveObject>(policy,
Environment.ProcessorCount * 2);
});
return services;
}
}
// 使用对象池
public class DataProcessor
{
private readonly ObjectPool<ExpensiveObject> _pool;
public DataProcessor(ObjectPool<ExpensiveObject> pool)
{
_pool = pool;
}
public string ProcessData(byte[] input)
{
var obj = _pool.Get();
try
{
// 使用对象...
input.CopyTo(obj.Buffer, 0);
obj.Builder.Append("Processed: ");
obj.Builder.Append(Convert.ToBase64String(input));
return obj.Builder.ToString();
}
finally
{
_pool.Return(obj); // 归还到池中,而不是让 GC 回收
}
}
}减少 GC 压力的编码习惯
/// <summary>
/// GC 友好编码实践
/// </summary>
public class GCFriendlyCoding
{
// 1. 使用 Span 和 stackalloc 减少堆分配
public static void ProcessWithSpan(ReadOnlySpan<byte> input)
{
// 栈上分配,不产生 GC 压力
Span<byte> buffer = stackalloc byte[256];
input.CopyTo(buffer);
}
// 2. 使用 ArrayPool 减少大数组分配
public static void ProcessWithArrayPool(byte[] input)
{
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(input.Length);
try
{
input.CopyTo(buffer, input.Length);
// 处理数据...
}
finally
{
pool.Return(buffer);
}
}
// 3. 使用 StringBuilder 避免字符串拼接分配
public static string BuildReport(List<DataItem> items)
{
// 预分配容量
var sb = new StringBuilder(items.Count * 50);
foreach (var item in items)
{
sb.AppendLine($"{item.Name}: {item.Value}");
}
return sb.ToString();
}
// 4. 避免在热路径中使用 LINQ(减少闭包分配)
public static List<DataItem> FilterItems(List<DataItem> items, int threshold)
{
// LINQ 版本(每次创建迭代器和闭包)
// return items.Where(x => x.Value > threshold).ToList();
// 手动循环版本(零分配)
var result = new List<DataItem>(items.Count);
foreach (var item in items)
{
if (item.Value > threshold)
result.Add(item);
}
return result;
}
// 5. 使用 ValueTask 避免异步方法的 Task 分配
public static ValueTask<int> ComputeAsync(int input)
{
// 同步完成时不分配 Task 对象
if (input < 0)
return ValueTask.FromResult(0);
// 需要异步时才分配
return new ValueTask<int>(ComputeCoreAsync(input));
}
private static async Task<int> ComputeCoreAsync(int input)
{
await Task.Delay(10);
return input * 2;
}
// 6. 使用 record struct 减少堆分配
public readonly record struct Point(double X, double Y);
// 7. 避免 params object[] 产生的数组分配
public static void Log(string message)
{
Console.WriteLine(message);
}
// 而不是 Log(string format, params object[] args)
}
public record DataItem(string Name, int Value);使用 struct 减少分配
/// <summary>
/// 在合适场景使用 struct 减少 GC 压力
/// </summary>
// 小型、短生命周期的数据使用 struct
public readonly struct Money(decimal amount, string currency)
{
public decimal Amount { get; } = amount;
public string Currency { get; } = currency;
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("货币类型不匹配");
return new Money(Amount + other.Amount, Currency);
}
}
// 使用 Memory 和 ReadOnlyMemory 避免字符串分割分配
public static class TextProcessor
{
public static ReadOnlyMemory<char> GetFirstWord(ReadOnlyMemory<char> text)
{
int spaceIndex = text.Span.IndexOf(' ');
return spaceIndex < 0 ? text : text[..spaceIndex];
}
}总结
.NET 内存泄漏虽然不像 C/C++ 那样直接,但由于引用意外保持而导致的隐式泄漏同样需要重视。通过理解常见的泄漏模式、掌握诊断工具、采用预防性编码实践,可以有效地预防和解决内存泄漏问题。
关键知识点
- 事件订阅未取消是最常见的泄漏原因
- 静态集合无限增长会持续消耗内存
- Timer 未停止会阻止对象被回收
- IDisposable 模式是防止资源泄漏的基础
- 弱引用可以在需要引用但不希望阻止 GC 时使用
- dotnet-dump 和 dotnet-counters 是生产环境诊断的利器
- ObjectPool 和 ArrayPool 可以显著减少 GC 压力
常见误区
误区1:.NET 有 GC 就不会有内存泄漏
GC 只回收不可达的对象。如果代码仍然持有对对象的引用(即使不再使用),GC 不会回收它们。
误区2:内存泄漏一定是 bug
有时"泄漏"是设计缺陷——缓存没有过期策略、日志列表没有大小限制。这些需要从设计层面解决。
误区3:设置 GC.Collect() 可以解决问题
强制 GC 不能解决泄漏问题,只会暂时缓解。应找到并修复根因。
误区4:内存使用增长就是泄漏
内存增长可能是正常的(缓存填充、负载增加)。只有持续增长且永不回落才是泄漏。
进阶路线
- GC 内部机制:深入了解代、段、LOH/SOH/POH
- 性能分析专家:精通 dotMemory、PerfView 的高级功能
- SOS 调试:使用 WinDbg + SOS 进行深度分析
- 内存 profiler 开发:理解 ETW 和 CLR Profiling API
- Server GC vs Workstation GC:不同 GC 模式的适用场景
适用场景
- 长时间运行的 Windows 服务
- ASP.NET Core Web 应用
- WPF/WinForms 桌面应用
- 后台任务处理器
- 实时数据处理服务
落地建议
- 在 CI/CD 中集成内存泄漏检测测试
- 生产环境部署内存监控和自动 dump 抓取
- 代码审查清单中包含 IDisposable 相关检查
- 建立内存基线,监控偏离趋势
- 定期进行耐久测试(Soak Test)检测泄漏
- 所有包含 IDisposable 字段的类都应实现 IDisposable
排错清单
复盘问题
- 你的应用是否定期进行耐久测试?最长的连续运行时间是多少?
- 你的应用在正常负载下的内存增长趋势是什么?
- 你是否有自动化的内存监控和告警机制?
- 上次内存泄漏排查是什么时候?根因是什么?
- 你的代码审查是否包含内存管理方面的检查?
延伸阅读
- .NET GC Documentation
- dotnet-dump Documentation
- Profiling .NET Memory Usage
- 《Pro .NET Memory Management》- Konrad Kokosa
- 《Writing High-Performance .NET Code》- Ben Adams
