一:背景 上一篇我們聊到瞭如何用 PerfView 去偵察 NTHeap 的記憶體泄漏,這種記憶體泄漏往往是用 C 的 malloc 或者 C++ 的 new 分配而不釋放所造成的,這一篇我們來聊一下由 VirtualAlloc 方法造成的泄漏如何去甄別? 瞭解 VirtualAlloc 的朋友肯定說, ...
一:背景
上一篇我們聊到瞭如何用 PerfView 去偵察 NTHeap 的記憶體泄漏,這種記憶體泄漏往往是用 C 的 malloc
或者 C++ 的 new
分配而不釋放所造成的,這一篇我們來聊一下由 VirtualAlloc
方法造成的泄漏如何去甄別?
瞭解 VirtualAlloc
的朋友肯定說, C# 這種高層語言怎麼可能會用 VirtualAlloc
呢?即便是 C++
大概率也不會用這個,其實這麼說還是世面見少了,經歷的案例太少,接下來我們就來簡要聊一聊。
二: C# 中真的會用 VirtualAlloc 嗎
常規的 C# 記憶體分配確實不會直接調用 VirtualAlloc,但那些圖形圖形的工具方法肯定會直接用的,比如說 Bitmap,如果不信的話,我可以讓你眼見為實,先上一段代碼。
static void Main(string[] args)
{
for (int i = 0; i < int.MaxValue; i++)
{
Test2();
Console.WriteLine(i);
}
Console.ReadLine();
}
public static void Test2()
{
int width = 1000;
int height = 1000;
Bitmap bitmap = new Bitmap(width, height);
string path = @"D:\test\1.jpg";
bitmap.Save(path);
}
這段代碼中我會生成 1000x1000
的圖片,接下來用 bp KernelBase!VirtualAlloc
去做一個攔截。
0:009> bp KernelBase!VirtualAlloc
0:009> g
Breakpoint 0 hit
KERNELBASE!VirtualAlloc:
00007ffd`0d53f9e0 4883ec38 sub rsp,38h
0:000> k
# Child-SP RetAddr Call Site
00 00000000`001ce828 00007ffc`eaaf4483 KERNELBASE!VirtualAlloc
01 00000000`001ce830 00007ffc`eaaf35fb gdiplus!GpMemoryBitmap::AllocBitmapData+0x137
02 00000000`001ce870 00007ffc`eaacded1 gdiplus!GpMemoryBitmap::AllocBitmapMemory+0x3f
03 00000000`001ce8b0 00007ffc`eaacddf2 gdiplus!GpMemoryBitmap::InitNewBitmap+0x49
04 00000000`001ce8f0 00007ffc`eaacdf6f gdiplus!CopyOnWriteBitmap::CopyOnWriteBitmap+0x8a
05 00000000`001ce930 00007ffc`eaace074 gdiplus!GpBitmap::GpBitmap+0x6b
06 00000000`001ce970 00007ffc`357d8143 gdiplus!GdipCreateBitmapFromScan0+0xc4
07 00000000`001ce9d0 00007ffc`357e8eb1 0x00007ffc`357d8143
08 00000000`001ceaa0 00007ffc`357d3288 System_Drawing_Common!System.Drawing.Bitmap..ctor+0x31 [_/src/libraries/System.Drawing.Common/src/System/Drawing/Bitmap.cs @ 80]
09 00000000`001ceaf0 00007ffc`357d2974 ConsoleApp7!ConsoleApp7.Program.Test2+0x58 [D:\net6\ConsoleApp1\ConsoleApp7\Program.cs @ 27]
...
從輸出中可以看到在 Program.Test2
調用的過程中果然被 VirtualAlloc
攔住了,而區區 200 多個 Bitmap 就已經分配了 1G 個記憶體,截圖如下:
而此時的 GCHeap 上才區區 1.7M
。
0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000000002941030
generation 1 starts at 0x0000000002941018
generation 2 starts at 0x0000000002941000
ephemeral segment allocation context: none
segment begin allocated committed allocated size committed size
0000000002940000 0000000002941000 0000000002ADFFE8 0000000002AE2000 0x19efe8(1699816) 0x1a1000(1708032)
Large object heap starts at 0x0000000012941000
segment begin allocated committed allocated size committed size
0000000012940000 0000000012941000 0000000012941018 0000000012942000 0x18(24) 0x1000(4096)
Pinned object heap starts at 0x000000001A941000
000000001A940000 000000001A941000 000000001A949C10 000000001A952000 0x8c10(35856) 0x11000(69632)
Total Allocated Size: Size: 0x1a7c10 (1735696) bytes.
Total Committed Size: Size: 0x1a2000 (1712128) bytes.
------------------------------
GC Allocated Heap Size: Size: 0x1a7c10 (1735696) bytes.
GC Committed Heap Size: Size: 0x1a2000 (1712128) bytes.
非常明顯的非托管泄漏。
三:如何用 Perfview 檢測
perfview 是一個非常好的運行時檢測工具,它也是根據 鉤子函數 攔截後看分配量來做一個權重,最終根據權重占比尋找到問題函數調用棧。
勾選上 VirtualAlloc
之後就可以點擊 Start Collection
,5s 之後就會生成一個 統計報表
,我們點擊 Memory -> Net Virtual Alloc Stacks
選項。
在彈出面板中選擇我們的程式,點擊 CallTree
面板,清除 GroupPats
分組,截圖如下:
從面板中可以看到,在記憶體分配權重總量上,Test2()
占比 97.9%, 說明確實是一個問題,而且是初始化 Bitmap
出來的。
到這裡, VirtualAlloc 的泄漏問題就找出來了, 如果用 WinDbg 分析的話,還需要開啟 ust 選項,也是記錄線程棧,但使用起來相對繁瑣。