.NET陷阱之奇怪的OutOfMemoryException
我們?cè)陂_發(fā)過程中曾經(jīng)遇到過一個(gè)奇怪的問題:當(dāng)軟件加載了很多比較大規(guī)模的數(shù)據(jù)后,會(huì)偶爾出現(xiàn)OutOfMemoryException異常,但通過內(nèi)存檢查工具卻發(fā)現(xiàn)還有很多可用內(nèi)存。于是我們懷疑是可用內(nèi)存總量充足,但卻沒有足夠的連續(xù)內(nèi)存了——也就是說存在很多未分配的內(nèi)存空隙。但不是說.NET運(yùn)行時(shí)的垃圾收集器會(huì)壓縮使用中的內(nèi)存,從而使已經(jīng)釋放的內(nèi)存空隙連成一片嗎?于是我深入研究了一下垃圾回收相關(guān)的內(nèi)容,最終明確的了問題所在——大對(duì)象堆(LOH)的使用。如果你也遇到過類似的問題或者對(duì)相關(guān)的細(xì)節(jié)有興趣的話,就繼續(xù)讀讀吧。
如果沒有特殊說明,后面的敘述都是針對(duì)32位系統(tǒng)。
首先我們來探討另外一個(gè)問題:不考慮非托管內(nèi)存的使用,在最壞情況下,當(dāng)系統(tǒng)出現(xiàn)OutOfMemoryException異常時(shí),有效的內(nèi)存(程序中有GC Root的對(duì)象所占用的內(nèi)存)使用量會(huì)是多大呢?2G? 1G? 500M? 50M?或者更?。ㄊ遣皇且詾槲以陂_玩笑)?來看下面這段代碼(參考 https://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/)。
- public class Program
- {
- static void Main(string[] args)
- {
- var smallBlockSize = 90000;
- var largeBlockSize = 1 << 24;
- var count = 0;
- var bigBlock = new byte[0];
- try
- {
- var smallBlocks = new List<byte[]>();
- while (true)
- {
- GC.Collect();
- bigBlock = new byte[largeBlockSize];
- largeBlockSize++;
- smallBlocks.Add(new byte[smallBlockSize]);
- count++;
- }
- }
- catch (OutOfMemoryException)
- {
- bigBlock = null;
- GC.Collect();
- Console.WriteLine("{0} Mb allocated",
- (count * smallBlockSize) / (1024 * 1024));
- }
- Console.ReadLine();
- }
- }
這段代碼不斷的交替分配一個(gè)較小的數(shù)組和一個(gè)較大的數(shù)組,其中較小數(shù)組的大小為90, 000字節(jié),而較大數(shù)組的大小從16M字節(jié)開始,每次增加一個(gè)字節(jié)。如代碼第15行所示,在每一次循環(huán)中bigBlock都會(huì)引用新分配的大數(shù)組,從而使之前的大數(shù)組變成可以被垃圾回收的對(duì)象。在發(fā)生OutOfMemoryException時(shí),實(shí)際上代碼會(huì)有count個(gè)小數(shù)組和一個(gè)大小為 16M + count 的大數(shù)組處于有效狀態(tài)。最后代碼輸出了異常發(fā)生時(shí)小數(shù)組所占用的內(nèi)存總量。
下面是在我的機(jī)器上的運(yùn)行結(jié)果——和你的預(yù)測(cè)有多大差別?提醒一下,如果你要親自測(cè)試這段代碼,而你的機(jī)器是64位的話,一定要把生成目標(biāo)改為x86。
- 23 Mb allocated
考慮到32位程序有2G的可用內(nèi)存,這里實(shí)現(xiàn)的使用率只有1%!
下面即介紹個(gè)中原因。需要說明的是,我只是想以最簡(jiǎn)單的方式闡明問題,所以有些語(yǔ)言可能并不精確,可以參考http://msdn.microsoft.com/en-us/magazine/cc534993.aspx以獲得更詳細(xì)的說明。
.NET的垃圾回收機(jī)制基于“Generation”的概念,并且一共有G0, G1, G2三個(gè)Generation。一般情況下,每個(gè)新創(chuàng)建的對(duì)象都屬于于G0,對(duì)象每經(jīng)歷一次垃圾回收過程而未被回收時(shí),就會(huì)進(jìn)入下一個(gè)Generation(G0 -> G1 -> G2),但如果對(duì)象已經(jīng)處于G2,則它仍然會(huì)處于G2中。
軟件開始運(yùn)行時(shí),運(yùn)行時(shí)會(huì)為每一個(gè)Generation預(yù)留一塊連續(xù)的內(nèi)存(這樣說并不嚴(yán)格,但不影響此問題的描述),同時(shí)會(huì)保持一個(gè)指向此內(nèi)存區(qū)域中尚未使用部分的指針P,當(dāng)需要為對(duì)象分配空間時(shí),直接返回P所在的地址,并將P做相應(yīng)的調(diào)整即可,如下圖所示?!卷槺阏f一句,也正是因?yàn)檫@一技術(shù),在.NET中創(chuàng)建一個(gè)對(duì)象要比在C或C++的堆中創(chuàng)建對(duì)象要快很多——當(dāng)然,是在后者不使用額外的內(nèi)存管理模塊的情況下?!?/p>
在對(duì)某個(gè)Generation進(jìn)行垃圾回收時(shí),運(yùn)行時(shí)會(huì)先標(biāo)記所有可以從有效引用到達(dá)的對(duì)象,然后壓縮內(nèi)存空間,將有效對(duì)象集中到一起,而合并已回收的對(duì)象占用的空間,如下圖所示。
但是,問題就出在上面特別標(biāo)出的“一般情況”之外。.NET會(huì)將對(duì)象分成兩種情況區(qū)別對(duì)象,一種是大小小于85, 000字節(jié)的對(duì)象,稱之為小對(duì)象,它就對(duì)應(yīng)于前面描述的一般情況;另外一種是大小在85, 000之上的對(duì)象,稱之為大對(duì)象,就是它造成了前面示例代碼中內(nèi)存使用率的問題。在.NET中,所有大對(duì)象都是分配在另外一個(gè)特別的連續(xù)內(nèi)存(LOH, Large Object Heap)中的,而且,每個(gè)大對(duì)象在創(chuàng)建時(shí)即屬于G2,也就是說只有在進(jìn)行Generation 2的垃圾回收時(shí),才會(huì)處理LOH。而且在對(duì)LOH進(jìn)行垃圾回收時(shí)不會(huì)壓縮內(nèi)存!更進(jìn)一步,LOH上空間的使用方式也很特殊——當(dāng)分配一個(gè)大對(duì)象時(shí),運(yùn)行時(shí)會(huì)優(yōu)先嘗試在LOH的尾部進(jìn)行分配,如果尾部空間不足,就會(huì)嘗試向操作系統(tǒng)請(qǐng)求更多的內(nèi)存空間,只有在這一步也失敗時(shí),才會(huì)重新搜索之前無效對(duì)象留下的內(nèi)存空隙。如下圖所示:
從上到下看
1.LOH中已經(jīng)存在一個(gè)大小為85K的對(duì)象和一個(gè)大小為16M對(duì)象,當(dāng)需要分配另外一個(gè)大小為85K的對(duì)象時(shí),會(huì)在尾部分配空間;
2.此時(shí)發(fā)生了一次垃圾回收,大小為16M的對(duì)象被回收,其占用的空間為未使用狀態(tài),但運(yùn)行時(shí)并沒有對(duì)LOH進(jìn)行壓縮;
3.此時(shí)再分配一個(gè)大小為16.1M的對(duì)象時(shí),分嘗試在LOH尾部分配,但尾部空間不足。所以,
4.運(yùn)行時(shí)向操作系統(tǒng)請(qǐng)求額外的內(nèi)存,并將對(duì)象分配在尾部;
5.此時(shí)如果再需要分配一個(gè)大小為85K的對(duì)象,則優(yōu)先使用尾部的空間。
所以前面的示例代碼會(huì)造成LOH變成下面這個(gè)樣子,當(dāng)最后要分配16M + N的內(nèi)存時(shí),因?yàn)榍懊嬉呀?jīng)沒有任何一塊連續(xù)區(qū)域滿足要求時(shí),所以就會(huì)引發(fā)OutOfMemoryExceptiojn異常。
要解決這一問題其實(shí)并不容易,但可以考慮下面的策略。
#p#
1.將比較大的對(duì)象分割成較小的對(duì)象,使每個(gè)小對(duì)象大小小于85, 000字節(jié),從而不再分配在LOH上;
2.盡量“重用”少量的大對(duì)象,而不是分配很多大對(duì)象;
3.每隔一段時(shí)間就重啟一下程序。
最終我們發(fā)現(xiàn),我們的軟件中使用數(shù)組(List<float>)保存了一些曲線數(shù)據(jù),而這些曲線的大小很可能會(huì)超過了85, 000字節(jié),同時(shí)曲線對(duì)象的個(gè)數(shù)也非常多,從而對(duì)LOH造成了很大的壓力,甚至出現(xiàn)了文章開頭所描述的情況。針對(duì)這一情況,我們采用了策略1的方法,定義了一個(gè)類似C++中deque的數(shù)據(jù)結(jié)構(gòu),它以分塊內(nèi)存的方式存儲(chǔ)數(shù)據(jù),而且保證每一塊的大小都小于85, 000,從而解決了這一問題。
此外要說的是,不要以為64位環(huán)境中可以忽略這一問題。雖然64位環(huán)境下有更大的內(nèi)存空間,但對(duì)于操作系統(tǒng)來說,.NET中的LOH會(huì)提交很大范圍的內(nèi)存區(qū)域,所以當(dāng)存在大量的內(nèi)存空隙時(shí),即使不會(huì)出現(xiàn)OutOfMemoryException異常,也會(huì)使得內(nèi)頁(yè)頁(yè)面交換的頻率不斷上升,從而使軟件運(yùn)行的越來越慢。
最后分享我們定義的分塊列表,它對(duì)IList<T>接口的實(shí)現(xiàn)行為與List<T>相同,代碼中只給出了比較重要的幾個(gè)方法。
- public class BlockList<T> : IList<T>
- {
- private static int maxAllocSize;
- private static int initAllocSize;
- private T[][] blocks;
- private int blockCount;
- private int[] blockSizes;
- private int version;
- private int countCache;
- private int countCacheVersion;
- static BlockList()
- {
- var type = typeof(T);
- var size = type.IsValueType ? Marshal.SizeOf(default(T)) : IntPtr.Size;
- maxAllocSize = 80000 / size;
- initAllocSize = 8;
- }
- public BlockList()
- {
- blocks = new T[8][];
- blockSizes = new int[8];
- blockCount = 0;
- }
- public void Add(T item)
- {
- int blockId = 0, blockSize = 0;
- if (blockCount == 0)
- {
- UseNewBlock();
- }
- else
- {
- blockId = blockCount - 1;
- blockSize = blockSizes[blockId];
- if (blockSize == blocks[blockId].Length)
- {
- if (!ExpandBlock(blockId))
- {
- UseNewBlock();
- ++blockId;
- blockSize = 0;
- }
- }
- }
- blocks[blockId][blockSize] = item;
- ++blockSizes[blockId];
- ++version;
- }
- public void Insert(int index, T item)
- {
- if (index > Count)
- {
- throw new ArgumentOutOfRangeException("index");
- }
- if (blockCount == 0)
- {
- UseNewBlock();
- blocks[0][0] = item;
- blockSizes[0] = 1;
- ++version;
- return;
- }
- for (int i = 0; i < blockCount; ++i)
- {
- if (index >= blockSizes[i])
- {
- index -= blockSizes[i];
- continue;
- }
- if (blockSizes[i] < blocks[i].Length || ExpandBlock(i))
- {
- for (var j = blockSizes[i]; j > index; --j)
- {
- blocks[i][j] = blocks[i][j - 1];
- }
- blocks[i][index] = item;
- ++blockSizes[i];
- break;
- }
- if (i == blockCount - 1)
- {
- UseNewBlock();
- }
- if (blockSizes[i + 1] == blocks[i + 1].Length
- && !ExpandBlock(i + 1))
- {
- UseNewBlock();
- var newBlock = blocks[blockCount - 1];
- for (int j = blockCount - 1; j > i + 1; --j)
- {
- blocks[j] = blocks[j - 1];
- blockSizes[j] = blockSizes[j - 1];
- }
- blocks[i + 1] = newBlock;
- blockSizes[i + 1] = 0;
- }
- var nextBlock = blocks[i + 1];
- var nextBlockSize = blockSizes[i + 1];
- for (var j = nextBlockSize; j > 0; --j)
- {
- nextBlock[j] = nextBlock[j - 1];
- }
- nextBlock[0] = blocks[i][blockSizes[i] - 1];
- ++blockSizes[i + 1];
- for (var j = blockSizes[i] - 1; j > index; --j)
- {
- blocks[i][j] = blocks[i][j - 1];
- }
- blocks[i][index] = item;
- break;
- }
- ++version;
- }
- public void RemoveAt(int index)
- {
- if (index < 0 || index >= Count)
- {
- throw new ArgumentOutOfRangeException("index");
- }
- for (int i = 0; i < blockCount; ++i)
- {
- if (index >= blockSizes[i])
- {
- index -= blockSizes[i];
- continue;
- }
- if (blockSizes[i] == 1)
- {
- for (int j = i + 1; j < blockCount; ++j)
- {
- blocks[j - 1] = blocks[j];
- blockSizes[j - 1] = blockSizes[j];
- }
- blocks[blockCount - 1] = null;
- blockSizes[blockCount - 1] = 0;
- --blockCount;
- }
- else
- {
- for (int j = index + 1; j < blockSizes[i]; ++j)
- {
- blocks[i][j - 1] = blocks[i][j];
- }
- blocks[i][blockSizes[i] - 1] = default(T);
- --blockSizes[i];
- }
- break;
- }
- ++version;
- }
- private bool ExpandBlock(int blockId)
- {
- var length = blocks[blockId].Length;
- if (length == maxAllocSize)
- {
- return false;
- }
- length = Math.Min(length * 2, maxAllocSize);
- Array.Resize(ref blocks[blockId], length);
- return true;
- }
- private void UseNewBlock()
- {
- if (blockCount == blocks.Length)
- {
- Array.Resize(ref blocks, blockCount * 2);
- Array.Resize(ref blockSizes, blockCount * 2);
- }
- blocks[blockCount] = new T[initAllocSize];
- blockSizes[blockCount] = 0;
- ++blockCount;
- }
- }
原文鏈接:http://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html