.NET 内存泄漏的争议

前几天发布了几篇关于要当心使用 Task.Run 的文章,看了博客园的全部评论。发现有很多人在纠结示例中的现象是否是属于内存泄漏,本文分享一下我我的的见解,你们能够保留本身的意见。算法

在阅读本文前,若是你对 GC 分代算法还不了解,建议先阅读个人上一篇文章:当心使用 Task.Run 终篇解惑app

背景

仍是先把前面两篇文章的示例贴出来:性能

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.");
    }
}

或许是我表述的问题,更或许是我把本来是一篇的文章折成了两篇发布,形成了一些误解。因此在这里我对后两篇的内容再解释一下。this

有的童鞋可能误解了这个示例要演示的是什么。我演示的是,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 中是否属于内存泄漏?code

正题

咱们知道,.NET 应用程序主要使用三种类型的内存:堆栈托管堆非托管堆。绝大多数咱们在 .NET 中使用的引用类型都是分配在托管堆上的,例如本文示例中的 myClass 对象。发生在托管堆上的内存泄漏咱们能够把它称为托管内存泄漏对象

关于 .NET 托管堆上的内存泄漏,我直接引用其它两篇文章的现象描述吧(文章地址在文末)。内存

第一篇[1]描述的一个内存泄漏的现象是:

If the reference is stored in a field reference in the class where the method is declared, it’s not so smart, since it’s impossible to determine whether it will be reused later on, or at least very very hard. If this data structure becomes unnecessary, you should clear the reference you’re holding to it so that GC will pick it up later.

也说是在方法中捕获类成员的现象,和本文示例相符。若是对象再也不须要使用了,你应该清除掉它“身上”的引用,以让 GC 在下一次搜索时把它回收掉。

第二篇[2](个人《为何要当心使用Task.Run》文章就参考了这篇文章)是这样描述的:

There are 2 related core causes for memory leaks. The first core cause is when you have objects that are still referenced but are effectually unused. Since they are referenced, the GC won’t collect them and they will remain forever, taking up memory. This can happen, for example, when you register to events but never unregister. Let’s call this a managed memory leak.

和第一篇的意思差很少,也是说当对象实际上再也不使用了,但由于它还被引用,GC 则不会回收它们,这种现象做者把它归为致使内存泄漏的一个主要缘由。

第二篇[2]文中还有这么一段:

Many share the opinion that managed memory leaks are not memory leaks at all since they are still referenced and theoretically can be de-allocated. It’s a matter of definition and my point of view is that they are indeed memory leaks. They hold memory that can’t be allocated for another instance and will eventually cause an out-of-memory exception.

翻译以下:

不少人都认为,托管内存泄漏根本不是内存泄漏,由于它们仍然被引用,理论上能够去分配。这是一个定义的问题,个人观点是,它们确实是内存泄漏。它们持有的内存没法分配给另外一个实例,最终可能会形成内存溢出异常。

简单归纳就是不少人认为托管内存泄漏不属于内存泄漏,这具备争议性,做者认为这是定义问题。

维基上的定义是这样的:

内存泄漏(Memory leak)是在计算机科学中,因为疏忽或错误形成程序未能释放已经再也不使用的内存。

这个定义并无对内存泄漏在时间上设限,请注意“因为疏忽或错误”和“再也不使用”这两个重要关键词。”未能释放“是永久仍是长时间?并无明肯定义。若是你要说我是在咬文嚼字,嗯,随你吧。

一个 .NET 应用,托管堆中处于 Gen 2 的未回收资源会有不少,其中基本上都是须要使用的。

不须要再使用的资源长时间驻留在内存的托管堆上,它逃过了 Gen 0,逃过了 Gen 1,甚至逃过了 N 次 Gen 2,这是否属于内存泄漏,存在很大的争议。我认为这也是定义问题,站在操做系统的视角和托管堆“分代”的视角天然会获得不同的理解。

就像最近头条上不少人对 1=0.999...(无限循环)这个数学问题的争议同样,有的人认为这个等式是对的,有的人认为它是错的。

最后,我选择以托管堆的视角来理解,个人观点和第二篇引用文的做者同样,因编码不当致使再也不须要使用的资源长时间驻留内存(延迟回收),属于内存泄漏。延迟回收也属于代码缺陷,虽然,不少场景大可没必要在乎这点性能。你们随意,哪一种更能帮助你理解你便选择哪一种。

文中连接:

[1]. http://dwz.date/d48W

[2]. http://dwz.date/d48U

附前两篇文章连接:

当心使用 Task.Run 续篇

当心使用 Task.Run 终篇解惑

相关文章
相关标签/搜索