【问题标题】:Can delegates cause a memory leak? GC.TotalMemory(true) seems to indicate so委托会导致内存泄漏吗? GC.TotalMemory(true) 似乎表明了这一点
【发布时间】:2025-12-07 03:05:01
【问题描述】:

代码

using System;
internal static class Test
{
    private static void Main()
    {
        try
        {
            Console.WriteLine("{0,10}: Start point", GC.GetTotalMemory(true));
            Action simpleDelegate = SimpleDelegate;
            Console.WriteLine("{0,10}: Simple delegate created", GC.GetTotalMemory(true));
            Action simpleCombinedDelegate = simpleDelegate + simpleDelegate + simpleDelegate;
            Console.WriteLine("{0,10}: Simple combined delegate created", GC.GetTotalMemory(true));
            byte[] bigManagedResource = new byte[100000000];
            Console.WriteLine("{0,10}: Big managed resource created", GC.GetTotalMemory(true));
            Action bigManagedResourceDelegate = bigManagedResource.BigManagedResourceDelegate;
            Console.WriteLine("{0,10}: Big managed resource delegate created", GC.GetTotalMemory(true));
            Action bigCombinedDelegate = simpleCombinedDelegate + bigManagedResourceDelegate;
            Console.WriteLine("{0,10}: Big combined delegate created", GC.GetTotalMemory(true));
            GC.KeepAlive(bigManagedResource);
            bigManagedResource = null;
            GC.KeepAlive(bigManagedResourceDelegate);
            bigManagedResourceDelegate = null;
            GC.KeepAlive(bigCombinedDelegate);
            bigCombinedDelegate = null;
            Console.WriteLine("{0,10}: Big managed resource, big managed resource delegate and big combined delegate removed, but memory not freed", GC.GetTotalMemory(true));
            GC.KeepAlive(simpleCombinedDelegate);
            simpleCombinedDelegate = null;
            Console.WriteLine("{0,10}: Simple combined delegate removed, memory freed, at last", GC.GetTotalMemory(true));
            GC.KeepAlive(simpleDelegate);
            simpleDelegate = null;
            Console.WriteLine("{0,10}: Simple delegate removed", GC.GetTotalMemory(true));
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
        Console.ReadKey(true);
    }
    private static void SimpleDelegate() { }
    private static void BigManagedResourceDelegate(this byte[] array) { }
}

输出

GC.TotalMemory(true)
    105776: Start point
    191264: Simple delegate created
    191328: Simple combined delegate created
 100191344: Big managed resource created
 100191780: Big managed resource delegate created
 100191812: Big combined delegate created
 100191780: Big managed resource, big managed resource delegate and big combined delegate removed, but memory not freed
    191668: Simple combined delegate removed, memory freed, at last
    191636: Simple delegate removed

【问题讨论】:

  • 感谢可执行复制,顺便说一句!

标签: c# .net memory-leaks delegates garbage-collection


【解决方案1】:

有趣的案例。这是解决方案:

组合委托是观察上纯粹的:看起来委托对外部来说是不可变的。但在内部,正在修改现有的代表。在某些情况下,出于性能原因,它们共享相同的_invocationList(针对少数代表连接到同一事件的场景进行优化)。不幸的是,simpleCombinedDelegate_invocationList 引用了 bigMgdResDelegate,这导致内存保持活动状态。

【讨论】:

  • 对图像和所有内容的出色回答。
  • 哇,这太令人惊讶了!但它是真正的突变,还是巧妙的局部优化?也许编译器/JIT 在方法中向前看,并在创建第一个委托之前创建一个预先填充的 4 元素数组?
  • @Weeble,不确定您对 JIT 优化的意思,但 JIT 通常非常愚蠢。它不会做那样复杂的事情。无论如何,JIT 只能在 IL 代码命令它时改变东西。它从不引入突变本身,因为那是不安全的。
  • 我试图了解它是如何工作的。在创建bigCombinedDelegate 之前,simpleCombinedDelegate 的_invocationList 是什么大小,为什么?如果是 4,它怎么知道最后一个 slot 可以安全地进行变异并且还没有与另一个代表共享?如果是 3,分配一个新的大小为 4 的数组并丢弃旧的大小为 3 的数组有什么好处?我认为性能优势在于避免分配,但现在我不确定。
  • 我也没有。使用反射器查看源。数组可以包含空值,顺便说一句。
【解决方案2】:

我可能在这里忽略了要点,但是垃圾收集在设计上是不确定的。因此,由 .NET 框架决定何时回收内存。

您可以在一个简单的循环中运行 GC.GetTotalMemory 并获得不同的数字。也许不足为奇,因为文档指定返回的数字是一个近似值。

【讨论】:

  • 同意这个答案主要是因为 OP 提出的测试太简单了,无法检测到任何有用的东西。托管环境中的“内存泄漏”通常归结为设计不良的代码(包含对事物列表的引用的静态对象。)在游戏的这一点上,您不太可能在 GC 中找到任何实际错误。
  • GetTotalMemory 执行显式 GC。这个测试是相当可靠的。另请注意,在此测试的所有其他情况下,清空变量都有效(进一步证明这可以按预期工作)。
  • 我认为 OP 的重点是 GC.GetTotalMemory(true) 应该强制收集,但是在将大型委托都清空并强制收集之后,内存仍然被分配。
  • 我是说这可能不是 GC 中的错误,而是一个人为的例子。
  • 是的,垃圾收集发生了,但收集了哪些代?