为什么C#中的lambda表达式会导致内存泄漏?

use*_*248 8 c# lambda memory-leaks

注意:这不仅仅是一些随机无用的代码,这是尝试在C#中重现lambda表达式和内存泄漏的问题.

在C#中检查以下程序.这是一个简单的控制台应用程序:

  1. 创建Test类型的新对象
  2. 写入控件创建对象
  3. 调用垃圾收集
  4. 等待任何用户输入
  5. 关闭

我使用JetBrains DotMemory运行这个程序,我带了两个内存快照:一个在初始化对象后,另一个在收集后.我比较了快照并得到了我所期望的:一个类型为Test的死对象.

但这是窘境:然后我在对象的构造函数中创建一个本地lambda表达式,我不会在任何地方使用它.它只是一个本地构造函数变量.我在DotMemory中运行相同的过程,突然间,我得到了一个Test + <>类型的对象,它可以在垃圾回收中幸存下来.

请参阅DotMemory附带的保留路径报告:lambda表达式有一个指向Test + <>对象的指针,这是预期的.但谁有指向lambda表达式的指针,为什么它保存在内存中?

另外,这个Test + <>对象 - 我认为它只是暂存对象来保存lambda方法,并且与原始的Test对象无关,我是对的吗?

public class Test
{
    public Test()
    {
        // this line causes a leak
        Func<object, bool> t = _ => true;
    }

    public void WriteFirstLine()
    {
        Console.WriteLine("Object allocated...");
    }

    public void WriteSecondLine()
    {
        Console.WriteLine("Object deallocated. Press any button to exit.");
    }
}

class Program
{
    static void Main(string[] args)
    {
        var t = new Test();
        t.WriteFirstLine();
        Console.ReadLine();
        t.WriteSecondLine();
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Console.ReadLine();
    }
}
Run Code Online (Sandbox Code Playgroud)

DotMemory保留路径报告

Evk*_*Evk 11

如果你用某些东西(比如dotpeek)反编译你的代码,你会发现编译器生成了这样的东西:

public class Test {
    public Test() {
        if (Test.ChildGeneratedClass.DelegateInstance != null)
            return;
        Test.ChildGeneratedClass.DelegateInstance = 
            Test.ChildGeneratedClass.Instance.DelegateFunc;
    }

    public void WriteFirstLine() {
        Console.WriteLine("Object allocated...");
    }

    public void WriteSecondLine() {
        Console.WriteLine("Object deallocated. Press any button to exit.");
    }

    [CompilerGenerated]
    [Serializable]
    private sealed class ChildGeneratedClass {
        // this is what's called Test.<c> <>9 in your snapshot
        public static readonly Test.ChildGeneratedClass Instance;
        // this is Test.<c> <>9__0_0
        public static Func<object, bool> DelegateInstance;

        static ChildGeneratedClass() {
            Test.ChildGeneratedClass.Instance = new Test.ChildGeneratedClass();
        }

        internal bool DelegateFunc(object _) {
            return true;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,它创建了子类,将您的函数作为该类的实例方法,在静态字段中创建该类的单例实例,最后使用您的引用方法创建静态字段.毫无疑问,GC无法收集编译器生成的静态成员.当然,这些对象不是为您创建的每个对象创建的,只是一次,因此我无法将其称为"泄漏".Func<object,boolDelegateFuncTest

  • 感谢您提供如此详尽且非常出色的解释。我终于明白了。不,这根本不是泄漏!当我对我的真实应用程序进行概要分析并在兔子洞中追踪它时,我发现了这一点,直到我发现看起来像是由 lambdas 引起的泄漏。我现在明白它是如何工作的了。这可能是我在 StackOverflow 上得到的最好的答案。谢谢! (2认同)

Jon*_*eet 6

我怀疑您看到的是编译器优化的效果。

假设Test()被多次调用。编译器每次都可以创建一个新的委托——但这似乎有点浪费。lambda 表达式既不捕获this局部变量或参数,也不捕获任何局部变量或参数,因此可以将单个委托实例重用于Test(). 编译器发出代码来懒惰地创建委托,但将其存储在静态字段中。所以它是这样的:

private static Func<object, bool> cachedT;

public Test()
{
    if (cachedT == null)
    {
        cachedT = _ => true;
    }
    Func<object, bool> t = cachedT;
}
Run Code Online (Sandbox Code Playgroud)

现在这确实创建了一个永远不会被垃圾收集的对象,但如果Test频繁调用它会降低 GC 压力。不幸的是,编译器无法真正知道哪个可能更好。

通过查看由 lambda 表达式产生的委托,可以通过引用相等性检测到这一点。例如,这会打印 True(至少对我而言;这是一个编译器实现细节):

using System;

class Test
{
    private Func<object> CreateFunc()
    {
        return () => new object();
    }

    static void Main()
    {
        Test t = new Test();
        var f1 = t.CreateFunc();
        var f2 = t.CreateFunc();
        Console.WriteLine(ReferenceEquals(f1, f2));
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您将 lambda 表达式更改为() => this;它会打印 False。