PerfView專題 (第十一篇):使用 Diff 功能洞察 C# 記憶體泄漏增量

一:背景

去年 GC架構師 Maoni 在 (2021 .NET 開發者大會)

[//ke.segmentfault.com/course/1650000041122988/section/1500000041123017] 上演示過 PerfView 的 Diff 功能來尋找記憶體增量,個人感覺這個功能非常不錯,簡單省事,所以這裡就整合到 PerfView 專題中,分享一下給大家。

二:洞察記憶體增量

1. 什麼是記憶體增量

其實非常好理解,就是當你的程式出現了記憶體泄漏,你可以在程式記憶體增長的過程中截取兩個 dump 文件,然後通過 PerfView 觀察其中的記憶體增量是什麼? 幫助我們快速找出可能被泄漏的對象。

當然你用 WinDBG 的話也是沒有問題的,只不過需要用肉眼掃一下而已,接下來舉兩個例子說明一下。

2. 靜態集合的記憶體泄漏

很多 dump 的記憶體泄漏,源自於裡面的某一個 static 變數無限堆積所致,為了方便說明,先上一段測試程式碼。


    internal class Program
    {
        static void Main(string[] args)
        {
            Task.Run(RunTest);
            Console.ReadLine();
        }

        public static List<string> my_big_list = new List<string>();

        static void RunTest()
        {
            for (int i = 0; i < 50000; i++)
            {
                my_big_list.Add(string.Join(",", Enumerable.Range(0, 10000)));
                Console.WriteLine(i);
            }
        }
    }

接下來在程式的運行過程中,我們分別截取兩個 dump 文件, 點擊菜單欄的 Memory -> Take Heap Snapshot 按鈕,在 Filter 中搜索需要採集的進程,然後點擊 Dump GC Heap 即可,參考如下圖:

稍等片刻,你就會看到兩個 gcdump 文件,這個和普通的 dump 是不一樣的,算是 PerfView 專用的輕量級 dump 文件,截圖如下:

接下來點擊兩個 gcdump 中的 Heap Stacks,對比 inc% 列後發現,記憶體都被一個叫 my_big_list 變數給吃掉了,前者的count為 10509, 後者是 15172,截圖如下:

雖然肉眼可以簡單觀察,但這裡可以使用專業的 Diff 功能,讓 PerfView 幫我洞察 棧 的總體增量差異,點擊菜單欄中的 Diff -> With Baseline: Heap Stacks [.....] 按鈕,即讓本 gcdump 和另一個 gcdump 做比較,截圖如下:

不過要注意的是,這兩個窗口一定要打開,這個是比較坑的,哈哈,接下來就會看到如下圖:

從圖中可以清晰的看到,這兩個 dump 的增量主要來自於 my_big_list 集合,往細處說就是 string 增長了 4663 個。

3. 事件event泄漏

我們再看一個事件泄漏的例子,參考如下程式碼:


    // event 泄漏
    class Program
    {
        static event Action TestEvent;

        static void Main(string[] args)
        {
            var memory = new TestAction();

            //handle 泄漏
            for (int i = 0; i < int.MaxValue; i++)
            {
                TestEvent += memory.Run;

                if (i % 500 == 0)
                {
                    Console.WriteLine(i);
                }
            }

            Console.ReadLine();
        }

        public static void OnTestEvent()
        {
            if (TestEvent != null)
            {
                TestEvent();
            }
            else
            {
                Console.WriteLine("Test Event is null");
            }
        }

        class TestAction
        {
            public void Run()
            {
                Console.WriteLine("TestAction Run.");
            }
        }
    }

將程式運行起來,用 Process Explorer 抓兩個 dump 文件下來,然後點擊 Memory -> Take Heap Snapshot From Dump 按鈕,截圖如下:

在彈出的對話框中設置需要提取的 dump 文件,稍等片刻就會生成如下兩個 gcdump 文件,截圖如下:

接下來將兩個 gcdump 都打開,發現記憶體都被程式中的一個叫 TestEvent 佔用了,如下圖所示:

接下來就可以使用 Diff 對比功能了,可以觀察到,TestEvent 下面的 Action 增量了將近 700w 個,截圖如下:

這裡稍微說一下,為什麼會增量 700w 的 Action,這主要是因為 event 是一個多播委託,內部有一個 Action 集合,也正是這個 Action 集合 在無限膨脹。

Tags: