ConditionalWeakTable与GC.Collect()行为异常:WeakReference.Target未回收原因排查
为什么第二次GC.Collect()后WeakReference仍存活?这不是Bug,问题出在循环引用
这不是.NET的Bug,核心原因是委托实例与目标对象通过ConditionalWeakTable形成了循环强引用,导致GC无法回收这两个对象。
测试结果
weakRef.Target is alive = True, expected true because inst keep a hold on SomeClass. weakRef.Target is alive = True, expected false, because there is no more ref on SomeClass.
测试代码
public static class DelegateKeeper { private static ConditionalWeakTable<object, Action> cwtAction = new ConditionalWeakTable<object, Action>(); public static void KeepAlive(Action action) => cwtAction.Add(action.Target, action); } public class SomeClass { public void DoSomething() { } } public static class GcHelper { public static void Collect() { // OK surely overkill but just to make sure. I will reduce it when everyting will be understood. GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); GC.WaitForPendingFinalizers(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true); GC.WaitForPendingFinalizers(); } } SomeClass instanceSomeClass; WeakReference<Action> weakRef; [TestMethod] public void TestLifeOfObject() { Init(); GcHelper.Collect(); Debug.WriteLine($"weakRef.Target is alive = {weakRef.TryGetTarget(out _)}, expected true because inst keep a hold on SomeClass."); RemoveLastReferenceOnSomeClass(); GcHelper.Collect(); Debug.WriteLine($"weakRef.Target is alive = {weakRef.TryGetTarget(out _)}, expected false, because there is no more ref on SomeClass."); } private void Init() { instanceSomeClass = new SomeClass(); var action = instanceSomeClass.DoSomething; weakRef = new WeakReference<Action>(action); DelegateKeeper.KeepAlive(action); } private void RemoveLastReferenceOnSomeClass() { instanceSomeClass = null; }
问题拆解
- 委托的强引用特性:
instanceSomeClass.DoSomething生成的委托实例action,内部会自动持有对instanceSomeClass(目标对象)的强引用——这是实例方法委托的固有特性,用来保证调用方法时目标对象存在。 - ConditionalWeakTable的行为:
DelegateKeeper中的ConditionalWeakTable以action.Target(即instanceSomeClass)为弱键,action本身为强值。CWT的规则是:只要弱键对应的对象还存活,就会通过强引用保留对应的value;只有当弱键对象被GC判定为不可达时,才会移除条目并释放value。 - 循环引用的形成:当你把
instanceSomeClass置为null后,外部对目标对象的强引用消失,但此时:action仍然强引用instanceSomeClass;- CWT因为
instanceSomeClass还存活,所以继续强引用action; - 这就形成了一个闭环:
instanceSomeClass存活 →action被CWT保留 →instanceSomeClass被action强引用而存活。
- GC无法打破循环:GC回收循环引用的前提是,循环内的对象没有外部强引用。但这里的循环依赖CWT的弱键存活状态,GC无法判定
instanceSomeClass为不可达——因为action还在引用它,而action的存活又依赖于instanceSomeClass的存活,最终导致两者都无法被回收,weakRef.Target(即action)自然也会保持存活。
解决思路
如果需要让目标对象和委托能被正常回收,要避免CWT的value持有对弱键的强引用。比如,不要直接存储action,而是拆分委托的方法信息和目标对象,只存储不依赖目标对象存活的部分(比如仅存储MethodInfo),但这样会丢失实例方法的上下文,需要根据实际业务场景调整逻辑。
内容的提问来源于stack exchange,提问作者Eric Ouellet
相关产品推荐
相关产品推荐

