.NET陷阱之奇怪的OutOfMemoryException

首先我们来探讨另外一个问题:不考虑非托管内存的使用,在最坏情况下,当系统出现OutOfMemoryException异常时,有效的内存(程序中有GC Root的对象所占用的内存)使用量会是多大呢?

我们在开发过程中曾经遇到过一个奇怪的问题:当软件加载了很多比较大规模的数据后,会偶尔出现OutOfMemoryException异常,但通过内存检查工具却发现还有很多可用内存。于是我们怀疑是可用内存总量充足,但却没有足够的连续内存了——也就是说存在很多未分配的内存空隙。但不是说.NET运行时的垃圾收集器会压缩使用中的内存,从而使已经释放的内存空隙连成一片吗?于是我深入研究了一下垃圾回收相关的内容,最终明确的了问题所在——大对象堆(LOH)的使用。如果你也遇到过类似的问题或者对相关的细节有兴趣的话,就继续读读吧。

如果没有特殊说明,后面的叙述都是针对32位系统。

首先我们来探讨另外一个问题:不考虑非托管内存的使用,在最坏情况下,当系统出现OutOfMemoryException异常时,有效的内存(程序中有GC Root的对象所占用的内存)使用量会是多大呢?2G? 1G? 500M? 50M?或者更小(是不是以为我在开玩笑)?来看下面这段代码(参考 https://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/)。

1.public class Program  
2. {  
3.     static void Main(string[] args)  
4.     {  
5.         var smallBlockSize = 90000;  
6.         var largeBlockSize = 1 << 24;  
7.         var count = 0;  
8.         var bigBlock = new byte[0];  
9.         try 
10.         {  
11.             var smallBlocks = new List<byte[]>();  
12.             while (true)  
13.             {  
14.                 GC.Collect();  
15.                 bigBlock = new byte[largeBlockSize];  
16.                 largeBlockSize++;  
17.                 smallBlocks.Add(new byte[smallBlockSize]);  
18.                 count++;  
19.             }  
20.         }  
21.         catch (OutOfMemoryException)  
22.         {  
23.             bigBlock = null;  
24.             GC.Collect();  
25.             Console.WriteLine("{0} Mb allocated",   
26.                 (count * smallBlockSize) / (1024 * 1024));  
27.         }  
28.           
29.         Console.ReadLine();  
30.     }  
31. } 

这段代码不断的交替分配一个较小的数组和一个较大的数组,其中较小数组的大小为90, 000字节,而较大数组的大小从16M字节开始,每次增加一个字节。如代码第15行所示,在每一次循环中bigBlock都会引用新分配的大数组,从而使之前的大数组变成可以被垃圾回收的对象。在发生OutOfMemoryException时,实际上代码会有count个小数组和一个大小为 16M + count 的大数组处于有效状态。最后代码输出了异常发生时小数组所占用的内存总量。

下面是在我的机器上的运行结果——和你的预测有多大差别?提醒一下,如果你要亲自测试这段代码,而你的机器是64位的话,一定要把生成目标改为x86。
1.23 Mb allocated

考虑到32位程序有2G的可用内存,这里实现的使用率只有1%!

下面即介绍个中原因。需要说明的是,我只是想以最简单的方式阐明问题,所以有些语言可能并不精确,可以参考http://msdn.microsoft.com/en-us/magazine/cc534993.aspx以获得更详细的说明。

.NET的垃圾回收机制基于“Generation”的概念,并且一共有G0, G1, G2三个Generation。一般情况下,每个新创建的对象都属于于G0,对象每经历一次垃圾回收过程而未被回收时,就会进入下一个Generation(G0 -> G1 -> G2),但如果对象已经处于G2,则它仍然会处于G2中。

软件开始运行时,运行时会为每一个Generation预留一块连续的内存(这样说并不严格,但不影响此问题的描述),同时会保持一个指向此内存区域中尚未使用部分的指针P,当需要为对象分配空间时,直接返回P所在的地址,并将P做相应的调整即可,如下图所示。【顺便说一句,也正是因为这一技术,在.NET中创建一个对象要比在C或C++的堆中创建对象要快很多——当然,是在后者不使用额外的内存管理模块的情况下。】
这里写图片描述

在对某个Generation进行垃圾回收时,运行时会先标记所有可以从有效引用到达的对象,然后压缩内存空间,将有效对象集中到一起,而合并已回收的对象占用的空间,如下图所示。
这里写图片描述

但是,问题就出在上面特别标出的“一般情况”之外。.NET会将对象分成两种情况区别对象,一种是大小小于85, 000字节的对象,称之为小对象,它就对应于前面描述的一般情况;另外一种是大小在85, 000之上的对象,称之为大对象,就是它造成了前面示例代码中内存使用率的问题。在.NET中,所有大对象都是分配在另外一个特别的连续内存(LOH, Large Object Heap)中的,而且,每个大对象在创建时即属于G2,也就是说只有在进行Generation 2的垃圾回收时,才会处理LOH。而且在对LOH进行垃圾回收时不会压缩内存!更进一步,LOH上空间的使用方式也很特殊——当分配一个大对象时,运行时会优先尝试在LOH的尾部进行分配,如果尾部空间不足,就会尝试向操作系统请求更多的内存空间,只有在这一步也失败时,才会重新搜索之前无效对象留下的内存空隙。如下图所示:

这里写图片描述

从上到下看

1.LOH中已经存在一个大小为85K的对象和一个大小为16M对象,当需要分配另外一个大小为85K的对象时,会在尾部分配空间;

2.此时发生了一次垃圾回收,大小为16M的对象被回收,其占用的空间为未使用状态,但运行时并没有对LOH进行压缩;

3.此时再分配一个大小为16.1M的对象时,分尝试在LOH尾部分配,但尾部空间不足。所以,

4.运行时向操作系统请求额外的内存,并将对象分配在尾部;

5.此时如果再需要分配一个大小为85K的对象,则优先使用尾部的空间。

所以前面的示例代码会造成LOH变成下面这个样子,当最后要分配16M + N的内存时,因为前面已经没有任何一块连续区域满足要求时,所以就会引发OutOfMemoryExceptiojn异常。
这里写图片描述

要解决这一问题其实并不容易,但可以考虑下面的策略。

1.将比较大的对象分割成较小的对象,使每个小对象大小小于85, 000字节,从而不再分配在LOH上;

2.尽量“重用”少量的大对象,而不是分配很多大对象;

3.每隔一段时间就重启一下程序。

最终我们发现,我们的软件中使用数组(List)保存了一些曲线数据,而这些曲线的大小很可能会超过了85, 000字节,同时曲线对象的个数也非常多,从而对LOH造成了很大的压力,甚至出现了文章开头所描述的情况。针对这一情况,我们采用了策略1的方法,定义了一个类似C++中deque的数据结构,它以分块内存的方式存储数据,而且保证每一块的大小都小于85, 000,从而解决了这一问题。

此外要说的是,不要以为64位环境中可以忽略这一问题。虽然64位环境下有更大的内存空间,但对于操作系统来说,.NET中的LOH会提交很大范围的内存区域,所以当存在大量的内存空隙时,即使不会出现OutOfMemoryException异常,也会使得内页页面交换的频率不断上升,从而使软件运行的越来越慢。

最后分享我们定义的分块列表,它对IList接口的实现行为与List相同,代码中只给出了比较重要的几个方法。

1.public class BlockList<T> : IList<T>  
2. {  
3.     private static int maxAllocSize;  
4.     private static int initAllocSize;  
5.     private T[][] blocks;  
6.     private int blockCount;  
7.     private int[] blockSizes;  
8.     private int version;  
9.     private int countCache;  
10.     private int countCacheVersion;  
11.   
12.     static BlockList()  
13.     {  
14.         var type = typeof(T);  
15.         var size = type.IsValueType ? Marshal.SizeOf(default(T)) : IntPtr.Size;  
16.         maxAllocSize = 80000 / size;  
17.         initAllocSize = 8;  
18.     }  
19.   
20.     public BlockList()  
21.     {  
22.         blocks = new T[8][];  
23.         blockSizes = new int[8];  
24.         blockCount = 0;  
25.     }  
26.   
27.     public void Add(T item)  
28.     {  
29.         int blockId = 0, blockSize = 0;  
30.         if (blockCount == 0)  
31.         {  
32.             UseNewBlock();  
33.         }  
34.         else 
35.         {  
36.             blockId = blockCount - 1;  
37.             blockSize = blockSizes[blockId];  
38.             if (blockSize == blocks[blockId].Length)  
39.             {  
40.                 if (!ExpandBlock(blockId))  
41.                 {  
42.                     UseNewBlock();  
43.                     ++blockId;  
44.                     blockSize = 0;  
45.                 }  
46.             }  
47.         }  
48.   
49.         blocks[blockId][blockSize] = item;  
50.         ++blockSizes[blockId];  
51.         ++version;  
52.     }  
53.   
54.     public void Insert(int index, T item)  
55.     {  
56.         if (index > Count)  
57.         {  
58.             throw new ArgumentOutOfRangeException("index");  
59.         }  
60.   
61.         if (blockCount == 0)  
62.         {  
63.             UseNewBlock();  
64.             blocks[0][0] = item;  
65.             blockSizes[0] = 1;  
66.             ++version;  
67.             return;  
68.         }  
69.   
70.         for (int i = 0; i < blockCount; ++i)  
71.         {  
72.             if (index >= blockSizes[i])  
73.             {  
74.                 index -= blockSizes[i];  
75.                 continue;  
76.             }  
77.   
78.             if (blockSizes[i] < blocks[i].Length || ExpandBlock(i))  
79.             {  
80.                 for (var j = blockSizes[i]; j > index; --j)  
81.                 {  
82.                     blocks[i][j] = blocks[i][j - 1];  
83.                 }  
84.   
85.                 blocks[i][index] = item;  
86.                 ++blockSizes[i];  
87.                 break;  
88.             }  
89.   
90.             if (i == blockCount - 1)  
91.             {  
92.                 UseNewBlock();  
93.             }  
94.   
95.             if (blockSizes[i + 1] == blocks[i + 1].Length  
96.                 && !ExpandBlock(i + 1))  
97.             {  
98.                 UseNewBlock();  
99.                 var newBlock = blocks[blockCount - 1];  
100.                 for (int j = blockCount - 1; j > i + 1; --j)  
101.                 {  
102.                     blocks[j] = blocks[j - 1];  
103.                     blockSizes[j] = blockSizes[j - 1];  
104.                 }  
105.   
106.                 blocks[i + 1] = newBlock;  
107.                 blockSizes[i + 1] = 0;  
108.             }  
109.   
110.             var nextBlock = blocks[i + 1];  
111.             var nextBlockSize = blockSizes[i + 1];  
112.             for (var j = nextBlockSize; j > 0; --j)  
113.             {  
114.                 nextBlock[j] = nextBlock[j - 1];  
115.             }  
116.   
117.             nextBlock[0] = blocks[i][blockSizes[i] - 1];  
118.             ++blockSizes[i + 1];  
119.   
120.             for (var j = blockSizes[i] - 1; j > index; --j)  
121.             {  
122.                 blocks[i][j] = blocks[i][j - 1];  
123.             }  
124.   
125.             blocks[i][index] = item;  
126.             break;  
127.         }  
128.   
129.         ++version;  
130.     }  
131.   
132.     public void RemoveAt(int index)  
133.     {  
134.         if (index < 0 || index >= Count)  
135.         {  
136.             throw new ArgumentOutOfRangeException("index");  
137.         }  
138.   
139.         for (int i = 0; i < blockCount; ++i)  
140.         {  
141.             if (index >= blockSizes[i])  
142.             {  
143.                 index -= blockSizes[i];  
144.                 continue;  
145.             }  
146.   
147.             if (blockSizes[i] == 1)  
148.             {  
149.                 for (int j = i + 1; j < blockCount; ++j)  
150.                 {  
151.                     blocks[j - 1] = blocks[j];  
152.                     blockSizes[j - 1] = blockSizes[j];  
153.                 }  
154.   
155.                 blocks[blockCount - 1] = null;  
156.                 blockSizes[blockCount - 1] = 0;  
157.                 --blockCount;  
158.             }  
159.             else 
160.             {  
161.                 for (int j = index + 1; j < blockSizes[i]; ++j)  
162.                 {  
163.                     blocks[i][j - 1] = blocks[i][j];  
164.                 }  
165.   
166.                 blocks[i][blockSizes[i] - 1] = default(T);  
167.                 --blockSizes[i];  
168.             }  
169.   
170.             break;  
171.         }  
172.   
173.         ++version;  
174.     }  
175.   
176.     private bool ExpandBlock(int blockId)  
177.     {  
178.         var length = blocks[blockId].Length;  
179.         if (length == maxAllocSize)  
180.         {  
181.             return false;  
182.         }  
183.   
184.         length = Math.Min(length * 2, maxAllocSize);  
185.         Array.Resize(ref blocks[blockId], length);  
186.         return true;  
187.     }  
188.   
189.     private void UseNewBlock()  
190.     {  
191.         if (blockCount == blocks.Length)  
192.         {  
193.             Array.Resize(ref blocks, blockCount * 2);  
194.             Array.Resize(ref blockSizes, blockCount * 2);  
195.         }  
196.   
197.         blocks[blockCount] = new T[initAllocSize];  
198.         blockSizes[blockCount] = 0;  
199.         ++blockCount;  
200.     }  
201. } 

原文链接:http://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值