OutOfMemoryException和对象锁定
众所周知,在CLR中,内存管理是由垃圾收集器(GC)完成的。当GC在新对象的预分配内存块(GC堆)中找不到内存,并且无法从操作系统预订足够的内存来扩展GC堆时,它抛出OutOfMemoryException(OOM)。
问题
我不时听到关于OOM的抱怨——人们分析代码并监控内存使用情况,发现有时当有足够的可用内存时,他们的.NET应用程序会抛出OOM。在我见过的大多数情况下,问题是:
- 操作系统的虚拟地址空间是碎片。这通常是由应用程序中的某些非托管组件引起的。这个问题在非托管世界中存在很长一段时间,但它可能会对GC造成严重影响。GC堆以段为单位进行管理,在V1.0和V1.1中,工作站版本的GC堆大小为16MB,服务器版本为32MB。这意味着当CLR需要扩展GC堆时,它必须为服务器应用程序找到32MB连续的可用虚拟内存。在用户模式下,对于2GB地址空间的系统,这通常不是问题。但是,如果应用程序中有一些非托管dll不小心操作虚拟内存,则虚拟地址空间可能会被划分为可用内存和保留内存的小块。因此,GC将无法找到足够大的空闲内存,尽管总的空闲内存足够。这种问题可以通过查看整个虚拟地址空间来找出哪个块被哪个组件保留。
- GC堆本身是碎片化的,这意味着GC不能在已经保留的段中分配对象,而这些段实际上有足够的空闲空间。我想在这个博客里关注这个问题。
GC堆一瞥
通常托管堆不应该出现碎片问题,因为在GC期间堆是压缩的。下面显示了一个过于简单化的CLR GC堆模型:
- 所有对象彼此相邻;堆的顶部是可用空间。
|---------|
|free |
|_________|
|Object B |
| |
|_________|
|Object A |
|_________|
| ... |
- 新对象在空闲空间中分配。分配总是发生在顶部,就像堆栈一样。
|---------|
|free |
|_________|
|Object C |
|_________|
|Object B |
| |
|_________|
|Object A |
|_________|
| ... |
- 当空闲空间用完时,会发生GC。在GC期间,标记可访问的对象。
|---------|
|Object C | (marked)
|_________|
|Object B |
| |
|_________|
|Object A | (marked)
|_________|
| ... |
- 在GC之后,堆被压缩,活动(可访问)对象被重新定位,死(不可访问)对象被清除。
|---------|
|free |
|_________|
|Object C |
|_________|
|Object A |
|_________|
| ... |
GC堆中的可用空间
在上面的模型中,您可以看到GC实际上在对堆进行碎片整理方面做得很好。可用空间始终位于堆的顶部,可用于新的分配。但在实际生产中,空闲空间可以驻留在分配的对象之间。这是因为:
- 有时,GC可以选择在不必要时不压缩堆的一部分。由于重新定位所有对象的成本可能很高,因此在某些情况下,GC可能会避免这样做。在这种情况下,GC将在堆中保留一个可用空间列表,以备将来压缩。这不会导致堆碎片化,因为GC完全控制可用空间。GC可以在以后任何时候在必要时填充这些块。
- 固定对象不能移动。因此,如果一个固定对象在GC中幸存下来,它可能会创建一个可用空间块,如下所示:
GC前:GC后:
|---------| |---------|
|Object C | (pinned, reachable) |Object C | (pinned)
|_________| |_________|
|Object B | (unreachable) | free |
| | | |
|_________| |_________|
|Object A | (reachable) |Object A |
|_________| |_________|
| ... | | ... |
如何锁定分割GC堆
如果应用程序一直以这种模式固定对象:固定一个新对象,执行一些分配,固定另一个对象,执行一些分配。。。并且所有固定的对象长时间保持固定,会产生大量的自由空间,如下图所示:
- 锁定新对象
|---------|
|free |
|_________|
|Pinned 1 |
|_________|
|Object A |
|_________|
| ... |
- 分配后,另一个对象被锁定
|---------|
|free |
|_________|
|Pinned 2 |
|_________|
| ... |
|_________|
|Pinned 1 |
|_________|
|Object A |
|_________|
| ... |
- 更多的对象被锁定,中间有未锁定的对象
|_________|
|Pinned n |
|_________|
| ... |
|_________|
|Pinned 2 |
|_________|
| ... |
|_________|
|Pinned 1 |
|_________|
|Object A |
|_________|
| ... |
- GC发生了,因为无法重新定位固定的对象,所以堆中仍有可用空间
|_________|
|Pinned n |
|_________|
| free |
|_________|
|Pinned 2 |
|_________|
| free |
|_________|
|Pinned 1 |
|_________|
| free |
|_________|
| ... |
这样的进程可以创建一个具有大量空闲插槽的GC堆。这些空闲槽被部分重用用于分配,但是当它们太小或者剩余的空间太小时,只要对象被固定住,GC就不能使用它们。这将阻止GC有效地使用堆,并可能最终导致OOM。
有一点使情况更糟的是,尽管开发人员可能不直接使用固定对象,但有些.Net库在幕后使用它们,比如异步IO。例如在V1.0和V1.0中传递到缓冲区接受器Socket被库固定,以便非托管代码可以访问缓冲区。考虑一个socket服务器应用程序,它每秒处理数千个socket请求,由于连接速度慢,每个请求可能需要几分钟的时间;GC堆可能会因大量的固定对象和较长的生存期而碎片化,有些对象被钉住;那么OOM就可能发生。
如何诊断问题
要确定GC堆是否碎片化,SOS是最好的工具。Sos.dll是.NET framework附带的调试器扩展,它可以检查CLR中的某些基础数据结构。例如,“DumpHeap”可以遍历GC堆并转储堆中的每个对象,如下所示:
0:000>!dumpheap
Address MT Size
00a71000 0015cde812Free
00a7100c 0015cde812Free
00a71018 0015cde812Free
00a71024 5ba583286800a71068 5ba583806800a710ac 5ba584306800a710f0 5ba5dba468...
00a91000 5ba88bd8206400a91810 0019fe482032Free
00a92000 5ba88bd8409600a93000 0019fe488192Free
00a95000 5ba88bd84096...
total1892objects
Statistics:
MT Count TotalSize Class Name
5ba7607c1 12System.Security.Permissions.HostProtectionResource
5ba75d541 12System.Security.Permissions.SecurityPermissionFlag
5ba61f181 12System.Collections.CaseInsensitiveComparer
...
0015cde86 10260Free
5ba57bf8318 18136System.String
...
- 上一篇: 仅通过转储来排除内存泄漏
- 下一篇: 使用Java中的InputStream读取文件数据