.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 終篇解惑

相關文章
相關標籤/搜索