好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策

.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策

.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策

我们在开发过程中曾经遇到过一个奇怪的问题:当软件加载了很多比较大规模的数据后,会偶尔出现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。

 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的尾部进行分配,如果尾部空间不足,就会尝试向操作系统请求更多的内存空间,只有在这一步也失败时,才会重新搜索之前无效对象留下的内存空隙。如下图所示:

从上到下看

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://www.cnblogs.com/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策的详细内容...

  阅读:41次

上一篇: 嵌套类

下一篇:backbone 之事件(events)