.NET陷阱之奇怪的OutOfMemoryException
来源:互联网 发布:阿里云邮企业版下载 编辑:程序博客网 时间:2024/09/21 08:56
首先我们来探讨另外一个问题:不考虑非托管内存的使用,在最坏情况下,当系统出现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
- .NET陷阱之奇怪的OutOfMemoryException
- .NET陷阱之奇怪的OutOfMemoryException
- .NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策
- .NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策
- ASP.NET中的OutOfMemoryException
- ASP.NET中的OutOfMemoryException
- .Net System.OutOfMemoryException
- 调试.NET程序OutOfMemoryException
- 解決 ASP.NET 中 System.OutOfMemoryException 的問題
- 解決 ASP.NET 中 System.OutOfMemoryException 的問題
- ASP.NET 中关于 System.OutOfMemoryException 的问题与解决方法
- ASP.NET 中关于 System.OutOfMemoryException 的问题与解决方法
- 解决 ASP.NET 中 System.OutOfMemoryException 的问题
- 引发类型为“System.OutOfMemoryException”的异常-.Net 内存溢出
- OutOfMemoryException问题的处理
- OutOfMemoryException问题的处理
- .Net 内存溢出(System.OutOfMemoryException
- .Net比较奇怪的问题
- Unity3d 获取屏幕depth与normal
- 读写ini文件
- 大数据平台搭建之components版本选择
- SpringMVC返回类型
- 如何在一个辅助中修改游戏的可视距离
- .NET陷阱之奇怪的OutOfMemoryException
- Shell脚本基本命令
- 黑马程序员——反射
- git使用时遭遇the authenticity of host can't be established
- Android之异步线程原理
- MATLAB中去掉字符串中的空格,num2str引发的问题
- 大数据平台搭建之nexus私服
- java中long和double类型操作的非原子性探究
- QT5-TableWidget字符串显示