.net中事件引起的内存泄漏分析
目录
系列主题:基于消息的软件架构模型演变
系列主题:基于消息的软件架构模型演变
在Winform和Asp.net时代,事件被大量的应用在UI和后台交互的代码中。看下面的代码:
1 2 3 4 5 6 7 8 9 10 | private void BindEvent() { var btn = new Button(); btn.Click += btn_Click; } void btn_Click( object sender, EventArgs e) { MessageBox.Show( "click" ); } |
这样的用法可以引起内存泄漏吗?为什么我们平时一直写这样的代码从来没关注过内存泄漏?等分析完原因后再来回答这个问题。
为了测试原因,我们先写一个EventPublisher类用来发布事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public class EventPublisher { public static int Count; public event EventHandler<PublisherEventArgs> OnSomething; public EventPublisher() { Interlocked.Increment( ref Count); } public void TriggerSomething() { RaiseOnSomething( new PublisherEventArgs(Count)); } protected void RaiseOnSomething(PublisherEventArgs e) { EventHandler<PublisherEventArgs> handler = OnSomething; if (handler != null ) handler( this , e); } ~EventPublisher() { Interlocked.Decrement( ref Count); } } |
这个类提供了一个事件OnSomething,另外在构造函数和析构函数中分别会对变量Count进行累加和递减。Count的数量反应了EventPublisher的实例在内存中的数量。
写一个Subscriber用来订阅这个事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public class Subscriber { public string Text { get ; set ; } public List<StringBuilder> List = new List<StringBuilder>(); public static int Count; public Subscriber() { Interlocked.Increment( ref Count); for ( int i = 0; i < 1000; i++) { List.Add( new StringBuilder(1024)); } } public void ShowMessage( object sender, PublisherEventArgs e) { Text = string .Format( "There are {0} publisher in memory" ,e.PublisherReferenceCount); } ~Subscriber() { Interlocked.Decrement( ref Count); } } |
Subscriber同样用Count来反映内存中的实例数量,另外我们在构造函数中使用StringBuilder开辟1000*1024Size的大小以方便我们观察内存使用量。
最后一步,写一个简单的winform程序,然后在一个Button的Click事件中写入测试代码:
1 2 3 4 5 6 7 8 9 10 11 | private void btnStartShortTimePublisherTest_Click( object sender, EventArgs e) { for ( int i = 0; i < 100; i++) { var publisher = new EventPublisher(); publisher.OnSomething += new Subscriber().ShowMessage; publisher.TriggerSomething(); } MessageBox.Show( string .Format( "There are {0} publishers in memory, {1} subscribers in memory" , EventPublisher.Count, Subscriber.Count)); } |
for循环中的代码是一个很普通的事件调用代码,我们将Subscriber实例中的ShowMessage方法绑定到了publisher对象的OnSomething事件上,为了观察内存的变化我们循环100次。
执行结果如下:
publisher和subscriber的数量都为3,这并不代表发生了内存泄漏,只不过是没有完全回收完毕而已。每个publisher在出了for循环后就会被认为没有任何用处,从而被正确回收。而注册在上面的观察者subscriber也能被正确回收。
再放一个Button,并在Click中写以下测试代码:
1 2 3 4 5 6 7 8 9 10 11 | private void BtnStartLongTimePublisher_Click( object sender, EventArgs e) { for ( int i = 0; i < 100; i++) { var publisher = new EventPublisher(); publisher.OnSomething += new Subscriber().ShowMessage; publisher.TriggerSomething(); LongLivedEventPublishers.Add(publisher); } MessageBox.Show( string .Format( "There are {0} publishers in memory, {1} subscribers in memory" , EventPublisher.Count,Subscriber.Count)); } |
这次for循环中不同之处在于我们将publisher保存在了一个list容器当中,从而保证100个publisher不能垃圾回收。这次的执行结果如下:
我们看到100个subscribers全部保存在内存中。如果观察资源管理器中的内存使用率,你也能发现内存突然涨了几百兆并且再不会减少。
想一下下面的场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class Runner { private LongTimeService _service; public Runner() { _service = new LongTimeService(); } public void Run() { _service.SomeThingUpdated += (o, e) => { /*do some thing*/ }; _service.SomeThingUpdated += (o, e) => { /*do some thing*/ }; _service.SomeThingUpdated += (o, e) => { /*do some thing*/ }; _service.SomeThingUpdated += (o, e) => { /*do some thing*/ }; } } |
LongTimeService是一个长期运行的服务,从来不被销毁,这将导致所有注册在SomeThingUpdated 事件上的观察者也不会能回收。当有大量的观察者不停的注册在SomeThingUpdated 上时,就会发生内存泄漏。
这三个测试说明了引起事件内存泄漏的场景:当观察者注册在了一个生命周期长于自己的事件主题上,观察者不能被内存回收。
解决办法是在事件上显示调用-=符号。
再回过头来看开始提出来的问题:当使用了Button的Click事件的时候,会发生内存泄漏吗?
1 | btn.Click += btn_Click; |
观察者是谁?btn_Click方法的拥有者,也就是Form实例。
主题是谁?Button的实例btn
主题btn什么时候销毁?当Form实例被销毁的时候。
当Form被销毁的时候,btn及其观察者都会被销毁。除非Form从来不销毁,并且大量的观察者持续注册在了btn.Click上才能发生内存泄漏,当然这种场景是很少见的。所以我们开发winform或者asp.net的时候一般来说并不会关心内存泄漏的问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)