本以为带有GC
功能的 C# 不会有内存泄露问题,原来我天真了,其实 C# 也会有内存泄露问题,只是不像 C/C++ 那么有负担。
常见泄露方式
在 C/C++ 语言中,内存泄露是因为分配了内存后未释放,同时没有指针指向它导致的。
而在 C# 中,内存泄露是因为内存被引用了,但引用这块内存的实例却从不会被访问到,从而导致 GC 无法回收它。
未取消事件订阅
1 | public class Publisher |
不出意外的话,事件处理器会被调用两次。因为 Publisher 实例的 MyEvent 一直持有对 Subscriber 的引用,导致内存泄露。
最好的解决方式是手动用-=
操作及时取消订阅,这是最好的方式,没有之一。
使用弱事件模式
弱事件模式(Weak Event Pattern)是一种在事件订阅中使用弱引用的设计模式。它旨在解决常见的事件订阅导致的潜在内存泄漏问题。
订阅者对象使用弱引用来订阅事件。通过使用弱引用,订阅者对象可以在没有其他强引用时被垃圾回收,从而避免潜在的内存泄漏问题。当订阅者对象被垃圾回收时,事件发布者不再持有对其的引用,因此不会阻止其被回收。
1 | public void Subscribe(Publisher publisher) |
WeakEventManager 是基础。.NET 内置了一些弱事件类如 WeakEventManager<TEventSource,TEventArgs>、PropertyChangedEventManager 等等。
再次运行程序,还是会有两次输出,这是因为 GC 没有及时回收 Subscriber,这可能会导致各种意想不到的副作用。所以在使用弱事件时需要注意这个问题。
lambda函数
lambda函数适合作为事件处理器吗?
1 | publisher.MyEvent += (object sender, EventArgs args) => |
结果是 GC 之后处理器仍然会被调用。本质上这个处理器还是强引用,且无法被取消。
而如果在这个函数中有任何对类实例的引用,那么也会造成内存泄露。
1 | publisher.MyEvent += (object sender, EventArgs args) => |
所以为了避免内存泄露,还是要为实例指定一个变量并稍后取消订阅:
1 | EventHandler handler = delegate (object sender, EventArgs e) |
静态引用
1 | public class MyClass |
在这段代码中,MyClass
类的构造函数会将每个新创建的实例添加到一个静态的List<MyClass>
集合中。由于List<MyClass>
是静态的,它会一直保留对所有实例的引用。这将导致内存泄漏,因为这些对象无法被释放。
为了解决这个问题,可以通过添加一个析构函数或者手动释放资源的方法来从_instances
集合中移除实例。例如:
1 | public class MyClass |
计时器
1 | public class MyClass |
由于没有在适当的时候停止或销毁 Timer,它将一直保持对 MyClass 实例的引用,导致内存泄漏。
总结
内存泄漏的本质是引用没有被正确释放,导致对象无法被垃圾回收器回收。在 C# 中,垃圾回收器负责自动管理内存,它会跟踪对象之间的引用关系,并在对象不再被引用时,自动回收它们所占用的内存。
当一个对象不再被引用时,垃圾回收器会将其标记为可回收。但如果该对象仍然被其他对象引用,或者存在循环引用,垃圾回收器无法判断该对象是否仍然需要保留。这就导致了一个内存泄漏的情况,即对象无法被垃圾回收器正确释放,从而持续占用内存。
为了避免内存泄漏,需要及时释放对象的引用。这可以通过手动解除对象之间的引用、及时取消注册事件处理器、释放非托管资源、将静态变量设置为弱引用等方式来实现。这样,垃圾回收器就能够正确地判断对象是否可以被回收,并及时释放对象所占用的内存。
参考资料
可能会导致.NET内存泄露的8种行为
5 Techniques to avoid Memory Leaks by Events in C# .NET you should know