C# 大型对象堆学习总结
生活随笔
收集整理的這篇文章主要介紹了
C# 大型对象堆学习总结
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
大型對象堆揭秘
http://blog.csdn.net/jfkidear/article/details/18358551CLR 全面透徹解析
大型對象堆揭秘
Maoni Stephens
?目錄
大型對象堆和 GC?
何時回收大型對象?
LOH 性能意義?
回收 LOH 的性能數據?
使用調試器?
CLR 垃圾回收器 (GC) 將對象分為大型、小型兩類。如果是大型對象,與其相關的一些屬性將比對象較小時顯
得更為重要。例如,壓縮大型對象(將內存復制到堆上的其他位置)的費用相當高。在本月的專欄中,我將深入探討大型對象堆。我將討論符合什么條件的對象才能稱之為大型對象,如何回收這些大型對象,以及大型對
象具備哪些性能意義。
大型對象堆和 GC
在 Microsoft? .NET Framework 1.1 和 2.0 中,如果對象大于或等于 85,000 字節,將被視為大型對象。此
數字根據性能優化的結果確定。當對象分配請求傳入后,如果符合該大小閾值,便會將此對象分配給大型對象
堆。這究竟是什么意思呢?要理解這些內容,先了解一些關于 .NET 垃圾回收器的基礎知識可能會有所幫助。
眾所周知,.NET 垃圾回收器是分代回收器。它包含三代:第 0 代、第 1 代和第 2 代。之所以分代,是因為
在良好調優的應用程序中,您可以在第 0 代清除大部分對象。例如,在服務器應用程序中,與每個請求關聯的
分配將在完成請求后清除。仍存在的分配請求將轉到第 1 代,并在那里進行清除。從本質上講,第 1 代是新
對象區域與生存期較長的對象區域之間的緩沖區。
從分代的角度來說,大型對象屬于第 2 代,因為只有在第 2 代回收過程中才能回收它們。回收一代時,同時
也會回收所有前面的代。例如,執行第 1 代垃圾回收時,將同時回收第 1 代和第 0 代。執行第 2 代垃圾回
收時,將回收整個堆。因此,第 2 代垃圾回收也稱為完整垃圾回收。在本專欄中,我將使用術語“第 2 代垃
圾回收”而不是“完整垃圾回收”,但它們可以互換。
垃圾回收器堆的各代是按邏輯劃分的。實際上,對象存在于托管堆棧段上。托管堆棧段是垃圾回收器通過調用?
VirtualAlloc 代表托管代碼在操作系統上保留的內存塊。加載 CLR 時,將分配兩個初始堆棧段(一個用于小
型對象,另一個用于大型對象),我將它們分別稱為小型對象堆 (SOH) 和大型對象堆 (LOH)。
然后,通過將托管對象置于任一托管堆棧段上來滿足分配請求。如果對象小于 85,000 字節,則將其放在 SOH?
段上;否則將其放在 LOH 段上。隨著分配到各段上的對象越來越多,會以較小塊的形式提交這些段。
對于 SOH,垃圾回收未處理的對象將進入下一代;由此第 0 代回收未處理的對象將被視為第 1 代對象,依此
類推。但是,最后一代回收未處理的對象仍會被視為最后一代中的對象。也就是說,第 2 代垃圾回收未處理的
對象仍是第 2 代對象;LOH 未處理的對象仍是 LOH 對象(由第 2 代回收)。用戶代碼只能在第 0 代(小型
對象)或 LOH(大型對象)中分配。只有垃圾回收器可以在第 1 代(通過提升第 0 代回收未處理的對象)和
第 2 代(通過提升第 1 代和第 2 代回收未處理的對象)中“分配”對象。
觸發垃圾回收后,垃圾回收器將尋找存在的對象并將它們壓縮。不過對于 LOH,由于壓縮費用很高,CLR 團隊
會選擇掃過所有對象,列出沒有被清除的對象列表以供以后重新使用,從而滿足大型對象的分配請求。相鄰的
被清除對象將組成一個自由對象。
有一點必須注意,雖然目前我們不會壓縮 LOH,但將來可能會進行壓縮。因此,如果您分配了大型對象并希望
確保它們不被移動,則應將其固定起來。
請注意,下面的圖僅用于說明。我使用了很少的對象,只為說明堆上發生的事件。實際上,還存在許多對象。
圖 1 說明了一種情況,在第一次第 0 代 GC 后形成了第 1 代,其中 Obj1 和 Obj3 被清除;在第一次第 1?
代 GC 后形成了第 2 代,其中 Obj2 和 Obj5 被清除。
圖 1 SOH 分配和垃圾回收(單擊圖像可查看大圖)
圖 2 說明在第 2 代垃圾回收后,您將看到 Obj1 和 Obj2 被清除,內存中原來存放 Obj1 和 Obj2 的空間將
成為一個可用空間,隨后可用于滿足 Obj4 的分配請求。從最后一個對象 Obj3 到此段末尾的空間仍可用于以
后的分配請求。
圖 2 LOH 分配和垃圾回收(單擊圖像可查看大圖)
如果沒有足夠的可用空間來容納大型對象分配請求,我會先嘗試從操作系統獲取更多段。如果失敗,我將觸發
第 2 代垃圾回收以便釋放一些空間。
在第 2 代垃圾回收期間,我會把握時機將不包含任何活動對象的段釋放回操作系統(通過調用 VirtualFree)
。從最后一個存在的對象到該段末尾的內存將退回。而且,盡管已重置可用空間,但仍會提交它們,這意味著
操作系統無需將其中的數據重新寫入磁盤。圖 3 說明了一種情況,我將一個段(段 2)釋放回操作系統,并在
剩下的段中退回了更多空間。如果需要使用該段末尾的已退回空間來滿足新的大型對象分配請求,我可以再次
提交該內存。
圖 3 垃圾回收期間在 LOH 上釋放的已消除段(單擊圖像可查看大圖)
有關提交/退回的說明,請參閱有關 VirtualAlloc 的 MSDN? 文檔,網址為 go.microsoft.com/fwlink/?
LinkId=116041。
何時回收大型對象
要確定何時回收大型對象,我們首先討論一下通常何時會執行垃圾回收。如果發生下列情況之一,將執行垃圾
回收:
分配超出第 0 代或大型對象閾值 大部分 GC 都是由于需在托管堆上進行分配而執行(這是最典型的情況)。
調用 System.GC.Collect 如果對第 2 代調用 GC.Collect(通過不向 GC.Collect 傳遞參數或將?
GC.MaxGeneration 作為參數傳遞),將立即回收 LOH 及其他托管堆。
系統內存太低 收到來自操作系統的高內存通知時會發生此情況。如果我認為執行第 2 代垃圾回收會有所幫助
,就會觸發一個垃圾回收。
閾值是各代的屬性。將對象分配給某代時,會增加該代的內存量,使之接近該代的閾值。當超出某代的閾值時
,便會在該代觸發垃圾回收。因此,當您分配小型或大型對象時,需要分別使用第 0 代和 LOH 的閾值。當垃
圾回收器分配到第 1 代和第 2 代中時,將使用第 1 代的閾值。運行此程序時,會動態調整這些閾值。
LOH 性能意義
下面,我們來看一下分配成本。CLR 確保清除了我提供的每個新對象的內存。這意味著大型對象的分配成本完
全由清理的內存(除非觸發了垃圾回收)決定。如果需要兩輪才能清除 1 個字節,則意味著需要 170,000 輪
才能清除最小的大型對象。這對于分配較大的大型對象的人們來說很平常。對于 2GHz 計算機上的 16MB 對象
,大約需要 16ms 才能清除內存。這些成本相當大。
現在我們來看一下回收成本。前面曾提到,LOH 和第 2 代將一起回收。如果超過兩者中任何一個的閾值,都會
觸發第 2 代回收。如果由于第 2 代為 LOH 而觸發了第 2 代回收,則第 2 代本身在垃圾回收后不一定會變得
更小。因此,如果第 2 代中的數據不多,這將不是問題。但是,如果第 2 代很大,則觸發多次第 2 代垃圾回
收可能會產生性能問題。如果要臨時分配許多大型對象,并且您擁有一個大型 SOH,則運行垃圾回收可能會花
費很長時間;毫無疑問,如果仍繼續分配和處理真正的大型對象,分配成本肯定會大幅增加。
LOH 上的特大對象通常是數組(很少會有非常大的實例對象)。如果數組元素包含很多引用,則成本將會很高
。如果元素不包含任何引用,則根本無需處理此數組。例如,如果使用數組存儲二進制樹中的節點,一種實現
方法是按實際節點引用某個節點的左側節點和右側節點:
?class Node
{
? ? Data d;
? ? Node left;
? ? Node right;
};
Node[] binary_tr = new Node [num_nodes];
如果 num_nodes 很大,則意味著至少需要對每個元素處理兩個引用。另一種方法是存儲左側節點和右側節點的
索引:
?class Node
{
? ? Data d;
? ? uint left_index;
? ? uint right_index;
};
這樣,您可將左側節點的數據作為 binary_tr[left_index].d 引用,而非作為 left.d 引用;而垃圾回收器無
需查看左側節點和右側節點的任何引用。
在這三個回收原因中,通常前兩個比第三個出現得多。因此,最好能夠分配一個大型對象池并重新使用這些對
象,而不是分配臨時對象。Yun Jin 在其博客日志 (go.microsoft.com/fwlink/?LinkId=115870) 中介紹了一
個此類緩沖池的示例。當然,您可能希望增加緩沖區大小。
回收 LOH 的性能數據
可以通過某些方法來回收與 LOH 相關的性能數據。不過,在介紹它們之前,我們先談論一下為什么要進行回收
。
在開始回收特定區域的性能數據前,希望您已經找到需查看此區域的原因,或您已查看了其他已知區域但未發
現任何問題可解釋您需要解決的性能問題。
有關詳細解釋,建議您閱讀我的博客日志(請參見 go.microsoft.com/fwlink/?LinkId=116467)。在日志中,
我介紹了內存和 CPU 的基礎知識。另外,2006 年 11 月期刊中的“CLR 全面透徹解析”針對內存問題進行了
調查,介紹了在托管過程中診斷可能與托管堆相關的性能問題涉及的步驟(請參見
msdn2.microsoft.com/magazine/cc163528)。
.NET CLR 內存性能計數器通常是調查性能問題的第一步。與 LOH 相關的計數器顯示第 2 代回收的數目和大型
對象堆的大小。第 2 代回收的數目顯示了自回收過程開始執行第 2 代垃圾回收的次數。計數器會在第 2 代垃
圾回收(也稱為完整垃圾回收)結束時遞增。此計數器顯示最后看到的值。
大型對象堆大小指的是大型對象堆的當前大小(以字節為單位,包括可用空間)。此計數器將在垃圾回收結束
時更新,而不是在每次分配時更新。
查看性能計數器的常用方法是使用性能監視器 (PerfMon.exe)。使用“添加計數器”可為您關注的過程添加感
興趣的計數器,如圖 4 所示。
圖 4 在性能監視器中添加計數器(單擊圖像可查看大圖)
您可以將性能計數器數據保存在性能監視器的日志文件中,也可以編程方式查詢性能計數器。大部分人在例行
測試過程中都采用此方式進行收集。如果發現計數器顯示的值不正常,則可以使用其他方法獲得更多詳細信息
以幫助調查。
使用調試器
在開始之前,請注意我此部分提及的調試命令僅適用于 Windows? 調試器。如果需要查看 LOH 上實際存在的對
象,您可以使用 CLR 提供的 SoS 調試器擴展,在前面提到的 2006 年 11 月期刊中已對此進行了介紹。圖 5
中顯示了分析 LOH 的輸出示例。
圖 5 中的加粗部分顯示 LOH 堆的大小為 (16,754,224 + 16,699,288 + 16,284,504 =) 49,738,016 個字節。
而在 023e1000 和 033db630 之間,System.Object[] 對象占用了 8,008,736 個字節;System.Byte[] 對象占
用了 6,663,696 個字節;可用空間占用了 2,081,792 個字節。
? ?圖 5 LOH 輸出
?0:003> .loadby sos mscorwks
0:003> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x013e35ec
generation 1 starts at 0x013e1b6c
generation 2 starts at 0x013e1000
ephemeral segment allocation context: none
?segment ? ?begin allocated ? ? size
0018f2d0 790d5588 ?790f4b38 0x0001f5b0(128432)
013e0000 013e1000 ?013e35f8 0x000025f8(9720)
Large object heap starts at 0x023e1000
?segment ? ?begin allocated ? ? size
023e0000 023e1000 ?033db630 0x00ffa630(16754224)
033e0000 033e1000 ?043cdf98 0x00fecf98(16699288)
043e0000 043e1000 ?05368b58 0x00f87b58(16284504)
Total Size ?0x2f90cc8(49876168)
------------------------------
GC Heap Size ?0x2f90cc8(49876168)
0:003> !dumpheap -stat 023e1000 ?033db630
total 133 objects
Statistics:
? ? ? MT ? ?Count ? ?TotalSize Class Name
001521d0 ? ? ? 66 ? ? ?2081792 ? ? ?Free
7912273c ? ? ? 63 ? ? ?6663696 System.Byte[]
7912254c ? ? ? ?4 ? ? ?8008736 System.Object[]
Total 133 objects
有時,您會看到 LOH 的總大小少于 85,000 個字節。為什么會這樣?這是因為運行時本身實際使用 LOH 分配
某些小于大型對象的對象。
由于不會壓縮 LOH,有時人們會懷疑 LOH 是碎片源。事實上,在得出這個結論前,您最好先弄清什么是碎片。
有一種托管堆碎片,由托管對象之間的可用空間量指示(換句話說,在 SoS 中執行 !dumpheap –type Free?
時看到的內容);還有虛擬內存 (VM) 地址空間碎片,即標記為 MEM_FREE 的內存以及在 windbg 中使用各種
調試器命令可看到的內容(請參見 go.microsoft.com/fwlink/?LinkId=116470)。圖 6 顯示了虛擬內存空間
中的碎片(請注意圖中的加粗文本)。
? ?圖 6 VM 空間碎片
?0:000> !address
? ? 00000000 : 00000000 - 00010000
? ? ? ? ? ? ? ? ? ? Type ? ? 00000000?
? ? ? ? ? ? ? ? ? ? Protect ?00000001 PAGE_NOACCESS
? ? ? ? ? ? ? ? ? ? State ? ?00010000 MEM_FREE
? ? ? ? ? ? ? ? ? ? Usage ? ?RegionUsageFree
? ? 00010000 : 00010000 - 00002000
? ? ? ? ? ? ? ? ? ? Type ? ? 00020000 MEM_PRIVATE
? ? ? ? ? ? ? ? ? ? Protect ?00000004 PAGE_READWRITE
? ? ? ? ? ? ? ? ? ? State ? ?00001000 MEM_COMMIT
? ? ? ? ? ? ? ? ? ? Usage ? ?RegionUsageEnvironmentBlock
? ? 00012000 : 00012000 - 0000e000
? ? ? ? ? ? ? ? ? ? Type ? ? 00000000?
? ? ? ? ? ? ? ? ? ? Protect ?00000001 PAGE_NOACCESS
? ? ? ? ? ? ? ? ? ? State ? ?00010000 MEM_FREE
? ? ? ? ? ? ? ? ? ? Usage ? ?RegionUsageFree
... [omitted]
-------------------- Usage SUMMARY --------------------------
? ? TotSize ( ? ? ?KB) ? Pct(Tots) Pct(Busy) ? Usage
? ? ?701000 ( ? ?7172) : 00.34% ? ?20.69% ? ?: RegionUsageIsVAD
? ?7de15000 ( 2062420) : 98.35% ? ?00.00% ? ?: RegionUsageFree
? ? 1452000 ( ? 20808) : 00.99% ? ?60.02% ? ?: RegionUsageImage
? ? ?300000 ( ? ?3072) : 00.15% ? ?08.86% ? ?: RegionUsageStack
? ? ? ?3000 ( ? ? ?12) : 00.00% ? ?00.03% ? ?: RegionUsageTeb
? ? ?381000 ( ? ?3588) : 00.17% ? ?10.35% ? ?: RegionUsageHeap
? ? ? ? ? 0 ( ? ? ? 0) : 00.00% ? ?00.00% ? ?: RegionUsagePageHeap
? ? ? ?1000 ( ? ? ? 4) : 00.00% ? ?00.01% ? ?: RegionUsagePeb
? ? ? ?1000 ( ? ? ? 4) : 00.00% ? ?00.01% ? ?: RegionUsageProcessParametrs
? ? ? ?2000 ( ? ? ? 8) : 00.00% ? ?00.02% ? ?: RegionUsageEnvironmentBlock
? ? ? ?Tot: 7fff0000 (2097088 KB) Busy: 021db000 (34668 KB)
-------------------- Type SUMMARY --------------------------
? ? TotSize ( ? ? ?KB) ? Pct(Tots) ?Usage
? ?7de15000 ( 2062420) : 98.35% ? : <free>
? ? 1452000 ( ? 20808) : 00.99% ? : MEM_IMAGE
? ? ?69f000 ( ? ?6780) : 00.32% ? : MEM_MAPPED
? ? ?6ea000 ( ? ?7080) : 00.34% ? : MEM_PRIVATE
-------------------- State SUMMARY --------------------------
? ? TotSize ( ? ? ?KB) ? Pct(Tots) ?Usage
? ? 1a58000 ( ? 26976) : 01.29% ? : MEM_COMMIT
? ?7de15000 ( 2062420) : 98.35% ? : MEM_FREE
? ? ?783000 ( ? ?7692) : 00.37% ? : MEM_RESERVE
Largest free region: Base 01432000 - Size 707ee000 (1843128 KB)
前面曾提到,托管堆上的碎片用于分配請求。通??吹降母嗍怯膳R時大型對象導致的虛擬內存碎片,需要頻
繁進行垃圾回收以便從操作系統獲取新的托管堆段,并將空托管堆段釋放回操作系統。
要驗證 LOH 是否會生成 VM 碎片,可在 VirtualAlloc 和 VirtualFree 上設置一個斷點,查看是誰調用了它
們。例如,如果想知道誰曾嘗試從操作系統分配大于 8MB 的 VM 塊,可按以下方式設置斷點:
?bp kernel32!virtualalloc "j (dwo(@esp+8)>800000) 'kb';'g'"
如果調用 VirtualAlloc 時分配大小大于 8MB (0x800000),此代碼會中斷調試器并顯示調用堆棧,否則不會中
斷調試器。
在 CLR 2.0 中,我們添加了名為 VM Hoarding 的功能,如果需要經常獲取和釋放段(包括用于大型對象堆和
小型對象堆兩者的段),則可以使用此功能。要指定 VM Hoarding 功能,請通過宿主 API 指定名為?
STARTUP_HOARD_GC_VM 的啟動標志(請參見 go.microsoft.com/fwlink/?LinkId=116471)。指定此標志后,只
會退回這些段上的內存并將其添加到備用列表中,而不會將該空段釋放回操作系統。備用列表上的段以后可用
于滿足新的段請求。因此,下次需要新段時,如果可以從此備用列表找到足夠大的段,便可以使用它。
請注意,對于太大的段,該功能不起作用。此功能還可供某些應用程序用以承載其已獲得的段,如一些服務器
應用程序,它們會盡可能避免生成 VM 空間碎片以防出現內存不足錯誤。由于它們通常是計算機上的主應用程
序,所以可以執行這些操作。強烈建議您在使用此功能時認真測試您的應用程序,以確保內存使用情況比較穩
定。
大型對象費用很高。由于 CLR 需要清除一些新分配大型對象的內存,以滿足 CLR 清除所有新分配對象內存的
保證,所以分配成本相當高。LOH 將與堆的其余部分一起回收,所以請仔細分析這會對您的應用程序性能造成
什么影響。如果可以,建議重新使用大型對象以避免托管堆和 VM 空間中生成碎片。
最后,到目前為止,在回收過程中尚不能壓縮 LOH,但不應依賴于此實現詳情。因此,要確保某些內容未被 GC?
移動,請始終將其固定起來。現在,請利用您剛學到的 LOH 知識對堆進行控制。
請將您的問題和意見發送至 clrinout@microsoft.com。
Maoni Stephens 是 Microsoft 的 CLR 團隊中研究垃圾回收器的高級開發人員。在加入 CLR 前,Maoni 已在?
Microsoft 操作系統組工作多年。
========
C# 內存管理優化暢想(一)---- 大對象堆(LOH)的壓縮
http://www.tuicool.com/articles/b22iqiI原文 ?http://www.cnblogs.com/ygc369/p/4861610.html
主題 C#
我們都知道,.net的GC是不會壓縮大對象堆的,因為其時間開銷不可接受,但這是以大對象堆產生大塊碎片為
代價的,如果以后要分配的大對象比最大的碎片還大,那么即使它比所有碎片的總大小要小,也是無法在不擴
展大對象堆的前提下分配成功的,此時有可能引發內存不足的異常。
我想到一個方案,可以讓大對象堆也能壓縮,而且時間開銷在可接受的范圍內,原理是利用頁表。我們知道,
程序能看到的內存地址都是虛擬地址,是通過頁表映射到物理地址的,連續的虛擬地址對應的物理地址未必連
續,反之亦然。在內存中移動大量數據,開銷很大,因為數據真的要在物理內存上復制,但如果我們不動物理
內存上的數據,只修改頁表及其緩存TLB,即修改了物理地址與虛擬地址的映射關系,開銷就會小得多,而且對
于應用程序來說,同樣達到了內存移動的效果。(物理內存上沒有數據移動,但對象的虛擬地址卻變了,對應
用程序來說,這就是數據移動了!)
當然,如果要用這種方法實現壓縮大對象堆,也會有一些局限性:比如每個大對象必須占據整數頁的空間,且
大對象的起始地址必須是某頁的起始地址,這樣大對象之間會出現一些小碎片(不會超過一頁的大小,即不超
過4K,與85K以上的大對象本身相比,還是很小的),但小碎片總比大碎片好呀,就看怎么權衡了,而且這些小
碎片也是可以被利用的,比如可以把一些大小合適的2代小對象存儲到這些小碎片中,以節約小對象堆的空間。
PS: 現在的一些虛擬機軟件的實現似乎就使用了類似的方法,以達到提高效率的目的。
該問題的英文討論貼:https://github.com/dotnet/coreclr/issues/555
========
C#:.NET陷阱之五:奇怪的OutOfMemoryException----大對象堆引起的問題與對策
http://blog.sina.com.cn/s/blog_47642c6e0102vh0v.html我們在開發過程中曾經遇到過一個奇怪的問題:當軟件加載了很多比較大規模的數據后,會偶爾出現
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/)。
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();} }
這段代碼不斷的交替分配一個較小的數組和一個較大的數組,其中較小數組的大小為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>相同,代碼中只給出了比較重要的幾個
方法。
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)
? ? ? ? {
? ?
========
總結
以上是生活随笔為你收集整理的C# 大型对象堆学习总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: win32 调试 API 学习总结
- 下一篇: C# - Marshal 学习总结