一:背景
1. 講故事
在.NET高級(jí)調(diào)試
的旅程中,我常常會(huì)與 Bitmap 短兵相接,它最大的一個(gè)危害就是會(huì)讓程序拋出匪夷所思的 OutOfMemoryException
,也常常會(huì)讓一些.NET開(kāi)發(fā)者們陷入其中不能自拔,痛不欲生,基于此,這一篇我從dump分析的角度給大家深挖一下 Bitmap 背后的故事。
二:Bitmap 背后的故事
1. Bitmap 能吃多少內(nèi)存
相信有很多朋友都知道 bitmap 吃的是非托管內(nèi)存,但相信也有很多朋友不知道這玩意竟然能吃掉bitmap自身大小的幾十倍,甚至上百倍??赡苓@么說(shuō)有點(diǎn)抽象,舉一個(gè)例子說(shuō)明一下,用 chatgpt 生成的參考代碼如下:
static void Main(string[] args)
{
Bitmap bitmap = new Bitmap(21000, 21000);
using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.Blue);
using (Pen pen = new Pen(Color.Red, 10000))
{
g.DrawEllipse(pen, 10000, 10000, 15000, 15000);
}
using (Font font = new Font("Arial", 1600))
{
using (Brush brush = new SolidBrush(Color.White))
{
g.DrawString("Hello, Bitmap!", font, brush, new PointF(100, 700));
}
}
}
bitmap.Save("example.png", System.Drawing.Imaging.ImageFormat.Png);
Console.ReadLine();
bitmap.Dispose();
Console.WriteLine("Bitmap saved as example.png");
Debugger.Break();
Console.ReadLine();
}
在 bitmap.Dispose();
之前加上一個(gè) Console.ReadLine();
故意不銷(xiāo)毀 bitmap 來(lái)觀察下內(nèi)存消耗,真是不看不知道,一看嚇一跳,居然吃了高達(dá) 1.7G 的內(nèi)存。
接下來(lái)按一下 Enter 觀察一下 bitmap 在磁盤(pán)上的大小,居然小到無(wú)語(yǔ)的2M ,這差距咂舌的 1000 倍啊,截圖如下:
這就是 bitmap 的恐怖之處,也是很多程序員疑惑的地方。
2. Bitmap 吃的是哪里的內(nèi)存
縱然有很多朋友知道是非托管內(nèi)存,但還是有必要用數(shù)據(jù)來(lái)展示一下,這個(gè)非常簡(jiǎn)單,可以用 !address -summary
觀察下提交內(nèi)存,用 !eeheap -gc
觀察下托管堆即可。
0:006> !address -summary
--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_MAPPED 168 200`03998000 ( 2.000 TB) 88.58% 1.56%
MEM_PRIVATE 96 42`01319000 ( 264.019 GB) 11.42% 0.20%
MEM_IMAGE 265 0`03820000 ( 56.125 MB) 0.00% 0.00%
--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 73 7dbd`f7b1f000 ( 125.742 TB) 98.24%
MEM_RESERVE 83 241`94389000 ( 2.256 TB) 99.92% 1.76%
MEM_COMMIT 446 0`74148000 ( 1.814 GB) 0.08% 0.00%
0:006> !eeheap -gc
========================================
Number of GC Heaps: 1
----------------------------------------
....
------------------------------
GC Allocated Heap Size: Size: 0x1d7f8 (120824) bytes.
GC Committed Heap Size: Size: 0x45000 (282624) bytes.
從卦中可以清晰的看到 MEM_COMMIT=1.814 GB
同時(shí) GC Committed Heap Size=2.8M
,妥妥的非托管泄漏。
3. 能找到 Bitmap 所屬的內(nèi)存段嗎
要想知道 bitmap 所侵占的內(nèi)存段,如果用 windbg 去調(diào)試的話(huà),可以對(duì) KERNELBASE!VirtualAlloc
下一個(gè) bp 斷點(diǎn)即可,參考如下:
0:000> k 5
# Child-SP RetAddr Call Site
00 00000010`5257e198 00007ffb`c2ec7662 KERNELBASE!VirtualAlloc
01 00000010`5257e1a0 00007ffb`c2ec684b gdiplus!GpMemoryBitmap::AllocBitmapData+0xc6
02 00000010`5257e1e0 00007ffb`c2e8a355 gdiplus!GpMemoryBitmap::AllocBitmapMemory+0x3f
03 00000010`5257e220 00007ffb`c2e8a47a gdiplus!GpMemoryBitmap::InitNewBitmap+0x49
04 00000010`5257e260 00007ffb`c2e8a2cb gdiplus!CopyOnWriteBitmap::CopyOnWriteBitmap+0x8a
...
但可惜的是你拿到的是 dump 文件,無(wú)法使用 bp 下斷點(diǎn),那怎么辦呢?只要這輩子積攢的福報(bào)夠多,自然不會(huì)有絕人之路,首先從托管類(lèi) Bitmap 上挖起。
0:000> !DumpObj /d 000001ef0b809648
Name: System.Drawing.Bitmap
MethodTable: 00007ffa86f0cf90
EEClass: 00007ffa86f34760
Tracked Type: false
Size: 40(0x28) bytes
File: D:\code\MyCode\ConsoleApplication1\bin\x64\Debug\net8.0\System.Drawing.Common.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffa86e370a0 400019c 18 System.IntPtr 1 instance 000001EF08B222F0 _nativeImage
00007ffa86d85fa8 400019d 8 System.Object 0 instance 0000000000000000 _userData
00007ffa86fc01a8 400019e 10 System.Byte[] 0 instance 0000000000000000 _rawData
00007ffa86f0cee8 4000014 10 System.Drawing.Color 1 static 0000000000000000 s_defaultTransparentColor
從 Bitmap 的字段布局來(lái)是用 _nativeImage 字段來(lái)持有著對(duì)原生 bitmap 的引用,下面的截圖也可以佐證。
說(shuō)了這么多,其實(shí)我想表達(dá)的是什么呢?雖然我不知道 gdiplus 的底層源碼,但有一點(diǎn)可以確認(rèn)的是,VirtualAlloc 返回的 ptr 和 這里的 _nativeImage 肯定是有偏移關(guān)系的,有可能是一級(jí)關(guān)系,有可能是 二級(jí)關(guān)系,在我的內(nèi)存地址視察下,總結(jié)如下:
- 在 Windows10 x64 環(huán)境下偏移為
+0x570
。 - 在 Windows10 x86 環(huán)境下偏移為
+0x2e8
。
接下來(lái)就可以在 windbg 中輕松做驗(yàn)證,先攔截 VirtualAlloc 找到大的地址段。
0:000> bp KERNELBASE!VirtualAlloc ".if (@rdx>=0x200000) { .printf \"============ %lu bytes ================\\n\",@rdx; k } .else {gc}"
breakpoint 0 redefined
0:000> g
============ 1764000000 bytes ================
# Child-SP RetAddr Call Site
00 00000060`d9f7e7b8 00007ffb`c2ec7662 KERNELBASE!VirtualAlloc
01 00000060`d9f7e7c0 00007ffb`c2ec684b gdiplus!GpMemoryBitmap::AllocBitmapData+0xc6
02 00000060`d9f7e800 00007ffb`c2e8a355 gdiplus!GpMemoryBitmap::AllocBitmapMemory+0x3f
03 00000060`d9f7e840 00007ffb`c2e8a47a gdiplus!GpMemoryBitmap::InitNewBitmap+0x49
04 00000060`d9f7e880 00007ffb`c2e8a2cb gdiplus!CopyOnWriteBitmap::CopyOnWriteBitmap+0x8a
05 00000060`d9f7e8c0 00007ffb`c2e8a1b4 gdiplus!GpBitmap::GpBitmap+0x6b
06 00000060`d9f7e900 00007ffa`86e91f95 gdiplus!GdipCreateBitmapFromScan0+0xc4
0:000> pt
KERNELBASE!VirtualAlloc+0x5a:
00007ffb`c25df28a c3 ret
0:000> r
rax=0000020759db0000 rbx=0000000000014820 rcx=00007ffbc4acd3c4
rdx=0000000000000000 rsi=000000000026200a rdi=000001c6c4bb2d20
rip=00007ffbc25df28a rsp=00000060d9f7e7b8 rbp=0000000000005208
r8=00000060d9f7e778 r9=0000000000005208 r10=0000000000000000
r11=0000000000000246 r12=0000000000005208 r13=0000000000000004
r14=0000000000005208 r15=0000000069248100
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
KERNELBASE!VirtualAlloc+0x5a:
00007ffb`c25df28a c3 ret
0:000> !address 0000020759db0000
Usage: <unknown>
Base Address: 00000207`59db0000
End Address: 00000207`c2ff9000
Region Size: 00000000`69249000 ( 1.643 GB)
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
Type: 00020000 MEM_PRIVATE
Allocation Base: 00000207`59db0000
Allocation Protect: 00000004 PAGE_READWRITE
Content source: 1 (target), length: 69249000
從卦中可以看到分配的地址段的首地址為 0000020759db0000
,解析來(lái)到 Bitmap._nativeImage+0x570
處做個(gè)驗(yàn)證即可,可以看到遙相呼應(yīng),輸出如下:
0:000> !DumpObj /d 000001c6c7409648
Name: System.Drawing.Bitmap
MethodTable: 00007ffa86f4cf90
EEClass: 00007ffa86f74760
Tracked Type: false
Size: 40(0x28) bytes
File: D:\code\MyCode\ConsoleApplication1\bin\x64\Debug\net8.0\System.Drawing.Common.dll
Fields:
MT Field Offset Type VT Attr Value Name
00007ffa86e770a0 400019c 18 System.IntPtr 1 instance 000001C6C4BB25B0 _nativeImage
00007ffa86dc5fa8 400019d 8 System.Object 0 instance 0000000000000000 _userData
00007ffa870001a8 400019e 10 System.Byte[] 0 instance 0000000000000000 _rawData
00007ffa86f4cee8 4000014 10 System.Drawing.Color 1 static 0000000000000000 s_defaultTransparentColor
0:000> dp 000001C6C4BB25B0+0x570 L2
000001c6`c4bb2b20 00000207`59db0000 00000000`00000003
三:總結(jié)
Bitmap使用不當(dāng)危害巨大,所以一定要謹(jǐn)記 盡早釋放
的原則,如果真的不幸被吃了很多內(nèi)存,也一定要明白那些未知的大內(nèi)存段是不是被 Bitmap 所關(guān)聯(lián),從而盡早的找到真正的禍根。
?
該文章在 2024/11/15 9:23:18 編輯過(guò)