注意:这不仅仅是一些随机无用的代码,这是尝试在C#中重现lambda表达式和内存泄漏的问题。
在C#中检查以下程序。这是一个简单的控制台应用程序:
我使用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();
}
}
答案 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。