PerfView專題 (第八篇):洞察 C# 記憶體泄漏之尋找靜態變數名和GC模式

一:背景

這篇我們來聊一下 PerfView 在協助 WinDbg 分析 Dump 過程中的兩個超實用技巧,可能會幫助我們快速定位最後的問題,主要有如下兩塊:

  1. 洞察記憶體泄漏中的靜態大集合變數名。

  2. 驗證當前程式的 GC 模式。

這裡就把經驗分享一下,希望讓大家少走彎路。

二:如何洞察

1. 查看靜態變數名

如果有過 dump 分析經驗的朋友應該知道,當你歷經千辛萬苦在 記憶體泄漏 的dump文件中找到了那個記憶體泄漏最大的集合,但遺憾的是,你不知道這個 集合 的變數名叫什麼?

為了方便講述,先上一段測試程式碼:


namespace ConsoleApp10
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Task.Run(Alloc1);

            Console.ReadLine();

        }
        public static List<string> mybiglist = new List<string>();

        static void Alloc1()
        {
            var rand = new Random();

            for (int i = 0; i < 10000; i++)
            {
                mybiglist.Add(string.Join(",", Enumerable.Range(1, 1000)));
                Console.WriteLine(mybiglist.Count);
            }
        }
    }
}

接下來把程式跑起來,終於你找到了那個記憶體佔用最大的 List<string> 集合,程式碼如下:


0:000> !gcroot -all 0000000002e27038

HandleTable:
    00000000004A13E8 (strong handle)
    -> 000000001A841018 System.Object[]
    -> 000000000284D680 System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]]
    -> 0000000012841038 System.String[]
    -> 0000000002E27038 System.String

可以看到,這個變數被 HandleTable 所持有,從經驗上來說其實就是一個 static 變數,現在我們迫切需要知道這個變數名叫什麼,因為離真相真的咫尺之遙了。。。

如果你沒有彙編基礎,我敢打賭你肯定在 WinDBG 中找不到這個變數名。 那有沒有快捷的方式顯示變數名呢? 肯定是可以的,這就需要藉助 PerfView 。

接下來點擊菜單的 Memory -> Take Heap Snapshot From Dump 按鈕,彈出如下對話框,輸入 dump 文件以及 output 地址,截圖如下:

接下來點擊 Dump GC Heap 讓 PerfView 從 ConsoleApp10.dmp 中取樣生成 *.gcdump 文件,接下來點擊 Heap Stacks -> RefTree ,通過 Inc% 可以觀察到 [static vars] 下的 mybiglist 取樣佔比最大,如圖所示:

到這裡第一個問題也就解決了,原來是一個叫 mybiglistList<string> 集合把記憶體給吃掉了,是不是非常的方便哈。

2. 查看手工修改的 GC 模式

在我的 dump 分析之旅中,曾經就遇到過一個案例,需要修改 GC 模式,比如說 併發模式 改成 非併發模式,那改完之後我如何驗證呢?

第一種方式就是通過 x 命令去搜 coreclr 中的符號,比如下面這樣:


0:000> x coreclr!GCConfig*
00007ffa`782763f6 coreclr!GCConfig::s_ConcurrentGC = true
00007ffa`7827b799 coreclr!GCConfig::s_ServerGC = false

雖然可以用 WinDbg 實現,但這種需要生成 dump 或者附加到進程中,那能不能在沒有侵入的情況下獲取 CoreCLR 當前的 GC 模式呢? 肯定是可以的,這又得需要藉助 PerfView 啦, 它的底層邏輯是截獲 Runtime/Start 這個 ETW 事件,在這個事件中有一個叫 StartupFlags 枚舉,裡面就記錄著當前的 GC 模式。

為了方便講述,在 *.csproj 中修改 GC 的模式為 Server 版,程式碼如下:


<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<ServerGarbageCollection>true</ServerGarbageCollection>
		<OutputType>Exe</OutputType>
		<TargetFramework>net6.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
		<Platforms>AnyCPU;x86</Platforms>
	</PropertyGroup>
</Project>

接下來啟動 PerfView ,點擊 Collect -> Collect 啟動收集,然後把程式跑起來,停止收集後,我們在 Filter 中輸入 Runtime/Start 事件,如果你的列表中沒有 StartupFlags 列的話,記得在 Cols 上選擇一下哦,截圖如下:

從圖中可以看到,當前的 StartupFlags=8392707 ,那這一串數字代表什麼意思呢?這就需要到 CoreCLR 中找到它的枚舉定義,接下來我們寫段程式碼將它翻譯出字元串形式。


    internal class Program
    {
        static void Main(string[] args)
        {
            var value = "8392707";

            Enum.TryParse<Test>(value, out var result);

            var txt = result.ToString().Replace(", ", "\r\n");

            Console.WriteLine(txt);
        }

        [Flags]
        enum Test
        {
            STARTUP_CONCURRENT_GC = 0x1,
            STARTUP_LOADER_OPTIMIZATION_MASK = (0x3 << 1),
            STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN = (0x1 << 1),
            STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN = (0x2 << 1),
            STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST = (0x3 << 1),
            STARTUP_LOADER_SAFEMODE = 0x10,
            STARTUP_LOADER_SETPREFERENCE = 0x100,
            STARTUP_SERVER_GC = 0x1000,
            STARTUP_HOARD_GC_VM = 0x2000,
            STARTUP_SINGLE_VERSION_HOSTING_INTERFACE = 0x4000,
            STARTUP_LEGACY_IMPERSONATION = 0x10000,
            STARTUP_DISABLE_COMMITTHREADSTACK = 0x20000,
            STARTUP_ALWAYSFLOW_IMPERSONATION = 0x40000,
            STARTUP_TRIM_GC_COMMIT = 0x80000,
            STARTUP_ETW = 0x100000,
            STARTUP_ARM = 0x400000,
            STARTUP_SINGLE_APPDOMAIN = 0x800000,
            STARTUP_APPX_APP_MODEL = 0x1000000,
            STARTUP_DISABLE_RANDOMIZED_STRING_HASHING = 0x2000000
        }
    }

程式跑起來後,截圖如下:

從圖中可以清晰的看到,當前的 GC 模式為 CONCURRENT_GC & SERVER_GC,這和 WinDBG 的輸出不約而同。

好了,本篇就聊這兩個超實用的分析技巧,希望對大家有所幫助。

Tags: