论.Net 2.0消失的虚拟内存空间

引言

在32位环境下,虚拟内存空间是一种宝贵的计算资源。普通情况下,4G虚拟内存中只有2G可以供应用程序使用。即便打开Windows操作系统的3GB开关,也只有接近3G的虚拟内存可用。许多在32位环境下运行的大型企业应用程序,都会面临OOM(OutOfMemory)问题。以个人的经验,.Net的GC Heap在800M以内时,系统会有较好的性能表现。当GC Heap超过800M时,GC效率即开始大幅下降。若GC Heap达到1.1G ~ 1.3G这个区间,往往就会出现OOM问题。从操作系统分配内存的角度看,OOM的原因只有一个,就是无法在VM中找到可用的连续内存空间,不能满足内存分配需求。造成这一结果的因素有很多,除了应用程序的确有重型内存分配需求这种不是原因的原因外,问题主因往往与碎片化有关。既有VM空间的碎片化,也有.Net大对象堆LOH的碎片化。在这些因素之外,.Net 2.0会导致系统总的可用VM空间莫名消失也是一个重要因素

问题现象

这是08年5月份分析的一个OOM的Case。一个大型软件,在运行时出现OOM现象。通过对内存dump文件进行分析,发现进程中加载的dll接近1000个,总尺寸合计约500M。但是通过windbg的!address命令,发现系统用于Image的开销为916M:

image

由于在整个VM空间中连续的内存块最大大小只有64K(Largest free region),因此在分配内存时发生了OOM异常。

 

我们最大的疑问在于RegionUsageImage和MEM_IMAGE两项统计的差异?从我们使用内存的方式看,都是常规的.Net Framework编程,并没有额外调用Windows API映射内存文件,何况MEM_IMAGE指示的是属于可执行映像的内存?那么除了Dll之外,还会是什么东东映射在这些内存区域呢?

 

微软有一款未公开的内部工具VAViewer,可以以图形化的方式检视内存区域:

clip_image002

上图是一个示意,不是当时原始的数据。该工具的一个特色是能以不同颜色的色块来表达内存区域的不同类型。当用VAViewer检视我们的Dll在内存中的状况时,偶然的时机,我们注意到就在该dll相邻的区域中,存在与该dll色块分布完全一致的内存(不同颜色的色块,其组合次序精确一致)。当发现这一点时,进一步检查不同的dll,几乎都能很容易地在相邻区域找到相同色块分布的内存。

 

为搞清楚这两片内存区域的数据差异,需要再次使用windbg工具,对内存数据进行详细分析。

例如: UFIDA.U9.InvTrans.InvTransBE.dll,windbg的 lm的输出如下:

image

在内存中查看UFIDA.U9.InvTrans.InvTransBE.dll对应的地址映射,如下:

image

而在内存1d440000处发现结构完全相同的一处内存映射:

image

通过.writemem命令dump出第一块和第三块内存进行比较(第二块因为是Reserve没有数据),发现仅第一块内存的0xA8偏移处的DWORD存在的差异,其它地方的内容完全相同

clip_image004

根据以上分析,可以推断.Net加载一个dll到内存中,映射了两份虚拟内存空间。其中一个被标记为RegionUsageImage,另一个则标记为RegionUsageIsVAD。两份VM空间的内容仅在标记内存使用方式的一个DWORD上有所不同。

进一步的研究

通过使用一个简单的小程序,可以轻易地复现问题。无论是静态引用,或者通过Load、LoadFrom、LoadFile来动态加载Assembly,均会出现占用两份VM的情况。唯一的例外是使用Load(Bytes[]),即将Assembly作为数据加载到内存。

 

尽管Load(Bytes[])可以规避双份VM占用的问题,但该方案有许多缺陷:

1. 加载到的内存与普通的Load完全不同,被放到了HighFrequenceHeap区域

2. 占用的VM内存全部变为Commit,而普通的Load方式有许多区域仅仅是Reserve,这会导致物理内存方面的压力

3. Load(Bytes[])只要重复调用,即可重复加载Dll,需要应用程序自行予以规避

 

以上只是初步研究过程中看到的缺陷,不排除还有更多潜在的问题,所以我们不认为这种方案有实际的可行性。

来自微软的答案

联系微软的工程师,确认了这一问题。问题的原因如下:

1. 内存的两次加载是由于分别调用了MapViewOfFile和LoadLibrary

2. 设计方案要求开发人员在LoadLibrary之前进行若干元数据方面的检查,并且由于一些其它方面的设计约束,在整个AppDomain被卸载之前,内存映射文件都不会被删除

 

如果对Windows编程有所了解的话,使用内存映射文件的方式来进行元数据的检查无疑是最快也最方便的做法。不过我们不能理解的是为什么不能在检查完毕之后,删除掉内存映射文件?尽管内存映射文件不会造成额外的物理内存压力(两份VM空间可以映射到相同的物理内存上),但毕竟减少了可用的虚拟内存空间,而这将增大OOM出现的概率。很遗憾,微软对于这个问题无法在当时的.Net版本上进行优化,但回应会在后续版本考虑改进。

后记

当.Net 4.0首次正式发布时,我们对这个问题进行了验证。不错,正如微软的工程师所承诺的那样,.Net 4.0已解决了这个问题。

 

在写这篇博文时,忽然发现.Net 2.0也已经解决了这个问题(注:.Net 3.0及.Net 3.5其内核仍然是.Net 2.0)。根据如下网页列出的.Net 2.0历次版本改进情况,应该是在2.0.50727.4448这个版本优化掉的,时间为2010-04-09日,离我们首次发现这个问题差不多整整过了两年时间:

http://blogs.msdn.com/b/dougste/archive/2007/09/06/version-history-of-the-clr-2-0.aspx

 

在32位环境下,VM内存空间是一种太宝贵的计算资源。由于.Net 2.0加载程序集实现上的一个缺陷,导致应用程序可用的VM空间莫名地消失。许多大型企业应用程序,VM空间减少300M以上可能是个常态。当时,我们通过合并dll数量和减小dll尺寸,在一定程度上控制了这个开销。所幸,随着64位计算环境的到来,这一问题迅速变得不那么重要了。

原文地址:https://www.cnblogs.com/hbzhang/p/2266172.html