问题简介
Out of memory异常是如何产生的
总的来说OutOfMemoryException会在两种情况下发生,
-
进程虚拟内存空间耗尽
-
系统物理内存耗尽
第二种情况我们可以参照系统进程管理器中性能选项卡,如果其中committed数值接近了limit,那说明第二种情况发生了。
不过大多数时候OutOfMemoryException发生是因为第一种原因,接下来我们将重点研究虚拟地址空间耗尽的问题。
无论机器插了多少内存条,32位操作系统可以寻址4GB的地址空间,如果系统没有打开3GB开关的话,其中2GB分配给操作系统内核,另外2GB分配给用户程序。内核的2GB空间被所有的进程,操作系统所共享。但用户模式的那2GB空间为每个进程独享。
我们来简单描述一下CLR内存分配的方式,当应用程序需要分配内存空间的时候,CLR会分配一段连续空间64MB给应用程序使用,如果应用程序需要分配的内存大于85000byte,那么这部分内存会在大对象堆中分配,如果大对象堆中没有这么大的连续内存空间,则为大对象堆分配新的一段16MB。如果一直这样分配下去的话,总会达到一个上限无法满足连续空间的分配请求,因为我们只有2GB的用户模式内存空间。这样outof memory异常就会发生了。当然我们没有讨论垃圾回收器会不停的回收内存,整理压缩空间。
主要两大原因会使进程虚拟内存耗尽,
- 程序分配的速度大于内存回收速度
- 虚拟内存地址空间太多碎片,减少了连续空间分配的可能性
通常情况下,当32位应用程序虚拟内存使用了800M以上的时候,发生out of memory的几率会大大增加,因为内存碎片难以避免,连续内存空间可能所剩不多,这也是为什么会发现在内存使用率增高的情况下性能会逐步下降,因为垃圾回收的几率也在提高。
问题重现
接下来我们来看一个out ofmemory的例子。
using System;
using System.Collections.ObjectModel;
namespace DemoOOM
{
class Program
{
Collection<byte[]> roots = new Collection<byte[]>();
static void Main(string[] args)
{
try
{
Program p = new Program();
p.AllocateSmallObjects();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.Read();
}
void AllocateSmallObjects()
{
byte[] smallObject;
int i = 1;
while (true)
{
smallObject = new byte[80*1024];
Console.WriteLine("allocate {0} 80K object", i);
roots.Add(smallObject);
i++;
}
}
}
}
22809* 80KB = 1781MB = 1.73GB,可以看到即使我们连续分配内存,碎片很少的情况下依然只能使用1.73左右的内存空间,进程需要空间分配给加载的dll文件,大对象堆,还有进程默认堆,线程堆栈,所有我们只能在接近2GB的时候发生OOM。
调试方法
接下来我们介绍一个OOM收集数据的基本方法
配置性能计数器(performance counter)
新建一个性能计数的日志收集,加入以下性能技术
•Process/PrivateBytes
•Process/VirtualBytes
•.NET CLRMemory/All counters
•.NET CLRLoading/All counters
更改注册表
添加如下注册表键值,使得在OOM产生的时候系统会让程序马上抛出一个breakpointexception,导致程序崩溃以便我们抓取dump。
.NET framework 1.1
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\
DWord: GCFailFastOnOOM
Value: 2
.NET framework 2.0及以上
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\GCBreakOnOOM
Dword: GCBreakOnOOM
Value: 2
配置DebugDiag
-
安装 DebugDiag然后打开
-
点击添加规则,选择crash继续
-
选择All IIS Processes然后选择Advanced Exception Configuration
-
选择Add Exception - Breakpoint Exception
-
ActionType选择Full UserDump
-
保存之后关闭
-
继续添加Advanced Breakpoint Configuration
-
AddBreakpoint之后输入KERNEL32!ExitProcess
-
ActionType选择Full UserDump
-
保存后下一步继续
还有关键的一步,切换到Processes选项卡,右键单击w3wp进程选择Monitor For Leaks,这是为了在进程中注入一个LeakTrace的Dll,有了它我们可以追踪非托管内存的分配情况,监测泄露。
问题发生之后memorydump会自动生成在debugdiag配置好的目录下面,同时停掉性能计数器。数据收集完毕。
如何去查看OOM的dump
我们已经知道发生了OOM异常,通过加载psscor2调试扩展,调用!dumpallexceptions来查看发生的所有的异常信息,发现OutOfMemoryException数量为2。注意这里有三个异常会一直列在这里,如果没有发生过该异常,他们的数量为1。
- System.ExecutionEngineException
- System.StackOverflowException
- System.OutOfMemoryException
原因是系统为了防止在这三种情况下没有机会创建相应的异常,所以先在堆里面创建好了三个备用。
0:000>!dae
Going todump the .NET Exceptions found in the heap.
Number ofexceptions of this type: 1
ExceptionMethodTable: 7856106c
Exceptionobject: 027510b4
Exceptiontype: System.ExecutionEngineException
Message:<none>
InnerException:<none>
StackTrace(generated):
<none>
StackTraceString:<none>
HResult:80131506
-----------------
Number ofexceptions of this type: 1
ExceptionMethodTable: 78560fdc
Exceptionobject: 0275106c
Exceptiontype: System.StackOverflowException
Message:<none>
InnerException:<none>
StackTrace(generated):
<none>
StackTraceString:<none>
HResult:800703e9
-----------------
Number of exceptions ofthistype: 2
ExceptionMethodTable: 78560f4c
Exceptionobject: 7e742848
Exception type:System.OutOfMemoryException
Message:<none>
InnerException:<none>
StackTrace(generated):
SP IP Function
002FF0B8 004F00F8DemoOOM.Program.AllocateSmallObjects()
002FF0D0 004F0092DemoOOM.Program.Main(System.String[])
StackTraceString:<none>
HResult:8007000e
-----------------
通过address命令来看内存的使用情况,可以看到
1.程序几乎用光了2G地址空间
2.内存中最大的free region只有15.3M,满足不了64M的分配需求。
0:000>!address -summary
--- UsageSummary ---------------- RgnCount ----------- Total Size -------- %ofBusy%ofTotal
<unknown> 270 76475000 ( 1.848 Gb) 97.52% 92.41%
Free 67 6b54000 ( 107.328 Mb) 5.24%
Image 204 29e0000 ( 41.875 Mb) 2.16% 2.04%
Heap 17 300000 ( 3.000 Mb) 0.15% 0.15%
Stack 17 300000 ( 3.000 Mb) 0.15% 0.15%
Other 10 43000 ( 268.000 kb) 0.01% 0.01%
TEB 3 3000 ( 12.000 kb) 0.00% 0.00%
PEB 1 1000 ( 4.000 kb) 0.00% 0.00%
--- TypeSummary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy%ofTotal
MEM_PRIVATE 265 74c48000 ( 1.824 Gb) 96.27% 91.23%
MEM_IMAGE 231 2c2d000 ( 44.176 Mb) 2.28% 2.16%
MEM_MAPPED 26 1c27000 ( 28.152 Mb) 1.45% 1.37%
--- StateSummary ---------------- RgnCount ----------- Total Size -------- %ofBusy%ofTotal
MEM_COMMIT 385 754f5000 ( 1.833 Gb) 96.72% 91.65%
MEM_FREE 67 6b54000 ( 107.328 Mb) 5.24%
MEM_RESERVE 137 3fa7000 ( 63.652 Mb) 3.28% 3.11%
---Protect Summary (for commit) - RgnCount ----------- Total Size -------- %ofBusy%ofTotal
PAGE_READWRITE 203 7233e000 ( 1.784 Gb) 94.16% 89.22%
PAGE_EXECUTE_READ 34 1c78000 ( 28.469 Mb) 1.47% 1.39%
PAGE_READONLY 92 136b000 ( 19.418 Mb) 1.00% 0.95%
PAGE_WRITECOPY 29 191000 ( 1.566 Mb) 0.08% 0.08%
PAGE_EXECUTE_WRITECOPY 2 1a000 ( 104.000 kb) 0.01% 0.00%
PAGE_EXECUTE_READWRITE 18 19000 ( 100.000 kb) 0.01% 0.00%
PAGE_READWRITE|PAGE_GUARD 7 10000 ( 64.000 kb) 0.00% 0.00%
---Largest Region by Usage ----------- Base Address -------- Region Size----------
<unknown> 42ab0000 2350000 ( 35.313 Mb)
Free 72f7b000 f55000 ( 15.332 Mb)
Image 7848f000 8f4000 ( 8.953 Mb)
Heap 11c9000 d7000 (860.000 kb)
Stack c01000 fb000 (1004.000kb)
Other 7efa0000 33000 (204.000 kb)
TEB 7efd7000 1000 ( 4.000 kb)
PEB 7efde000 1000 ( 4.000 kb)
通过dumpheap命令来查看托管堆中对象,发现占用内存最多是22819个byte数组。总大小为1.78G左右。在实际的分析中,我们要关注使用内存最多的对象,同时也要格外关注占用内存最多的属于我们自己定义的命名空间的对象。
0:000>!dumpheap -stat
Using ourcache to search the heap.
...
0x7856337c 16 896 System.Collections.Hashtable
0x78561ea8 63 1,260 System.RuntimeType
0x7854cb0c 65 1,560System.Security.Policy.StrongNameMembershipCondition
0x78563478 16 2,304System.Collections.Hashtable+bucket[]
0x78561958 34 6,068 System.Char[]
0x7854c8b8 232 6,496 System.Security.SecurityElement
0x78562d58 278 6,672 System.Collections.ArrayList
0x785344f8 338 34,232 System.Object[]
0x78560d28 547 47,940 System.String
0x0034efc8 30 131,060 Free
0x78544ee8 2 131,104 System.Byte[][]
0x78563798 22,819 1,868,788,720 System.Byte[]
Total24,752 objects, Total size: 1,869,168,208
通过dumpheap加-mt参数来将所有的byte数组列出,发现byte数组分配非常的齐整,都是81,932,通过加-gen 2参数发现这些数组全部都在第二代。
为什么这么多byte数组?这是我们为了演示,但是实际中的dump我们肯定要认真思考他们来自哪里。
为什么全在第二代?接下来继续。
0:000>!dumpheap -mt 0x78563798
Using ourcache to search the heap.
Address MT Size Gen
0x0275427c0x78563798 12 2 System.Byte[]
...
0x027b92100x78563798 81,932 2 System.Byte[]
0x027cd21c0x78563798 81,932 2 System.Byte[]
0x027e12280x78563798 81,932 2 System.Byte[]
0x027f52340x78563798 81,932 2 System.Byte[]
0x028092400x78563798 81,932 2 System.Byte[]
0x0281d24c0x78563798 81,932 2 System.Byte[]
0x028312580x78563798 81,932 2 System.Byte[]
…
0:000>!dumpheap -mt 0x78563798 -gen2
Using ourcache to search the heap.
Address MT Size Gen
0x0275427c0x78563798 12 2 System.Byte[]
...
0x0277d1ec0x78563798 81,932 2 System.Byte[]
0x027911f80x78563798 81,932 2 System.Byte[]
0x027a52040x78563798 81,932 2 System.Byte[]
0x027b92100x78563798 81,932 2 System.Byte[]
0x027cd21c0x78563798 81,932 2 System.Byte[]
0x027e12280x78563798 81,932 2 System.Byte[]
0x027f52340x78563798 81,932 2 System.Byte[]
0x028092400x78563798 81,932 2 System.Byte[]
通过GCRoot命令来查看对象的根。可以看到对象最终被一个Collection<Byte[]>所引用。
0:004>!gcroot 0x027911f8
Note:Roots found on stacks may be false positives. Run "!help gcroot" for
moreinfo.
ScanThread 0 OSThread 169c
ESP:3bef1c:Root: 02a6797c(System.Collections.ObjectModel.Collection`1[[System.Byte[],mscorlib]])->
02a6798c(System.Collections.Generic.List`1[[System.Byte[],mscorlib]])->
2df0d1a4(System.Byte[][])->
27911f8(System.Byte[])
ESP:3bef30:Root: 02a6797c(System.Collections.ObjectModel.Collection`1[[System.Byte[],mscorlib]])->
02a6798c(System.Collections.Generic.List`1[[System.Byte[],mscorlib]])->
2df0d1a4(System.Byte[][])->
27911f8(System.Byte[])
ESP:3befc8:Root: 02a67970(DemoOOM.Program)->
02a6797c(System.Collections.ObjectModel.Collection`1[[System.Byte[],mscorlib]])->
02a6798c(System.Collections.Generic.List`1[[System.Byte[],mscorlib]])->
2df0d1a4(System.Byte[][])->
27911f8(System.Byte[])
ScanThread 2 OSThread 2cc8
通过objsize命令来看看这个根的大小?接近1.8G全被他占了。
0:000>!objsize 02a6797c
sizeof(0281797c)= 1,868,918,116 ( 0x6f656d64) bytes(System.Collections.ObjectModel.Collection`1[[System.Byte[], mscorlib]])
这个例子只是一个理想情况下的演示实例,现实世界中的问题不可能这么显而易见,最主要的还是要找到内存使用量最大的数据来自于哪里,存在是否合理。
内存使用量大的对象可以从堆得使用里观察(!dumpheap 带不同的参数)
这些对象来自于哪里可以根据
- 对象实例本身的数据(!do命令查看对象属性)
- 对象的根的路径(!gcroot查看对象根路径)
找到了这些对象并弄清楚他们来自何处,接下来就是要根据逻辑来判断这些对象的存在是否合理了,比如
- 分配这些对象的必要性
- 对象是否没有及时释放
最终我们把内存中不合理的存在都去除,如果还是抛OutOfMemoryException,那就考虑将程序移植到64位的系统上吧。
参考文档