一:背景

1. 講故事

前面幾篇文章説的都是對進程採集 snapshot 文件,但這種方式的前提需要在目標機器上運行 DotMemory 相關組件,這在很多生產環境下很難做到,我知道很多醫療,金融生產環境,部署一個外來文件都需要層層審批,尤其像 dotmemory 這種商業軟件,想上去門到沒有。。。

目前主流的做法就是生成dump文件拿到線下分析,如果 dotmemory 不集成這塊生態,那就是自絕於天地,接下來我們就來一起研究下。

二:分析轉儲文件

1. 測試代碼

為了方便演示,我故意模擬一個內存分配的快進快出案例,即在一個方法內分配大量的臨時對象,觀察內存的上漲情況,參考代碼如下:

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

    static void ProcessMemoryAllocation()
    {
        Console.WriteLine("開始分配內存...");

        // 分配 100MB 數組
        long bytes100MB = (long)100 * 1024 * 1024;
        byte[] array100MB = new byte[bytes100MB];
        array100MB[0] = 0x01;
        array100MB[array100MB.Length - 1] = 0xFF;
        Console.WriteLine($"✓ 100MB 數組分配成功: {bytes100MB:N0} 字節");

        // 分配 500MB 數組
        long bytes500MB = (long)500 * 1024 * 1024;
        byte[] array500MB = new byte[bytes500MB];
        array500MB[0] = 0x02;
        array500MB[array500MB.Length - 1] = 0xFE;
        Console.WriteLine($"✓ 500MB 數組分配成功: {bytes500MB:N0} 字節");

        // 分配 1GB 數組
        long bytes1GB = (long)1024 * 1024 * 1024;
        byte[] array1GB = new byte[bytes1GB];
        array1GB[0] = 0x03;
        array1GB[array1GB.Length - 1] = 0xFD;
        Console.WriteLine($"✓ 1GB 數組分配成功: {bytes1GB:N0} 字節");

        long totalBytes = bytes100MB + bytes500MB + bytes1GB;
        Console.WriteLine($"分配完成!");
        Console.WriteLine($"總計分配: {totalBytes:N0} 字節");
        Console.WriteLine($"約 {totalBytes / (1024.0 * 1024.0):F2} MB");
        Console.WriteLine($"約 {totalBytes / (1024.0 * 1024.0 * 1024.0):F2} GB");

    }
}

程序運行起來之後,可以看到當前程序吃了 1.7G 的內存,接下來用 process explorer 抓一個dump,截圖如下:

使用dotnet-dump分析dotnet轉儲文件_碎片化

2. 使用 dotmemory 分析

點擊 Import Process Dump 按鈕,找到要分析的文件,稍等之後就能看到快速檢測台,這裏要注意一下,dump文件越大,稍等的時間就越長,也可能等了幾十分鐘後結果爆了個內存不足,也是無語了,它的底層是通過一個單獨的 MemoryDumpConverter.exe 程序來抽取託管堆數據的,截圖如下:

使用dotnet-dump分析dotnet轉儲文件_生產環境_02

由於程序吃了 1.7G,所以要觀察下到底是誰吃掉了,觀察 Overview 視圖,截圖如下:

使用dotnet-dump分析dotnet轉儲文件_數組_03

從卦中可以看到 .NET 使用了 1.59G 內存,其中活對象是 105k ,看到這個數據很容易讓人聯想到上一篇説的 碎片化問題, 那到底是不是呢?

3. 是堆碎片化嗎

進入 Inspections 視圖,觀察 Heap Fragmentation 區域,發現 LOH 上的 free=1.59G,這就讓人很無語了,截圖如下:

使用dotnet-dump分析dotnet轉儲文件_數組_04

既然整條的 segment 都是 free,為什麼gc不在上一階段完整的回收它呢?帶着忐忑不安的心情發現是上圖中有一串數字 1.59GB occupied by 76 unreachable objects,啊,,, 原來是待回收的垃圾對象呀,但這個對象目前還不是 free 對象呀,為什麼要把它當作 free 處理呢? 這種處理方式和傳統的 windbg 處理模式完全不一樣,需要適應一樣,哈哈。

接下來點擊這 76 個不可達對象,可以看到是 3 個超大的 byte[] 給吞掉了,截圖如下:

使用dotnet-dump分析dotnet轉儲文件_生產環境_05

dump 的單個分析我們差不多搞定了,接下來研究下雙dump分析。

三. 雙 dump 增量分析

1. 測試代碼

為了能夠捕獲內存增量,修改了一下代碼,在增量的第一和第三階段各採一個dump文件,分別為 100M+ 和 1.7G+,代碼和截圖如下:

static void ProcessMemoryAllocation()
{
    Console.WriteLine("開始分配內存...");

    // 分配 100MB 數組
    long bytes100MB = (long)100 * 1024 * 1024;
    byte[] array100MB = new byte[bytes100MB];
    array100MB[0] = 0x01;
    array100MB[array100MB.Length - 1] = 0xFF;
    Console.WriteLine($"1. 100MB 數組分配成功: {bytes100MB:N0} 字節");

    Thread.Sleep(5000);

    // 分配 500MB 數組
    long bytes500MB = (long)500 * 1024 * 1024;
    byte[] array500MB = new byte[bytes500MB];
    array500MB[0] = 0x02;
    array500MB[array500MB.Length - 1] = 0xFE;
    Console.WriteLine($"2. 500MB 數組分配成功: {bytes500MB:N0} 字節");

    Thread.Sleep(5000);

    // 分配 1GB 數組
    long bytes1GB = (long)1024 * 1024 * 1024;
    byte[] array1GB = new byte[bytes1GB];
    array1GB[0] = 0x03;
    array1GB[array1GB.Length - 1] = 0xFD;
    Console.WriteLine($"3. 1GB 數組分配成功: {bytes1GB:N0} 字節");

    Thread.Sleep(5000);

    long totalBytes = bytes100MB + bytes500MB + bytes1GB;
    Console.WriteLine($"分配完成!");
    Console.WriteLine($"總計分配: {totalBytes:N0} 字節");
    Console.WriteLine($"約 {totalBytes / (1024.0 * 1024.0):F2} MB");
    Console.WriteLine($"約 {totalBytes / (1024.0 * 1024.0 * 1024.0):F2} GB");

    Console.ReadLine();
}

使用dotnet-dump分析dotnet轉儲文件_碎片化_06

這兩個dump都有了,如何比較增量呢? 做法比較簡單,先將兩個 dump 分別導入到 workspace 中,然後隨意在一個 overview 中選擇 Compare with snapshot from another workspace,即跨工作區比較,截圖如下:

使用dotnet-dump分析dotnet轉儲文件_碎片化_07

點擊完成之後,就可以看到兩個 snapshot 宏觀對比,並且在左邊區域中可以看到增量為 1.49G,截圖如下:

使用dotnet-dump分析dotnet轉儲文件_生產環境_08

鳥瞰圖並沒有解決問題,所以還要點擊右上角的 Compare 觀察堆上的對象詳情,截圖如下:

使用dotnet-dump分析dotnet轉儲文件_碎片化_09

卦中有兩個指標可以看懂。

  1. Objects delta: 快照1 (ObjectsA) 和 快照2 (ObjectB) 之間的數量增量。
  2. Bytes delta: 快照1 (BytesA) 和 快照2 (BytesB) 之間的佔用增量。

對比之後終於知道,疑似有 4 個超大的 byte[] 吃掉了 1.49G 的內存。

不過很遺憾的是,在快照對比中看不到這 4個byte[] 的具體詳情,what a pity 。。。

接下來怎麼辦呢? 只能單獨進 1.59G 的snapshot,在 Types 選項中找到 byte[] ,然後雙擊進入即可,截圖如下:

使用dotnet-dump分析dotnet轉儲文件_生產環境_10

四:總結

dotmemory 整合了dump 轉儲體系,如虎添翼,還是那句話,它是技術支持工程師的好助手。