C# 垃圾回收器高效工作
2019獨角獸企業重金招聘Python工程師標準>>>
首先我會專注于Workstation GC(因此所有的數字都是工作站GC的)。然后我會談談工作站GC和服務器GC之間的區別(有時候你沒有必要選擇,稍后我會解釋為什么)。
代:
把托管堆上的對象分成3代是為了調優垃圾回收的性能,大多數對象都在0代時消亡。例如:在一個服務器程序中,處理每個請求相關的對象,都會在請求完 成后消亡。本質上1代對象是在新分配對象和常駐內存之間的一個緩沖區。當你在性能計數器中觀察2代回收發生的次數比0代回收次數要少的多。而1代回收次數 相對來說不是很重要,回收1代對象比回收0代對象的代價高的不是很多。而回收2代對象就意味著要掃描整個托管堆了,代價相對要大得多。
GC段(segment):
首先讓我們看一下GC是如何向操作系統申請內存的。GC以段的方式保留內存。每一個段是16M(服務器模式下可能是64M)。當執行引擎啟動時,我們保留初始的GC段,一個給小對象用,另一個段給大對象用。有關大對象堆的垃圾回收請參考這里
在需要的時候可以向操作系統申請更多內存,或者交還給操作系統。當所有段都用完之后我們就申請一個新段。在每一次完整的垃圾回收之后多余的段會交還給操作系統。
大對象有自己的段,垃圾回收器對大對象的處理方式和小對象是不一樣的所以大對象不和小對象共享段。
分配:
當你在托管堆上分配一個對象時,要付出什么代價呢?如果我們不考慮回收的話,有兩點1是向前移動指針,2是為新對象清空內存。而對于實現Finalize的方法的對象還需要把對象的指針放到終結隊列中。
注意我說的是“如果我們不考慮回收”—這意味著分配的代價和對象的大小成正比。申請的越少,GC的代價就越小。如果你需要15個byte,就申請 15個字節;不要像使用maalloc一樣申請32個字節。有一個閥值,當超過這個值時,就會觸發垃圾回收。你要盡可能少的觸發垃圾回收。
GC堆和NT堆還有一點不同:分配對象的時間越接近,對象在GC堆上的也越接近。
在GC堆上分配的每一個對象都需要額外的8byte的開銷,4byte用來同步,4byte存放方法表指針。
回收:
首先我們要知道什么時候觸發回收? 有如下三種情況會觸發:
1. 分配時超過了0代堆的閥值
2. 調用了GC.Collect()方法
3. 操作系統給應用程序發出低內存信號
第1種情況是最典型的觸發原因,當分配的對象足夠多時,就會觸發0代堆的垃圾回收。在每一次回收之后,0代堆就清空了。然后繼續分配對象,0代堆填滿之后就會觸發下一次回收。
你要盡量避免第2種情況,這個很簡單,不要在程序代碼中調用GC.Collect方法就可以了。通常情況下你不應該調用Collect方法。BCL is basically the only place that should call this (in very limited places);當你在程序中調用GC.Collect方法時,性能會降低,因為回收提前執行了,而垃圾回收器執行回收的調度是經過算法優化的。
第3種情況受操作系統上運行的其他程序影響,這個你的程序沒法控制,你只能盡可能的優化好你的程序或模塊。
讓我們談一下這意味著什么。首先,托管堆是程序的工作集的一部分。它會消耗私有頁。在理想情況下,所有對象都在0代時消亡(這意味著,幾乎所有對象 都在0代回收,完全回收從不會發生)因此,你的GC堆永遠不會超過0代堆的大小。而事實上這種情況是不可能的,因此,你真的需要保證托管堆的大小是可控 的。
第二,你需要保證垃圾回收消耗的時間資源是可控的。這個意思是一要盡可能少觸發GC,二盡可能少發生高代的GC。一次高代的回收要比底一代回收的代價高得多,因為高代的回收要掃描更多的對象,要同時執行所有更低代的回收。
CLRProfiler是一個觀察GC堆看堆上的對象被那個對象引用的工具,它非常棒。
如何組織你的數據:
1) 用值類型還是引用類型
如你所知,值類型數據是存放在棧上的,而引用類型對象是存在托管堆上的。因此,人們會問,如何決定什么時候使用值類型,什么使用引用類型呢。值類型 不會觸發垃圾回收,但是如果你的值類型經常做裝箱操作,裝箱操作要比剛開始就創建一個引用類型對象要昂貴的多;當值類型對象作為參數傳遞時需要復制一份。 但是如果你的引用類型只有一個小成員如果做成引用類型的話,還需要額外4字節的指針開銷和同步開銷以及方法表開銷。因此該使用值類型還是引用類型是由類型 本身決定的。
2) 富引用對象(Reference rich object)
如果一個對象是富引用的,會給分配和回收都帶拉壓力。每一個內嵌的對象都需要8字節的額外開銷。因為分配的開銷和對象的大小是成正比的,所以開銷就大了一些。另外富引用會導致構建對象圖的時間增大,增加了回收的開銷。
因此我建議你設計對象時只設計必要的字段,如果對另外一個引用類型的強引用不是必須的,就不要引用它。你應該盡量避免讓已存在很長時間的對象引用新分配的對象。
3) 可終結對象(實現Finalize方法的對象)
如垃圾回收原理1中所述終結對象會延長回收的時間,不僅延長可終結對象本身的,還會延長它的引用鏈下游的所有對象的回收時間。所以如果對象必須是可終結的,你就要盡可能的隔離它,不讓它引用其他對象
4) 對象的存儲位置:
當你為一個對象的子對象分配空間時,你最好在同一時間分配父對象和子對象,這樣父子對象在托管堆上的地址就會在一起,回收起來也會一起回收,回收的效率就會相對高一些。
大對象:
當一個對象占用的內存超過85,000bytes時它就會被分配到LOH上。SOH段永遠都不會做移動—而只是清空對象(使用一個空的鏈表)。但是這個情況是一種實現的細節,你不應該依賴這個實現細節。如果你分配了一個大對象,不希望他發生移動,那么你應該fix它。
只有在2代回收時才會做大對象的回收,而2代回收的代價是很大的。有時候你會發現2代回收之后2代堆的大小并沒有發生多大變化,這有可能是因為大對象堆大小超過閥值觸發了2代回收。
一個好的實踐:分配一個大對象然后重復利用它。如果說你需要一個100k或者120k的大對象,你應該申請一個120k的然后重復利用它。多次分配臨時大對象可能會觸發2代回收,對性能會有負面影響。
這篇文章我們談談GC的不同工作模式,以及各個模式如何工作和他們之間的不同,讓你明白你的應用程序該如何選擇工作模式。
迄今為止運行時GC工作模式:
1)關閉并發的工作站GC
2)開啟并發的工作站GC
3)服務器GC
如果你在寫一個獨立的托管程序并且沒有做任何配置,你使用的GC工作模式是開啟并發的工作站GC。這一點多數人可能會感到驚訝,因為我們的文檔中并 沒有提起并發GC,有時候會把并發GC稱為”background GC”(while referring working GC as “foreground GC”).
如果你的程序在宿主程序中運行,宿主可能會為程序選擇GC的工作模式。
需要注意的是:如果你的程序配置成服務器GC,你的程序運行在一臺高性能的機器上,實際上你的服務器是運行在“關閉并發的工作GC”模式下。因為“關閉并發的工作站GC”為高性能服務器的大吞吐量服務做了優化。
各個GC模式的設計目標:
1) 關閉并發的工作站GC為高性能服務器的高吞吐量做了優化。我們在垃圾回收時根據分配和復活模式做動態調優因此可以程序運行時自動調優GC的工作效率。
2) 開啟并發的工作站GC是為要求精確響應時間的交互式應用程序設計的。開啟并發使垃圾回收造成的工作進程暫停時間縮短。 這個目的是用一些內存和CPU換來的,因此在這種模式下垃圾回收需要做的工作略多一點需要的回收時間會略長一些。
3) 服務器GC,從名字上我們可以看出這種工作模式是為服務器應用程序設計的;典型的場景是你有一個工作線程池這些線程坐著相似的處理。例如:做處理同樣的請 求或者處理相同類型的事務。所有的線程使用幾乎相同的分配模式。服務器GC是為要求高吞吐量的和高擴展性的多處理器服務器設計的
各個GC模式如何工作:
讓我們從關閉并發的工作站GC說起,其執行流程如下:
1) 一個托管進程做內存分配
2) 分配完所有可用的內存(我會解釋這是什么意思)
3) 觸發了垃圾回收,垃圾回收操作在做分配的線程上運行
4) GC調用SuspendEE來掛起所有的托管線程
5) GC開始工作
6) GC調用RestartEE來重啟工作托管進程
7) 托管進程繼續運行
你可以看到在第5步中所有的托管線程都停止執行來等待垃圾回收完成工作。SuspendEE不會掛起本地線程(native threads)。
GC中的每一代都有一個“分配預算”的概念。每一代的“分配預算”在運行時是動態調整的。因為我們經常在0代上做分配,你可以想象0代預算超支了,這樣就會觸發垃圾回收。這里的預算和GC堆的段大小完全不是一回事,預算比段大小要小得多。
CLR垃圾回收可以做內存移動也可以不做移動。不做移動時也稱為“清掃”,清掃的代價要比做壓縮的代價低一些,因為他不需要復制移動內存。
在開啟并發垃圾回收(Concurrent GC)時,最大的差異是掛起和重啟。 我前面提到并發GC允許更少的暫停時間。因此開啟并發的垃圾回收會盡可能少的執行垃圾回收,執行時間也非常短。在剩余的時間中如果需要托管線程可以運行和 分配內存。開啟并發時我們會在開始時給0代一個很大的分配預算來保證垃圾回收運行期間有足夠的空間分配對象。盡管如此,如果在并發回收運行中托管線程需要 分配過多的內存,線程也會被堵塞直到回收完成。
記住0代和1代回收非常快,所以在做0,1代回收時是不會做并發回收的。我們只是在2代回收時才會并發回收。如果我們決定做2代回收,我們會決定是否并發回收。
服務器垃圾回收,這種模式和前兩種完全不同。我們會為每一個CPU創建一個回收線程。垃圾回收在這些線程上執行而不是在分配線程上,其工作流程如下:
1. 一個托管線程做回收
2. 分配達到閥值
3. 給GC線程發信號,讓GC線程做垃圾回收,等待回收結束
4. GC線程運行,結束時發出回收完成的信號(在回收過程中,所有的托管線程會像工作站模式中一樣被掛起)
5. 托管線程收到信號重新開始運行
如果配置各個垃圾回收模式:
要關閉并發回收,在配置文件中添加下面配置項:
| <configuration> <runtime> <gcConcurrentenabled="false"/> </runtime> </configuration> |
要使用服務器GC,使用下面配置:
?| <configuration> <runtime> <gcServerenabled=“true"/> </runtime> </configuration> |
關于這兩個配置節可以參考msdn。
這篇文章我們談談固定對象的內存地址(pinning)和弱引用……這兩個和垃圾回收處理密切相關的東西。
固定對象的內存地址:
固定對象的內存地址和實現Finalize方法的對象有一點是相同的 …… 兩者都是因為我們的程序不得不和本地代碼打交道。
怎么固定對象的內存地址呢?有三種方法
1. 使用GCHandle的靜態方法Alloc(object val,GCHandleType type) ,將type值設為GCHandleType.Pinned
2. 在C#中使用fixed關鍵字
3. 在調用本地代碼時,本本地代碼固定(例如:to marshal LPWSTR as a String object, Interop pins the buffer for the duration of the call)
對于小對象堆來說,在代碼中固定對象地址是導致內存碎片的唯一原因,如果沒有固定內存地址的對象那么在小對象堆中就不應該有碎片。
而對于大對象堆,固定內存地址的操作是無效的,因為現在的垃圾回收機制是不會移動大對象堆的對象的。不過,這一點只是GC的內部實現,你不應該依賴于這個實現,如果大對象需要固定內存地址,你還是要寫固定需要的代碼。
內存碎片從來都不是一個好東西。它會增加垃圾回收工作的難度 —— 如果沒有固定內存地址的對象,垃圾回收器在移動內存時只需要將非垃圾對象覆蓋空閑內存就可以了,而堆上存在固定地址的對象,垃圾回收器就不得不在移動中考慮,不覆蓋這類對象,也不能移動它們。
那么如何才能知道你的程序中有多少內存碎片呢?你可以使用!dumpheap命令:“dumpheap –type Free -stat”會給出所有釋放對象占用內存的統計信息。 通常情況下如果碎片大小占總大小的比例小于10%的話,就沒什么可擔憂的。因此如果你看到釋放對象的絕對數很大,但是總數少于10%就沒必要害怕。
如果你確實需要固定對象的地址,請注意下面幾件事情:
1. 短時間的固定開銷會很小
“短時間”,多短算短呢? 如果固定內存地址的對象在垃圾回收之前就成為垃圾對象了,那么這個時間就是足夠短了。因為固定內存其實就是在對象頭置一個bit位,如果在對象存活期沒有 發生垃圾回收,那么就沒有額外開銷。如果在垃圾回收發生后這個對象還活著,垃圾回收器在移動內存時就得做更多的計算保證不會移動此對象,也不會覆蓋它。
2. 固定老對象的代價會比固定年輕對象的代價要小一些
何為“老對象”呢?是指經過兩次垃圾回收,已經被遷移到2代堆的對象;這時候對象的所在的內存區域已經相對穩定了。造成內存碎片的可能性會小一些。
兩個固定對象內存地址好實踐:
1. 在大對象堆上分配固定地址的對象,每次使用使使用其中的一部分
這樣做的優點是顯而易見的,大對象堆不會做內存移動操作,所以就不存在因為固定對象地址導致的開銷了;缺點是沒有現成的API來把大對象分成一小塊一小塊使用,這需要開發人員按需編碼使用。
2. 分配一個小對象的緩沖池,(and then hand them out when needed)
例如,我有一個緩沖池,方法M有一個byte[]數組需要固定內存地址。如果這個數組已經是2代對象了,就固定它。而如果緩沖區不需要使用很長時間,那么就在0代和1代回收時回收它。這樣所有在緩沖池中的對象就都是2代對象了。
void M(byte[] b)
{
if (GC.GetGeneration(b) == GC.MaxGeneration)
{
RealM(b);
return;
}
// GetBuffer will allocate one if no buffers
// are available in the buffer pool.
byte[] TempBuffer = BufferPool.GetBuffer();
RealM(TempBuffer);
CopyBackToUserBuffer(TempBuffer, b);
BufferPool.Release(TempBuffer);
}
弱引用:
弱引用是如何實現的呢?
一個弱引用對象有托管和非托管兩個部分。托管部分是WeakReference對象本身。在它的構造函數中我們創建一個GC句柄,它是非托管的部分 —— 這會在AppDomain的句柄表中插入一項(GCHandle類的Alloc方法都是這么做的,只不過是將不同類型插入到各自的表中)。當弱引用指向的 對象沒有強引用時就會被垃圾回收器回收掉。因為WeakReference對象本身是一個托管對象,所以它沒有強引用時也會被回收。
弱引用沒必要引用小對象:
如果你有一個非常小的對象,比如說一個DWORD字段,對象的大小是12byte(12byte是對象的最小尺寸)。而WeakReference 對象有一個IntPtr和一個bool字段,而GC句柄是一個指針的大小(32位機器4byte,64位機器8byte);這就是說你需要使用15個 byte的對象來延長一個12byte對象的長度,這是不劃算的。顯然你不應該創建很多弱引用對象來引用一些小對象。
那么,弱引用有什么用呢?
弱引用的作用是在垃圾回收發生之前即便對象上沒有強引用,也可以再次使用該對象。如果執行垃圾回收它會被回收。
為什么要使用弱引用來跟蹤一個對象的釋放,而不是用Finalizer方法呢? 使用弱引用的優點是跟蹤的對象不會被推遲到下次垃圾回收時才真正的被回收;缺點是需要消耗一點內存,只有當用戶代碼檢查弱引用指向的對象為null時才清除對象。
下面是兩個弱對象的使用實例:
Option A):
| classA { WeakReference _target; MyObject Target { set { _target =newWeakReference(value); } get { Object o = _target.Target; if(o !=null) { returno; } else { // my target has been GC'd - clean up Cleanup(); returnnull; } } } voidM() { // target needs to be alive throughout this method. MyObject target = Target; if(target ==null) // target has been GC'd, don't bother return; else { // always need target to be alive. DoSomeWork(); } GC.KeepAlive(target); } } |
Option B):
?| classA { WeakReference _target; MyObject ShortTemp; MyObject Target { set { _target =newWeakReference(value); } get { Object o = _target.Target; if(o !=null) { returno; } else { // my target has been GC'd - clean up Cleanup();?????? returnnull; } } } voidM() { // target needs to be alive throughout this method. MyObject target = Target; ShortTemp = target; if(target ==null) { // target has been GC'd, don't bother return; } else { // could assert that ShortTemp is not null. DoSomeWork(); } ShortTemp =null; } } |
使用弱對象維護緩存:
你可以創建一個WeakReference指向對象的數組
WeakReferencesToObjects WeakRefs[];
數組中的每個元素都是弱對象指向的對象。我們可以定期的遍歷這個數組看哪個對象已經死了然后釋放引用此對象的WeakReference對象。
如果我們經常處理已經釋放的對象,緩存就會在每次垃圾回收之后失效。
如果對你來說這還不夠,你可以使用二級緩存機制。
維持一段時間的緩存項的強引用列表,在這段時間之后,將強引用轉換成弱引用,弱引用意味著這些項將要被剔除。
你可能有不同的策略來處理緩存,比如根據緩存被讀取次數——讀取次數少的項被轉換成弱引用;或者根據緩存項的個數,如果緩存項數超過某個數字就把超過的緩存項設置為弱引用。這取決于你的應用。調優緩存是另一個完整的主題——或許將來我會寫一下。
轉載于:https://my.oschina.net/dgwutao/blog/139422
總結
以上是生活随笔為你收集整理的C# 垃圾回收器高效工作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AMD R7 7840H 处理器为中国专
- 下一篇: 盒马怎么切换到盒马门店