前几天发布了几篇关于要小心使用 Task.Run
的文章,看了博客园的所有评论。发现有不少人在纠结示例中的现象是不是属于内存泄漏,本文分享一下我个人的看法,大家可以保留自己的意见。
在阅读本文前,如果你对 GC 分代算法还不了解,建议先阅读我的上一篇文章:小心使用 Task.Run 终篇解惑。
背景
还是先把前面两篇文章的示例贴出来:
class Program
{
static void Main(string[] args)
{
Test();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
// 程序保活
while (true)
{
Thread.Sleep(100);
}
}
static void Test()
{
var myClass = new MyClass();
myClass.Foo();
// 到这,myClass 实例不再需要了
}
}
public class MyClass
{
private int _id;
public Task Foo()
{
return Task.Run(() =>
{
Console.WriteLine($"Task.Run is executing with ID {_id}");
Thread.Sleep(100); // 模拟耗时操作
});
}
~MyClass()
{
Console.WriteLine("MyClass instance has been colleted.");
}
}
或许是我表述的问题,更或许是我把原本是一篇的文章折成了两篇发布,造成了一些误解。所以在这里我对后两篇的内容再解释一下。
有的童鞋可能误解了这个示例要演示的是什么。我演示的是,myClass
实例对象不再需要使用时,GC 在其成员被捕获的情况下能否把它回收掉。我特意用 Test()
方法包装了一下 MyClass
实例的创建和调用,当 Test()
方法执行结束时,myClass
对象则变成了不再需要使用的对象。为了保证 GC 强制回收时,myClass
对象的成员是被引用(捕捉)着的,我在 Task.Run
的匿名方法中使用了 Thread.Sleep(100)
。
如果在 while
循环内不断执行强制回收或者在强制回收前等待足够长的时间,保证 Task.Run
执行完,myClass
对象当然会被回收,因为此时它不存在被不可回收的资源捕获的成员,这点我本以为不需要示例演示大家应该也是这么认为的。如果你了解 GC 的分代算法,你关注的会是,当 myClass
对象变成不再需要使用的资源时,它能否被 GC 在 Gen 0 阶段被回收;而不是关注它最终会不会被回收。
在实际 GC 自动回收的情况下(非手动强制回收),如果第一次扫描到 myClass
发现它被其它对象引用,则会把它标记为 Gen 1,再扫描到它时就会把它标记为 Gen 2。每错过一次回收时机,在内存驻留的时间就越长,它就越难被回收。GC 进行 Root 搜索时,它是否会去搜索某个对象是有统计学基础的。
好了,现在切入正题。问:示例中的现象在 .NET 中是否属于内存泄漏?
正题
我们知道,.NET 应用程序主要使用三种类型的内存:堆栈、托管堆和非托管堆。绝大多数我们在 .NET 中使用的引用类型都是分配在托管堆上的,例如本文示例中的 myClass
对象。发生在托管堆上的内存泄漏我们可以把它称为托管内存泄漏。
关于 .NET 托管堆上的内存泄漏,我直接引用其它两篇文章的现象描述吧(文章地址在文末)。
第一篇描述的一个内存泄漏的现象是:
也说是在方法中捕获类成员的现象,和本文示例相符。如果对象不再需要使用了,你应该清除掉它“身上”的引用,以让 GC 在下一次搜索时把它回收掉。
第二篇(我的《为什么要小心使用Task.Run》文章就参考了这篇文章)是这样描述的:
和第一篇的意思差不多,也是说当对象实际上不再使用了,但因为它还被引用,GC 则不会回收它们,这种现象作者把它归为导致内存泄漏的一个主要原因。
第二篇文中还有这么一段:
翻译如下:
简单概括就是很多人认为托管内存泄漏不属于内存泄漏,这具有争议性,作者认为这是定义问题。
维基上的定义是这样的:
这个定义并没有对内存泄漏在时间上设限,请注意“由于疏忽或错误”和“不再使用”这两个重要关键词。”未能释放“是永久还是长时间?并没有明确定义。如果你要说我是在咬文嚼字,嗯,随你吧。
一个 .NET 应用,托管堆中处于 Gen 2 的未回收资源会有很多,其中基本上都是需要使用的。
而不需要再使用的资源长时间驻留在内存的托管堆上,它逃过了 Gen 0,逃过了 Gen 1,甚至逃过了 N 次 Gen 2,亦或是仅仅延迟了一点点回收时间,这是否属于内存泄漏,存在很大的争议。我认为这也是定义问题,站在操作系统的视角和.NET托管堆的视角自然会得到不一样的理解。
就像最近头条上很多人对 1=0.999...(无限循环)
这个数学问题的争议一样,有的人认为这个等式是对的,有的人认为它是错的。不同的角度,不同的定义,答案就不一样。
最后,我选择以托管堆的视角来理解,我的观点和第二篇引用文的作者一样,因编码不当导致不再需要使用的资源长时间驻留内存(延迟回收),属于内存泄漏。延迟回收也属于代码缺陷,虽然,很多场景大可不必在意这点性能。大家随意,哪种更能帮助你理解你便选择哪种。
文中链接:
[1]. http://dwz.date/d48W
[2]. http://dwz.date/d48U
附前两篇文章链接: