The Classic Event-Subscriber Memory Leak
1Concept
Subscribing to an event on a long-lived publisher creates a strong reference from the publisher to the subscriber. The subscriber cannot be garbage collected, creating silent memory leaks.
2Architecture Diagram
[ Long-Lived Singleton Publisher ]
│ (Holds strong reference in delegate invocation list)
▼
[ Short-Lived Transient Form / Service ] ──> Cannot be collected by GC!3Code Example
C# 13 & .NET 9
using System;
public class LongLivedPublisher
{
public event Action? GlobalTick;
public void Trigger() => GlobalTick?.Invoke();
}
public class ShortLivedSubscriber : IDisposable
{
private readonly LongLivedPublisher _publisher;
private readonly string _name;
public ShortLivedSubscriber(LongLivedPublisher publisher, string name)
{
_publisher = publisher;
_name = name;
_publisher.GlobalTick += OnTick; // Strong reference created!
}
private void OnTick() => Console.WriteLine($"[{_name}] Tick received.");
public void Dispose()
{
_publisher.GlobalTick -= OnTick; // Crucial: Breaks reference!
Console.WriteLine($"[{_name}] Unsubscribed cleanly.");
}
}
public class MemoryLeakDemo
{
public static void Main()
{
var pub = new LongLivedPublisher();
using (var sub = new ShortLivedSubscriber(pub, "TempWorker"))
{
pub.Trigger();
}
Console.WriteLine("Worker disposed and unhooked.");
}
}4Expected Output
[TempWorker] Tick received. [TempWorker] Unsubscribed cleanly. Worker disposed and unhooked.
5Key Takeaways
- ✓Always unsubscribe events in `Dispose()`.
- ✓Long-lived publishers are the #1 source of memory leaks in desktop and server apps.
- ✓Use memory profilers (dotMemory / PerfView) to inspect GC roots.