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

    从上到下看

    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<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陷阱

     
    摘要: 我们在开发过程中曾经遇到过一个奇怪的问题:当软件加载了很多比较大规模的数据后,会偶尔出现OutOfMemoryException异常,但通过内存检查工具却发现还有很多可用内存。于是我们怀疑是可用内存总量充足,但却没有足够的连续内存了——也就是说存在很多未分配的内存空隙。但不是说.NET运行时的垃圾收集器会压缩使用中的内存,从而使已经释放的内存空隙连成一片吗?于是我深入研究了一下垃圾回收相关的内容,最终明确的了问题所在——大对象堆(LOH)的使用。如果你也遇到过类似的问题或者对相关的细节有兴趣的话,就继续读读吧。如果没有特殊说明,后面的叙述都是针对32位系统。首先我们来探讨另外一个问题:不考虑非阅读全文
    posted @ 2013-04-16 20:42 Bruce Bi 阅读(225) | 评论 (3) 编辑
    摘要: 大家可能都遇到过没有取消事件监听而带来的一些问题,像内存泄露、访问无效数据等。当我们写下如下代码时:source.StateChanged += observer.SourceStateChangedHandler实际上source会保持有对observer的一个引用,所以如果source的生命期长于observer的话,则当其它地方不引用observer时,如果不显示解除监听,则observer不会被垃圾回收。这可能会带来两个问题:其一,如果observer占用了大量内存的话,则这部分内存不会被释放;其二,程序的其它地方可能已经处于不一致的状态,这样当source.StateChanged事阅读全文
    posted @ 2013-04-08 18:43 Bruce Bi 阅读(876) | 评论 (4) 编辑
    摘要: 在我们的代码中,有时会在控件中添加对数据对象的引用。比如使用树节点的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) 编辑
    摘要: 我们的软件中需要很多自定义的光标,以便在用户交互过程中进行必要的提示。我们开始的做法是将光标放到资源文件中,然后用类似下面的代码加载: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) 编辑
    摘要: 代码中有一个类,其中包含一个字典(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陷阱
  • 相关阅读:
    python开发环境安装
    python文件I/O
    python字符串方法以及注释
    python列表
    php: Can't use function return value in write context
    Notice : brew install php70
    对web开发从业者的发展方向的思考
    关于微信跨号支付
    MySQL触发器写法
    MySQL慢查询日志
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3025155.html
Copyright © 2011-2022 走看看