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

时间:2017-10-26 19:40:37

标签: c# lambda memory-leaks

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

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

  1. 创建Test
  2. 类型的新对象
  3. 写入创建对象的控制台
  4. 调用垃圾回收
  5. 等待任何用户输入
  6. 关闭
  7. 我使用JetBrains DotMemory运行这个程序,我带了两个内存快照:一个在初始化对象后,另一个在收集后。我比较了快照并获得了我所期望的:一个类型为Test的死对象。

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

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

    此外,此测试+<> object - 我认为它只是暂存对象来保存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();
        }
    }
    

    DotMemory retention path report

2 个答案:

答案 0 :(得分:9)

如果你用某些东西(比如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;
        }
    }
}

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

答案 1 :(得分:4)

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

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

private static Func<object, bool> cachedT;

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

现在确实会创建一个永远不会被垃圾回收的对象,但如果经常调用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));
    }
}

但是如果将lambda表达式更改为() => this;,则会输出False。