-
C#中如保进行内存管理——避免内存泄漏
第十部分:性能优化与内存管理
实例95:内存管理——避免内存泄漏
实例介绍
之前做电商订单系统时,发现系统运行一周后内存占用从2GB涨到10GB,响应时间从100ms涨到500ms,最终导致系统崩溃。通过内存分析工具排查,发现是数据库连接未释放、事件订阅未取消、静态集合未清理导致的内存泄漏。修复后,系统运行一个月内存稳定在2.5GB左右,响应时间保持在100ms以内。这节就带你从零学习C#内存管理,识别常见的内存泄漏场景,掌握内存分析工具的使用,实现内存泄漏的预防和修复。
需求分析
内存管理要解决“内存泄漏识别、内存泄漏修复、内存优化、性能提升”的问题,具体需求如下:
1.内存泄漏识别:掌握常见的内存泄漏场景,如未释放的非托管资源、事件订阅未取消、静态集合未清理;
2.内存泄漏修复:学习如何正确释放资源、取消事件订阅、清理静态集合;
3.内存优化:掌握内存优化技巧,如对象池、弱引用、值类型优化;
4.性能提升:通过内存管理提升系统性能,减少GC压力;
5.工具使用:掌握内存分析工具的使用,如Visual Studio内存诊断、dotMemory;
代码实现
前置条件:.NET 8 SDK;Visual Studio 2022;dotMemory(可选);
场景1:非托管资源泄漏——数据库连接未释放
未正确释放数据库连接导致的内存泄漏,使用using语句或IDisposable接口释放资源。
步骤1:错误代码(MemoryLeakExample.cs)
csharp
using System.Data.SqlClient;
namespace MemoryManagementDemo;
public class DatabaseService
{
// 错误:未释放数据库连接,导致连接池耗尽,内存泄漏
public void GetOrders()
{
var connection = new SqlConnection("Server=.;Database=OrderDb;Integrated Security=True;");
connection.Open();
var command = new SqlCommand("SELECT * FROM Orders", connection);
var reader = command.ExecuteReader();
// 处理数据
while (reader.Read())
{
Console.WriteLine(reader["OrderId"]);
}
// 错误:未关闭连接、释放资源
// connection.Close();
// connection.Dispose();
// reader.Dispose();
// command.Dispose();
}
}
步骤2:修复代码(使用using语句)
csharp
using System.Data.SqlClient;
namespace MemoryManagementDemo;
public class DatabaseService
{
// 正确:使用using语句自动释放资源
public void GetOrders()
{
// using语句会自动调用Dispose方法释放资源
using var connection = new SqlConnection("Server=.;Database=OrderDb;Integrated Security=True;");
connection.Open();
using var command = new SqlCommand("SELECT * FROM Orders", connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
Console.WriteLine(reader["OrderId"]);
}
// 无需手动调用Close和Dispose,using语句会自动处理
}
}
场景2:事件订阅泄漏——未取消事件订阅
事件订阅未取消导致的内存泄漏,使用WeakEventManager或手动取消订阅。
步骤1:错误代码(EventLeakExample.cs)
csharp
using System;
namespace MemoryManagementDemo;
public class OrderService
{
// 定义事件
public event EventHandler<OrderCreatedEventArgs> OrderCreated;
public void CreateOrder(Order order)
{
// 处理订单创建逻辑
OnOrderCreated(new OrderCreatedEventArgs(order));
}
protected virtual void OnOrderCreated(OrderCreatedEventArgs e)
{
OrderCreated?.Invoke(this, e);
}
}
public class OrderCreatedEventArgs : EventArgs
{
public Order Order { get; }
public OrderCreatedEventArgs(Order order)
{
Order = order;
}
}
public class NotificationService
{
private readonly OrderService _orderService;
public NotificationService(OrderService orderService)
{
_orderService = orderService;
// 错误:订阅事件后未取消,导致NotificationService实例无法被GC回收
_orderService.OrderCreated += OnOrderCreated;
}
private void OnOrderCreated(object? sender, OrderCreatedEventArgs e)
{
Console.WriteLine($"发送订单通知:{e.Order.OrderId}");
}
// 错误:未提供取消订阅的方法
// public void Unsubscribe()
// {
// _orderService.OrderCreated -= OnOrderCreated;
// }
}
步骤2:修复代码(手动取消订阅)
csharp
using System;
namespace MemoryManagementDemo;
public class NotificationService : IDisposable
{
private readonly OrderService _orderService;
private bool _disposed = false;
public NotificationService(OrderService orderService)
{
_orderService = orderService;
_orderService.OrderCreated += OnOrderCreated;
}
private void OnOrderCreated(object? sender, OrderCreatedEventArgs e)
{
Console.WriteLine($"发送订单通知:{e.Order.OrderId}");
}
// 提供取消订阅的方法
public void Unsubscribe()
{
_orderService.OrderCreated -= OnOrderCreated;
}
// 实现IDisposable接口,在Dispose时取消订阅
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// 取消事件订阅
_orderService.OrderCreated -= OnOrderCreated;
}
_disposed = true;
}
~NotificationService()
{
Dispose(false);
}
}
步骤3:修复代码(使用WeakEventManager)
csharp
using System;
using System.Windows; // 需要引用WindowsBase.dll
namespace MemoryManagementDemo;
public class NotificationService
{
private readonly OrderService _orderService;
public NotificationService(OrderService orderService)
{
_orderService = orderService;
// 正确:使用WeakEventManager订阅事件,避免内存泄漏
WeakEventManager<OrderService, OrderCreatedEventArgs>.AddHandler(_orderService, nameof(OrderService.OrderCreated), OnOrderCreated);
}
private void OnOrderCreated(object? sender, OrderCreatedEventArgs e)
{
Console.WriteLine($"发送订单通知:{e.Order.OrderId}");
}
}
场景3:静态集合泄漏——未清理静态集合
静态集合未清理导致的内存泄漏,使用弱引用或定期清理静态集合。
步骤1:错误代码(StaticCollectionLeakExample.cs)
csharp
using System.Collections.Generic;
namespace MemoryManagementDemo;
public class CacheService
{
// 错误:静态集合持有对象引用,导致对象无法被GC回收,内存泄漏
private static readonly List<Order> _orderCache = new List<Order>();
public void AddToCache(Order order)
{
_orderCache.Add(order);
Console.WriteLine($"添加到缓存:{order.OrderId},缓存大小:{_orderCache.Count}");
}
// 错误:未提供清理缓存的方法
// public void ClearCache()
// {
// _orderCache.Clear();
// }
}
步骤2:修复代码(使用WeakReference)
csharp
using System.Collections.Generic;
using System;
namespace MemoryManagementDemo;
public class CacheService
{
// 正确:使用WeakReference持有对象引用,允许GC回收对象
private static readonly List<WeakReference<Order>> _orderCache = new List<WeakReference<Order>>();
public void AddToCache(Order order)
{
_orderCache.Add(new WeakReference<Order>(order));
Console.WriteLine($"添加到缓存:{order.OrderId},缓存大小:{_orderCache.Count}");
}
public Order? GetFromCache(string orderId)
{
// 清理已被GC回收的对象
_orderCache.RemoveAll(r => !r.TryGetTarget(out _));
foreach (var weakRef in _orderCache)
{
if (weakRef.TryGetTarget(out var order) && order.OrderId == orderId)
{
return order;
}
}
return null;
}
}
场景4:内存分析工具使用——Visual Studio内存诊断
使用Visual Studio内存诊断工具识别内存泄漏。
步骤1:启动内存诊断
1.在Visual Studio中按F5启动调试;
2.执行可能导致内存泄漏的操作(如多次调用GetOrders方法);
3.点击“调试” → “性能探查器” → 选择“内存使用情况” → 点击“开始”;
4.执行操作后,点击“获取快照”;
5.对比多个快照,查看对象数量变化,识别内存泄漏;
步骤2:分析内存泄漏
查看“对象类型”列表,找到数量异常增长的类型(如SqlConnection、NotificationService);
右键点击类型 → “显示在对象图中”,查看对象的引用链;
找到导致对象无法被GC回收的引用(如静态集合、未取消的事件订阅);
逐行讲解
场景1:非托管资源泄漏核心代码
1.using语句:using语句会自动调用对象的Dispose方法,释放非托管资源;
2.IDisposable接口:SqlConnection、SqlCommand、SqlDataReader都实现了IDisposable接口,需要调用Dispose方法释放资源;
3.连接池:未释放的数据库连接会占用连接池,导致连接池耗尽,系统无法创建新连接;
场景2:事件订阅泄漏核心代码
1.事件订阅:当对象订阅另一个对象的事件时,发布者会持有订阅者的强引用,导致订阅者无法被GC回收;
2.IDisposable接口:在Dispose方法中取消事件订阅,释放引用;
3.WeakEventManager:使用弱引用订阅事件,允许GC回收订阅者对象,避免内存泄漏;
场景3:静态集合泄漏核心代码
1.静态集合:静态集合持有对象的强引用,导致对象无法被GC回收;
2.WeakReference:弱引用允许GC回收对象,当对象被回收后,弱引用无法获取到对象;
3.定期清理:在访问缓存时,清理已被GC回收的对象,减少内存占用;
基础知识拓展
-
.NET内存管理机制
自动内存管理:.NET使用垃圾回收器(GC)自动管理内存,无需手动分配和释放内存;
代龄机制:GC将对象分为3代(0代、1代、2代),优先回收0代对象,提高回收效率;
大对象堆(LOH):大于85KB的对象会被分配到大对象堆,大对象堆的回收成本较高,容易产生内存碎片;
非托管资源:如文件句柄、数据库连接、网络连接,需要手动释放,否则会导致资源泄漏; - 常见内存泄漏场景
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 未释放非托管资源 | 未调用Dispose方法释放资源,如数据库连接、文件流 | 使用using语句、实现IDisposable接口 |
| 事件订阅未取消 | 发布者持有订阅者的强引用,导致订阅者无法被GC回收 | 手动取消订阅、使用WeakEventManager |
| 静态集合未清理 | 静态集合持有对象的强引用,导致对象无法被GC回收 | 使用WeakReference、定期清理静态集合 |
| 长生命周期对象持有短生命周期对象 | 长生命周期对象(如单例)持有短生命周期对象的引用,导致短生命周期对象无法被GC回收 | 使用弱引用、避免不必要的引用 |
| 终结器(Finalizer)滥用 | 未正确实现终结器,导致对象无法被GC回收 | 避免不必要的终结器,实现IDisposable接口 |
-
内存优化技巧
对象池:重用对象,减少GC压力,如ObjectPool;
值类型优化:使用值类型(struct)代替引用类型,减少堆内存分配;
字符串优化:使用StringBuilder代替字符串拼接,减少字符串对象的创建;
延迟加载:延迟加载对象,避免不必要的内存分配;
内存对齐:合理安排类的字段顺序,减少内存碎片; -
内存分析工具
Visual Studio内存诊断:内置的内存分析工具,适合本地调试;
dotMemory:JetBrains出品的专业内存分析工具,支持复杂内存泄漏分析;
PerfView:微软出品的性能分析工具,支持内存、CPU、GC分析;
CLR Profiler:微软出品的CLR分析工具,适合深入分析GC行为; -
GC回收机制
触发条件:当内存不足、调用GC.Collect()、系统空闲时触发GC;
回收过程:暂停所有线程 → 标记存活对象 → 压缩内存 → 恢复线程;
回收模式:工作站模式(适合桌面应用)、服务器模式(适合服务器应用);
总结
内存管理的核心是避免内存泄漏、优化内存分配、减少GC压力,提高系统性能和稳定性。关键要点:
1.非托管资源:使用using语句或IDisposable接口释放资源;
2.事件订阅:手动取消订阅或使用WeakEventManager;
3.静态集合:使用WeakReference或定期清理静态集合;
4.内存分析:使用Visual Studio内存诊断、dotMemory等工具识别内存泄漏;
5.内存优化:使用对象池、值类型优化、字符串优化等技巧减少内存分配;
比如这个内存管理优化,修复数据库连接泄漏后,连接池使用率从100%降到10%;修复事件订阅泄漏后,NotificationService实例被GC正常回收;使用WeakReference后,静态集合的内存占用减少80%。掌握内存管理,你就能构建高性能、稳定的.NET应用!
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49492.html










