.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策
.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策
我们在开发过程中曾经遇到过一个奇怪的问题:当软件加载了很多比较大规模的数据后,会偶尔出现OutOfMemoryException异常,但通过内存检查工具却发现还有很多可用内存。于是我们怀疑是可用内存总量充足,但却没有足够的连续内存了——也就是说存在很多未分配的内存空隙。但不是说.NET运行时的垃圾收集器会压缩使用中的内存,从而使已经释放的内存空隙连成一片吗?于是我深入研究了一下垃圾回收相关的内容,最终明确的了问题所在——大对象堆(LOH)的使用。如果你也遇到过类似的问题或者对相关的细节有兴趣的话,就继续读读吧。
如果没有特殊说明,后面的叙述都是针对32位系统。
首先我们来探讨另外一个问题:不考虑非托管内存的使用,在最坏情况下,当系统出现OutOfMemoryException异常时,有效的内存(程序中有GC Root的对象所占用的内存)使用量会是多大呢?2G? 1G? 500M? 50M?或者更小(是不是以为我在开玩笑)?来看下面这段代码(参考 https://HdhCmsTestsimple-talk测试数据/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。
23 Mb allocated
考虑到32位程序有2G的可用内存,这里实现的使用率只有1%!
下面即介绍个中原因。需要说明的是,我只是想以最简单的方式阐明问题,所以有些语言可能并不精确,可以参考http://msdn.microsoft测试数据/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的尾部进行分配,如果尾部空间不足,就会尝试向操作系统请求更多的内存空间,只有在这一步也失败时,才会重新搜索之前无效对象留下的内存空隙。如下图所示:
从上到下看
LOH中已经存在一个大小为85K的对象和一个大小为16M对象,当需要分配另外一个大小为85K的对象时,会在尾部分配空间; 此时发生了一次垃圾回收,大小为16M的对象被回收,其占用的空间为未使用状态,但运行时并没有对LOH进行压缩; 此时再分配一个大小为16.1M的对象时,分尝试在LOH尾部分配,但尾部空间不足。所以, 运行时向操作系统请求额外的内存,并将对象分配在尾部; 此时如果再需要分配一个大小为85K的对象,则优先使用尾部的空间。所以前面的示例代码会造成LOH变成下面这个样子,当最后要分配16M + N的内存时,因为前面已经没有任何一块连续区域满足要求时,所以就会引发OutOfMemoryExceptiojn异常。
要解决这一问题其实并不容易,但可以考虑下面的策略。
将比较大的对象分割成较小的对象,使每个小对象大小小于85, 000字节,从而不再分配在LOH上; 尽量“重用”少量的大对象,而不是分配很多大对象; 每隔一段时间就重启一下程序。最终我们发现,我们的软件中使用数组(List<float>)保存了一些曲线数据,而这些曲线的大小很可能会超过了85, 000字节,同时曲线对象的个数也非常多,从而对LOH造成了很大的压力,甚至出现了文章开头所描述的情况。 针对这一情况,我们采用了策略1的方法,定义了一个类似C++中deque的数据结构,它以分块内存的方式存储数据,而且保证每一块的大小都小于85, 000,从而解决了这一问题。
此外要说的是,不要以为64位环境中可以忽略这一问题。虽然64位环境下有更大的内存空间,但对于操作系统来说,.NET中的LOH会提交很大范围的内存区域,所以当存在大量的内存空隙时,即使不会出现OutOfMemoryException异常,也会使得内页页面交换的频率不断上升,从而使软件运行的越来越慢。
最后分享我们定义的分块列表,它对IList<T>接口的实现行为与List<T>相同,所以略去了注释。
View Code
1 [Serializable]
2 public class BlockList<T> : IList<T>
3 {
4 private static int maxAllocSize;
5
6 private static int initAllocSize;
7
8 private T[][] blocks;
9
10 private int blockCount;
11
12 private int [] blockSizes;
13
14 private int version;
15
16 private int countCache;
17
18 private int countCacheVersion;
19
20 static BlockList()
21 {
22 var type = typeof (T);
23 var size = type.IsValueType ? Marshal.SizeOf( default (T)) : IntPtr.Size;
24 maxAllocSize = 80000 / size;
25 initAllocSize = 8 ;
26 }
27
28 public BlockList()
29 {
30 Reset();
31 }
32
33 public BlockList(IEnumerable<T> collection)
34 : this ()
35 {
36 AddRange(collection);
37 }
38
39 public int Count
40 {
41 get
42 {
43 if (version != countCacheVersion)
44 {
45 countCache = 0 ;
46 for ( int i = 0 ; i < blockCount; ++ i)
47 {
48 countCache += blockSizes[i];
49 }
50
51 countCacheVersion = version;
52 }
53
54 return countCache;
55 }
56 }
57
58 bool ICollection<T> .IsReadOnly
59 {
60 get
61 {
62 return false ;
63 }
64 }
65
66 public int BlockCount
67 {
68 get
69 {
70 return blockCount;
71 }
72 }
73
74 public T this [ int index]
75 {
76 get
77 {
78 if (index < 0 )
79 {
80 throw new ArgumentOutOfRangeException( " index " );
81 }
82
83 for ( int i = 0 ; i < blockCount; ++ i)
84 {
85 if (index < blockSizes[i])
86 {
87 return blocks[i][index];
88 }
89
90 index -= blockSizes[i];
91 }
92
93 throw new ArgumentOutOfRangeException( " index " );
94 }
95 set
96 {
97 if (index < 0 )
98 {
99 throw new ArgumentOutOfRangeException( " index " );
100 }
101
102 for ( int i = 0 ; i < blockCount; ++ i)
103 {
104 if (index < blockSizes[i])
105 {
106 blocks[i][index] = value;
107 ++ version;
108 return ;
109 }
110
111 index -= blockSizes[i];
112 }
113
114 throw new ArgumentOutOfRangeException( " index " );
115 }
116 }
117
118 public void Reset()
119 {
120 blocks = new T[ 8 ][];
121 blockSizes = new int [ 8 ];
122 blockCount = 0 ;
123 }
124
125 public T[] GetBlockBuffer( int blockId)
126 {
127 return blocks[blockId];
128 }
129
130 public int GetBlockSize( int blockId)
131 {
132 return blockSizes[blockId];
133 }
134
135 public void Add(T item)
136 {
137 int blockId = 0 , blockSize = 0 ;
138 if (blockCount == 0 )
139 {
140 UseNewBlock();
141 }
142 else
143 {
144 blockId = blockCount - 1 ;
145 blockSize = blockSizes[blockId];
146 if (blockSize == blocks[blockId].Length)
147 {
148 if (! ExpandBlock(blockId))
149 {
150 UseNewBlock();
151 ++ blockId;
152 blockSize = 0 ;
153 }
154 }
155 }
156
157 blocks[blockId][blockSize] = item;
158 ++ blockSizes[blockId];
159 ++ version;
160 }
161
162 public void AddRange(IEnumerable<T> collection)
163 {
164 if (collection == null )
165 {
166 throw new ArgumentNullException( " collection " );
167 }
168
169 foreach ( var item in collection)
170 {
171 Add(item);
172 }
173
174 ++ version;
175 }
176
177 public void Clear()
178 {
179 Array.Clear(blocks, 0 , blocks.Length);
180 Array.Clear(blockSizes, 0 , blockSizes.Length);
181 blockCount = 0 ;
182 ++ version;
183 }
184
185 public bool Contains(T item)
186 {
187 return IndexOf(item) != - 1 ;
188 }
189
190 public void CopyTo(T[] array, int arrayIndex)
191 {
192 if (array == null )
193 {
194 throw new ArgumentNullException( " array " );
195 }
196
197 if (arrayIndex < 0 || arrayIndex + Count > array.Length)
198 {
199 throw new ArgumentException( " arrayIndex " );
200 }
201
202 for ( int i = 0 ; i < blockCount; ++ i)
203 {
204 Array.Copy(blocks[i], 0 , array, arrayIndex, blockSizes[i]);
205 arrayIndex += blockSizes[i];
206 }
207 }
208
209 public int IndexOf(T item)
210 {
211 var comparer = EqualityComparer<T> .Default;
212 for ( int i = 0 ; i < Count; ++ i)
213 {
214 if (comparer.Equals( this [i], item))
215 {
216 return i;
217 }
218 }
219
220 return - 1 ;
221 }
222
223 public void Insert( int index, T item)
224 {
225 if (index > Count)
226 {
227 throw new ArgumentOutOfRangeException( " index " );
228 }
229
230 if (blockCount == 0 )
231 {
232 UseNewBlock();
233 blocks[ 0 ][ 0 ] = item;
234 blockSizes[ 0 ] = 1 ;
235 ++ version;
236 return ;
237 }
238
239 for ( int i = 0 ; i < blockCount; ++ i)
240 {
241 if (index >= blockSizes[i])
242 {
243 index -= blockSizes[i];
244 continue ;
245 }
246
247 if (blockSizes[i] < blocks[i].Length || ExpandBlock(i))
248 {
249 for ( var j = blockSizes[i]; j > index; -- j)
250 {
251 blocks[i][j] = blocks[i][j - 1 ];
252 }
253
254 blocks[i][index] = item;
255 ++ blockSizes[i];
256 break ;
257 }
258
259 if (i == blockCount - 1 )
260 {
261 UseNewBlock();
262 }
263
264 if (blockSizes[i + 1 ] == blocks[i + 1 ].Length
265 && !ExpandBlock(i + 1 ))
266 {
267 UseNewBlock();
268 var newBlock = blocks[blockCount - 1 ];
269 for ( int j = blockCount - 1 ; j > i + 1 ; -- j)
270 {
271 blocks[j] = blocks[j - 1 ];
272 blockSizes[j] = blockSizes[j - 1 ];
273 }
274
275 blocks[i + 1 ] = newBlock;
276 blockSizes[i + 1 ] = 0 ;
277 }
278
279 var nextBlock = blocks[i + 1 ];
280 var nextBlockSize = blockSizes[i + 1 ];
281 for ( var j = nextBlockSize; j > 0 ; -- j)
282 {
283 nextBlock[j] = nextBlock[j - 1 ];
284 }
285
286 nextBlock[ 0 ] = blocks[i][blockSizes[i] - 1 ];
287 ++blockSizes[i + 1 ];
288
289 for ( var j = blockSizes[i] - 1 ; j > index; -- j)
290 {
291 blocks[i][j] = blocks[i][j - 1 ];
292 }
293
294 blocks[i][index] = item;
295 break ;
296 }
297
298 ++ version;
299 }
300
301 public bool Remove(T item)
302 {
303 int index = IndexOf(item);
304 if (index >= 0 )
305 {
306 RemoveAt(index);
307 ++ version;
308 return true ;
309 }
310
311 return false ;
312 }
313
314 public void RemoveAt( int index)
315 {
316 if (index < 0 || index >= Count)
317 {
318 throw new ArgumentOutOfRangeException( " index " );
319 }
320
321 for ( int i = 0 ; i < blockCount; ++ i)
322 {
323 if (index >= blockSizes[i])
324 {
325 index -= blockSizes[i];
326 continue ;
327 }
328
329 if (blockSizes[i] == 1 )
330 {
331 for ( int j = i + 1 ; j < blockCount; ++ j)
332 {
333 blocks[j - 1 ] = blocks[j];
334 blockSizes[j - 1 ] = blockSizes[j];
335 }
336
337 blocks[blockCount - 1 ] = null ;
338 blockSizes[blockCount - 1 ] = 0 ;
339 -- blockCount;
340 }
341 else
342 {
343 for ( int j = index + 1 ; j < blockSizes[i]; ++ j)
344 {
345 blocks[i][j - 1 ] = blocks[i][j];
346 }
347
348 blocks[i][blockSizes[i] - 1 ] = default (T);
349 -- blockSizes[i];
350 }
351
352 break ;
353 }
354
355 ++ version;
356 }
357
358 public void RemoveRange( int index, int count)
359 {
360 if (index < 0 || index + count > Count)
361 {
362 throw new ArgumentException();
363 }
364
365 for ( var i = 0 ; i < count; ++ i)
366 {
367 RemoveAt(index);
368 }
369 }
370
371 public IEnumerator<T> GetEnumerator()
372 {
373 return new Enumerator<T>( this );
374 }
375
376 IEnumerator IEnumerable.GetEnumerator()
377 {
378 return new Enumerator<T>( this );
379 }
380
381 private bool ExpandBlock( int blockId)
382 {
383 var length = blocks[blockId].Length;
384 if (length == maxAllocSize)
385 {
386 return false ;
387 }
388
389 length = Math.Min(length * 2 , maxAllocSize);
390 Array.Resize( ref blocks[blockId], length);
391 return true ;
392 }
393
394 private void UseNewBlock()
395 {
396 if (blockCount == blocks.Length)
397 {
398 Array.Resize( ref blocks, blockCount * 2 );
399 Array.Resize( ref blockSizes, blockCount * 2 );
400 }
401
402 blocks[blockCount] = new T[initAllocSize];
403 blockSizes[blockCount] = 0 ;
404 ++ blockCount;
405 }
406
407 [Serializable]
408 private struct Enumerator<U> : IEnumerator<U>
409 {
410 private BlockList<U> list;
411
412 private int index;
413
414 private U current;
415
416 private int version;
417
418 internal Enumerator(BlockList<U> blockList)
419 {
420 list = blockList;
421 index = 0 ;
422 version = list.version;
423 current = default (U);
424 }
425
426 public void Dispose()
427 {
428 }
429
430 public bool MoveNext()
431 {
432 if (version == list.version && index < list.Count)
433 {
434 current = list[index];
435 index++ ;
436 return true ;
437 }
438
439 return MoveNextRare();
440 }
441
442 private bool MoveNextRare()
443 {
444 if (version != list.version)
445 {
446 throw new InvalidOperationException();
447 }
448
449 index = list.Count + 1 ;
450 current = default (U);
451 return false ;
452 }
453
454 public U Current
455 {
456 get
457 {
458 return this .current;
459 }
460 }
461
462 object IEnumerator.Current
463 {
464 get
465 {
466 if (index == 0 || index == list.Count + 1 )
467 {
468 throw new InvalidOperationException();
469 }
470
471 return Current;
472 }
473 }
474
475 void IEnumerator.Reset()
476 {
477 if (version != list.version)
478 {
479 throw new InvalidOperationException();
480 }
481
482 index = 0 ;
483 current = default (U);
484 }
485 }
486 }
.NET陷阱
.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策
摘要: 我们在开发过程中曾经遇到过一个奇怪的问题:当软件加载了很多比较大规模的数据后,会偶尔出现OutOfMemoryException异常,但通过内存检查工具却发现还有很多可用内存。于是我们怀疑是可用内存总量充足,但却没有足够的连续内存了——也就是说存在很多未分配的内存空隙。但不是说.NET运行时的垃圾收集器会压缩使用中的内存,从而使已经释放的内存空隙连成一片吗?于是我深入研究了一下垃圾回收相关的内容,最终明确的了问题所在——大对象堆(LOH)的使用。如果你也遇到过类似的问题或者对相关的细节有兴趣的话,就继续读读吧。如果没有特殊说明,后面的叙述都是针对32位系统。首先我们来探讨另外一个问题:不考虑非 阅读全文
posted @ 2013-04-16 20:42 Bruce Bi 阅读(225) | 评论 (3) 编辑
.NET陷阱之四:事件监听带来的问题与弱监听器
摘要: 大家可能都遇到过没有取消事件监听而带来的一些问题,像内存泄露、访问无效数据等。当我们写下如下代码时:source.StateChanged += observer.SourceStateChangedHandler实际上source会保持有对observer的一个引用,所以如果source的生命期长于observer的话,则当其它地方不引用observer时,如果不显示解除监听,则observer不会被垃圾回收。这可能会带来两个问题:其一,如果observer占用了大量内存的话,则这部分内存不会被释放;其二,程序的其它地方可能已经处于不一致的状态,这样当source.StateChanged事 阅读全文
posted @ 2013-04-08 18:43 Bruce Bi 阅读(876) | 评论 (4) 编辑
.NET陷阱之三:“正确”使用控件也会造成内存泄露
摘要: 在我们的代码中,有时会在控件中添加对数据对象的引用。比如使用树节点的Tag属性保存相应的对象,以便在界面操作中能简单的进行访问。因为其它地方不会引用这些数据,所以我们期望在控件被销毁时,垃圾回收机制能回收相应的内存。但当软件运行了一段时间后,内存使用量会变得非常大。下面是简化后的示例代码: 1 using System; 2 using System.Windows.Forms; 3 4 namespace MemoryLeak 5 { 6 public class MainForm : Form 7 { 8 private Button holderButt... 阅读全文
posted @ 2013-04-03 11:21 Bruce Bi 阅读(112) | 评论 (1) 编辑
.NET陷阱之二:行为诡异的光标
摘要: 我们的软件中需要很多自定义的光标,以便在用户交互过程中进行必要的提示。我们开始的做法是将光标放到资源文件中,然后用类似下面的代码加载:var cursor = new Cursor(new MemoryStream(Resource.OpenHandIcon));... ...if (useDefaultCursor){ control.Cursor = Cursors.Default;}else{ control.Cursor = cursor;}但在测试过程中应该显示自定义光标时,总是时而替换成功,时而替换不成功。原来是.NET中提供的Cursor类的问题,Cursor的构造函... 阅读全文
posted @ 2013-04-02 17:11 Bruce Bi 阅读(81) | 评论 (1) 编辑
.NET陷阱之一:IDeserializationCallback带来的问题
摘要: 代码中有一个类,其中包含一个字典(Dictionary<Key, Value>),本来想让前者实现IDeserializationCallback接口,以便在反序列化时根据字典的内容做一些初始化工作,结果循环字典元素的代码就是不走。费了好大劲才找到原因,先来看有问题的代码: 1 using System; 2 using System.Collections.Generic; 3 using System.IO; 4 using System.Runtime.Serialization; 5 using System.Runtime.Serialization.Formatters 阅读全文
posted @ 2013-04-01 17:40 Bruce Bi 阅读(45) | 评论 (0) 编辑
分类: .NET陷阱
标签: .NET , LOH , OutOfMemoryException , 大对象堆
作者: Leo_wl
出处: http://HdhCmsTestcnblogs测试数据/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息查看更多关于.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策的详细内容...