一:背景
1. 讲故事
上个月有位敌人通过博客园的短消息找到我,说他的程序存在内存溢出状况,寻求如何解决。
要解决还得通过 windbg 剖析啦。
二:Windbg 剖析
1. 为什么会内存溢出
大家都晓得内存溢出对应着 .NET 中的 OutOfMemoryException
异样,这种异样有可能是托管代码手工抛出的,也有可能是CLR层面抛出的,话中有话就是能够通过两种形式排查。
- 托管线程是否挂载着异样?
0:000> !tThreadCount: 23UnstartedThread: 0BackgroundThread: 5PendingThread: 0DeadThread: 17Hosted Runtime: no Lock ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 362c 00fac868 26020 Preemptive 7ED701A0:00000000 00fa6b60 0 STA 5 2 2d70 00fbeba0 2b220 Preemptive 7EBA7AC0:00000000 00fa6b60 0 MTA (Finalizer) 7 3 3264 061c8890 102a220 Preemptive 00000000:00000000 00fa6b60 0 MTA (Threadpool Worker) 17 15 3f98 19682b90 202b220 Preemptive 7EBB0830:00000000 00fa6b60 0 MTA XXXX 16 0 2845fb00 35820 Preemptive 00000000:00000000 00fa6b60 0 Ukn 18 14 a7c 2842b1c8 202b220 Preemptive 00000000:00000000 00fa6b60 0 MTA XXXX 6 0 2c9b3778 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 18 0 288a1318 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 23 0 288a22f0 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 10 0 2ccf3550 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 21 0 288a1860 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 12 0 288a1da8 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 11 0 2c993640 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 8 0 2ccf3a98 35820 Preemptive 00000000:00000000 00fa6b60 0 Ukn XXXX 9 0 2ccf2030 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 7 0 2c9aed88 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 26 0 28898308 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 25 0 2c492c68 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 4 0 2c993b88 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 20 0 2c9af2d0 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 17 0 2c9afd60 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) XXXX 24 0 2c9b1280 1039820 Preemptive 00000000:00000000 00fa6b60 0 Ukn (Threadpool Worker) 23 22 2658 2c9b02a8 1029220 Preemptive 7ED5BFF8:00000000 00fa6b60 0 MTA (Threadpool Worker)
从输入信息看,这些线程并没有挂载任何托管异样,我去。。。
- 是否在 CLR 上抛出
这次要是看 托管堆(heap)
上的内存调配或者gc回收造成的内存不足,能够用 !ao
命令。
0:000> !aoThere was no managed OOM due to allocations on the GC heap
从输入信息看也没有任何异样,难堪了。。。 尼玛,那到底是因为什么呢?
2. 摸索溢出起因
呈现这种难堪状况,我只能狐疑生成这个dump的时候并没有get到那个点,或者是我的常识边界无限,不过天无绝人之路,不在那个 点
也必定在那个 点
左近,对吧,接下来用 !address -summary
看一下内存应用的归类信息。
0:000> !address -summary--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal<unknown> 1520 4c185000 ( 1.189 GB) 65.57% 59.45%Image 4306 1f140000 ( 497.250 MB) 26.78% 24.28%Free 1133 bf17000 ( 191.090 MB) 9.33%Heap 617 7626000 ( 118.148 MB) 6.36% 5.77%Stack 72 1740000 ( 23.250 MB) 1.25% 1.14%Other 34 7b000 ( 492.000 kB) 0.03% 0.02%TEB 24 30000 ( 192.000 kB) 0.01% 0.01%PEB 1 3000 ( 12.000 kB) 0.00% 0.00%--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotalMEM_MAPPED 549 34b60000 ( 843.375 MB) 45.42% 41.18%MEM_PRIVATE 1718 20424000 ( 516.141 MB) 27.80% 25.20%MEM_IMAGE 4307 1f155000 ( 497.332 MB) 26.78% 24.28%--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotalMEM_COMMIT 4904 66ddd000 ( 1.607 GB) 88.64% 80.37%MEM_RESERVE 1670 d2fc000 ( 210.984 MB) 11.36% 10.30%MEM_FREE 1133 bf17000 ( 191.090 MB) 9.33%--- Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy %ofTotalPAGE_READONLY 2272 382cf000 ( 898.809 MB) 48.41% 43.89%PAGE_READWRITE 1572 1eead000 ( 494.676 MB) 26.64% 24.15%PAGE_EXECUTE_READ 218 dd59000 ( 221.348 MB) 11.92% 10.81%PAGE_WRITECOPY 449 133e000 ( 19.242 MB) 1.04% 0.94%PAGE_EXECUTE_READWRITE 188 ab4000 ( 10.703 MB) 0.58% 0.52%PAGE_NOACCESS 156 9c000 ( 624.000 kB) 0.03% 0.03%PAGE_READWRITE | PAGE_GUARD 48 78000 ( 480.000 kB) 0.03% 0.02%PAGE_READWRITE | PAGE_WRITECOMBINE 1 2000 ( 8.000 kB) 0.00% 0.00%--- Largest Region by Usage ----------- Base Address -------- Region Size ----------<unknown> 1d200000 a001000 ( 160.004 MB)Image fed1000 36e4000 ( 54.891 MB)Free 33dfe000 1082000 ( 16.508 MB)Heap 3da84000 a1b000 ( 10.105 MB)Stack 1a10000 fd000 (1012.000 kB)Other 7fa40000 33000 ( 204.000 kB)TEB a4c000 3000 ( 12.000 kB)PEB a3d000 3000 ( 12.000 kB)
从下面的 MEM_COMMIT=1.607 GB 80.37%
信息看,以后内存占用 1.6G
,占比 80.37%
,能够看出它受到了一个 2G内存
的限度,而且从 !t
输入中的内存地址看,以后是 32bit 程序,所以这是一个经典的: 64零碎跑着32位程序被2G内存限度 的问题。
3. 如何冲破 2G 限度
要寻找答案,还得看最权威的 MSDN: https://docs.microsoft.com/en...
破局
还得设置程序的 IMAGE_FILE_LARGE_ADDRESS_AWARE
标记。
对于具体怎么设置,我找了三种办法。
- 应用 LargeAddressAware 安装包
参见 github: https://github.com/KirillOsen...
- 应用 editbin
能够在 vs 的生成事件中输出 editbin /largeaddressaware $(TargetPath)
。
- 应用代码形式
这种能够间接给生成好的 exe 减少 LargeAddressAware
标记,除了标记,还能检测,
using System;using System.IO;namespace PEFile{ public class LargeAddressAware { public static bool IsLargeAddressAware(string filePath) { bool isLargeAddressAware = false; PrepareStream(filePath, (stream, binaryReader) => isLargeAddressAware = (binaryReader.ReadInt16() & 0x20) != 0); return isLargeAddressAware; } public static void SetLargeAddressAware(string filePath) { PrepareStream(filePath, (stream, binaryReader) => { var value = binaryReader.ReadInt16(); if ((value & 0x20) == 0) { value = (short)(value | 0x20); stream.Position -= 2; var binaryWriter = new BinaryWriter(stream); binaryWriter.Write(value); binaryWriter.Flush(); } }); } private static void PrepareStream(string filePath, Action<Stream, BinaryReader> action) { using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.Read)) { if (stream.Length < 0x3C) { return; } var binaryReader = new BinaryReader(stream); // MZ header if (binaryReader.ReadInt16() != 0x5A4D) { return; } stream.Position = 0x3C; var peHeaderLocation = binaryReader.ReadInt32(); stream.Position = peHeaderLocation; // PE header if (binaryReader.ReadInt32() != 0x4550) { return; } stream.Position += 0x12; action(stream, binaryReader); } } }}
更多方法参考: https://stackoverflow.com/que...
三:总结
总的来说,2G 内存限度
是一个 32bit 程序所必须面对的问题,晓得了就好解决了,最初有一个问题要解释下,为什么 commit 内存高达 1.6G
,这是因为医疗类的软件,大多是 FastReport + DevExpress
这些重量级的经典搭配以及大量的图片资源占用了太多 native memory。