編寫高性能.net代碼--垃圾回收的前世今生
垃圾回收的內(nèi)容太多,比較繁瑣,先整理提綱,后續(xù)豐富更多實(shí)踐經(jīng)驗(yàn),還有收集PrefView和Windbug的使用



●垃圾回收簡介
●垃圾回收將會(huì)是你一直關(guān)注的性能因素
垃圾回收將會(huì)是你一直關(guān)注的性能因素。大部分容易察覺的性能問題,“顯然”都是由垃圾回收引起的。這些問題修正起來速度最快,也是需要你持續(xù)關(guān)注并時(shí)刻檢查的。我用了“顯然”這個(gè)詞,是因?yàn)槲覀儗?huì)發(fā)現(xiàn),很多問題實(shí)際上都是由于對(duì)垃圾回收器的行為和預(yù)期結(jié)果理解有誤。在.NET環(huán)境中,你需要更多地關(guān)注內(nèi)存的性能,至少要像對(duì)CPU性能一樣。較好的內(nèi)存性能是.NET程序流暢運(yùn)行的重要基礎(chǔ)
●垃圾回收可能導(dǎo)致系統(tǒng)開銷,非常不安
很多情況下垃圾回收器實(shí)際上會(huì)整體提高內(nèi)存堆的性能,因?yàn)樗芨咝У赝瓿蓛?nèi)存分配和碎片整理工作。垃圾回收肯定能為你的應(yīng)用程序帶來好處。
●本機(jī)代碼內(nèi)存管理--費(fèi)盡腦經(jīng)減少內(nèi)存碎片
Windows的本機(jī)代碼模式下,內(nèi)存堆維護(hù)著一張空閑內(nèi)存塊的列表,用于內(nèi)存的分配。盡管用到了低碎片化的內(nèi)存堆(Low Fragmentation Heaps),很多長時(shí)間運(yùn)行的本機(jī)代碼應(yīng)用還是得費(fèi)盡心機(jī)地對(duì)付內(nèi)存碎片問題。內(nèi)存分配操作的速度會(huì)越來越慢,因?yàn)橄到y(tǒng)分配程序遍歷空閑內(nèi)存表的時(shí)間會(huì)越來越長。內(nèi)存的占用率會(huì)持續(xù)增長,進(jìn)程肯定也需要重啟以開始新的生命周期。為了減少內(nèi)存碎片,有些本機(jī)代碼程序用大量代碼實(shí)現(xiàn)了自己的內(nèi)存分配機(jī)制,把默認(rèn)的malloc函數(shù)給替換掉了
●.NET環(huán)境,內(nèi)存分配工作量小
在.NET環(huán)境中,內(nèi)存分配的工作量很小,因?yàn)閮?nèi)存總是整段分配的,通常情況下不會(huì)比內(nèi)存的擴(kuò)大、減小或比較增加多少開銷。在通常情況下,不存在需要遍歷的空閑內(nèi)存列表,也幾乎不可能出現(xiàn)內(nèi)存碎片。其實(shí)GC內(nèi)存堆的效率還會(huì)更高,因?yàn)檫B續(xù)分配的多個(gè)對(duì)象往往在內(nèi)存堆中也是連續(xù)存放的,提高了就近訪問的可能性(Locality)
●內(nèi)存分配流程
在默認(rèn)的內(nèi)存分配流程中,會(huì)有一小段代碼先檢查目標(biāo)對(duì)象的大小,看看內(nèi)存分配緩沖區(qū)中所剩的內(nèi)存還夠不夠用。只要緩沖區(qū)還夠用,內(nèi)存分配過程就十分迅速,不存在資源爭用問題。如果內(nèi)存分配緩沖區(qū)已被耗盡,就會(huì)交由GC分配程序來檢索足以容納目標(biāo)對(duì)象的空閑內(nèi)存。然后一個(gè)新的分配緩沖區(qū)會(huì)被保留下來,用于以后的內(nèi)存分配
●簡單演示
簡單演示內(nèi)存分配過程的C#代碼如下。class MyObject { int x; int y; int z; } static void Main(string[] args) { var x = new MyObject(); } 首先,讓我們分解一下。以下是調(diào)用內(nèi)存分配函數(shù)的代碼。;把類的方法表指針拷貝到ecx中 ;作為new()的參數(shù) ;可以用!dumpmt查看值 mov ecx,3F3838h ;調(diào)用new call 003e2100 ;把返回值(對(duì)象的地址)拷貝到寄存器中 mov edi,eax 下面是實(shí)際的分配函數(shù)。;注意:為了格式統(tǒng)一,大部分代碼的地址都未給出 ; ;把eax值設(shè)為0x14,也就是需要分配給對(duì)象的內(nèi)存大小 ;數(shù)值來自于方法表 mov eax,dword ptr [ecx+4] ds:002b:003f383c=00000014 ;把內(nèi)存分配緩沖區(qū)數(shù)據(jù)寫入edx mov edx,dword ptr fs:[0E30h] ;edx+40存放著下一個(gè)可用的內(nèi)存地址 ;把其中的值加上對(duì)象所需大小,寫入eax add eax,dword ptr [edx+40h] ;把所需內(nèi)存地址與分配緩沖區(qū)的結(jié)束地址進(jìn)行比較 cmp eax,dword ptr [edx+44h] ;如果超出了內(nèi)存分配緩沖區(qū) ;跳轉(zhuǎn)到速度較慢的分配流程 ja 003e211b ;更新空閑內(nèi)存指針(在舊值上增加0x14字節(jié)) mov dword ptr [edx+40h],eax ;將指針減去對(duì)象大小 ;指向新對(duì)象的起始位置 sub eax,dword ptr [ecx+4] ;將方法表指針寫入對(duì)象的前4字節(jié) ;現(xiàn)在eax指向的是新對(duì)象 mov dword ptr [eax],ecx ;返回調(diào)用者 ret ;慢速分配流程(調(diào)用CLR方法) 003e211b jmp clr!JIT_New (71763534)
●服務(wù)器模式
如果你的垃圾回收配置成服務(wù)器模式,內(nèi)存分配過程就沒有快速和慢速之分,因?yàn)槊總€(gè)處理器都有各自的內(nèi)存堆。.NET的內(nèi)存分配流程比較簡單,而解除分配的過程則復(fù)雜得多,但這個(gè)復(fù)雜的過程不需要你直接處理。你只需要學(xué)習(xí)如何優(yōu)化即可

●2.1 ?基本運(yùn)作方式
●內(nèi)存堆
●本機(jī)內(nèi)存堆(Native Heap)
是由VirtualAlloc這個(gè)Windows API分配的,是由操作系統(tǒng)和CLR使用的,用于非托管代碼所需的內(nèi)存,比如Windows API、操作系統(tǒng)數(shù)據(jù)結(jié)構(gòu)、很多CLR數(shù)據(jù)等
●托管堆(Managed Heap)
CLR在托管堆(Managed Heap)上為所有.NET托管對(duì)象分配內(nèi)存,也被成為GC堆,因?yàn)槠渲械膶?duì)象均要受到垃圾回收機(jī)制的控制 托管堆又分為兩種——小對(duì)象堆和大對(duì)象堆(LOH),兩者各自擁有自己的內(nèi)存段(Segment)。每個(gè)內(nèi)存段的大小視配置和硬件環(huán)境而定,對(duì)于大型程序可以是幾百M(fèi)B或更大。小對(duì)象堆和LOH都可擁有多個(gè)內(nèi)存段
●小對(duì)象堆
小對(duì)象堆的內(nèi)存段進(jìn)一步劃分為3代
●暫時(shí)段(Ephemeral Segment)第0代和第1代總是位于同一個(gè)內(nèi)存段中
●而第2代可能跨越多個(gè)內(nèi)存段
●大對(duì)象堆(LOH)
●跨越多個(gè)內(nèi)存段
●小對(duì)象堆中分配內(nèi)存的對(duì)象的生存期
●默認(rèn)快速分配
對(duì)象小于85 000字節(jié),CLR都會(huì)把它分配在小對(duì)象堆中的第0代,通常緊挨著當(dāng)前已用內(nèi)存空間往后分配
●快速分配失敗
如果快速分配失敗,對(duì)象就可能會(huì)被放入第0代內(nèi)存堆中的任意地方,只要能容納得下就行
●空間不足
如果沒有合適的空閑空間,那么分配器就會(huì)擴(kuò)大第0代內(nèi)存堆,以便能存入新對(duì)象
●垃圾回收
如果擴(kuò)大內(nèi)存堆時(shí)超越了內(nèi)存段的邊界,則會(huì)觸發(fā)垃圾回收過程
●對(duì)象保持存活
對(duì)象總是誕生于第0代內(nèi)存堆。只要對(duì)象保持存活,每當(dāng)發(fā)生垃圾回收時(shí),GC都會(huì)把它提升一代。第0代和第1代內(nèi)存堆的垃圾回收有時(shí)候被稱為瞬時(shí)回收(Ephemeral Collection)
●01代的碎片整理
?
對(duì)象總是誕生于第0代內(nèi)存堆。只要對(duì)象保持存活,每當(dāng)發(fā)生垃圾回收時(shí),GC都會(huì)把它提升一代。第0代和第1代內(nèi)存堆的垃圾回收有時(shí)候被稱為瞬時(shí)回收(Ephemeral Collection)
●碎片整理引用的開銷
每一代內(nèi)存堆都有可能發(fā)生碎片整理。因?yàn)?/span>GC必須修正所有對(duì)象的引用,使它們指向新的位置,所以碎片整理的開銷相對(duì)較大,還有可能需要暫停所有托管線程。正因如此,垃圾回收器只在劃算(Productive)時(shí)才會(huì)進(jìn)行碎片整理
●2代內(nèi)存與完全垃圾回收
如果對(duì)象到達(dá)了第2代內(nèi)存堆,它就會(huì)一直留在那里直至終結(jié)。這并不意味著第2代內(nèi)存堆只會(huì)一直變大。如果第2代內(nèi)存堆中的對(duì)象都終結(jié)了,整個(gè)內(nèi)存段也沒有存活的對(duì)象了,垃圾回收器會(huì)把整個(gè)內(nèi)存段交還給操作系統(tǒng),或者作為其他幾代內(nèi)存堆的附加段。在進(jìn)行完全垃圾回收(Full Garbage Collection)時(shí),就可能發(fā)生這種第2代內(nèi)存堆的回收
●存活的意思
如果GC能夠通過任一已知的GC根對(duì)象(Root),沿著層層引用訪問到某個(gè)對(duì)象,那它就是存活的。GC根對(duì)象可以是程序中的靜態(tài)變量,或者某個(gè)線程的堆棧被正在運(yùn)行的方法占用(用于局部變量),或者是GC句柄(比如固定對(duì)象的句柄,Pinned Handle),或是終結(jié)器隊(duì)列(Finalizer Queue)。請(qǐng)注意,有些對(duì)象可能沒有受GC根對(duì)象的引用,但如果是位于第2代內(nèi)存堆中,那么第0代回收是不會(huì)清理這些對(duì)象的,必須等到完全垃圾回收才會(huì)被清理到
●代的提升
?
如果第0代堆即將占滿一個(gè)內(nèi)存段,而且垃圾回收也無法通過碎片整理獲取足夠的空閑內(nèi)存,那么GC會(huì)分配一個(gè)新的內(nèi)存段。新的內(nèi)存段會(huì)用于容納第1代和第0代堆,老的內(nèi)存段將會(huì)變?yōu)榈?代堆。老的第0代堆中的所有對(duì)象都會(huì)被放入新的第1代堆中,老的第1代堆同理將提升為第2代堆(提升很方便,不必復(fù)制數(shù)據(jù))
●2代堆
第2代堆繼續(xù)變大,就可能會(huì)跨越多個(gè)內(nèi)存段。LOH堆同樣也可能跨越多個(gè)內(nèi)存段。無論存在多少個(gè)內(nèi)存段,第0代和第1代總是位于同一個(gè)段中
●大的對(duì)象直接LOH分配
LOH則遵從另一套回收規(guī)則。大于85 000字節(jié)的對(duì)象將自動(dòng)在LOH中分配內(nèi)存,且沒有什么“代”的模式。超過這個(gè)尺寸的對(duì)象通常也就是數(shù)組和字符串了。出于性能考慮,在垃圾回收期間LOH不會(huì)自動(dòng)進(jìn)行碎片整理
●LOH碎片
在LOH中,垃圾回收器用一張空閑內(nèi)存列表來確定對(duì)象的存放位置,如果是在調(diào)試器中查看位于LOH的對(duì)象,你會(huì)發(fā)現(xiàn)有可能整個(gè)LOH都小于85 000字節(jié),而且可能還有對(duì)象的大小是小于已分配值的。這些對(duì)象通常都是CLR分配出去的,可以不予理睬
●垃圾回收原則
●如果回收了第1代,則也會(huì)同時(shí)回收第0代
●如果回收了第2代,則所有內(nèi)存堆都會(huì)回收,包括LOH
●如果發(fā)生了第0代或第1代垃圾回收,那么程序在回收期間就會(huì)暫停運(yùn)行
●對(duì)于第2代垃圾回收而言,有部分回收是在后臺(tái)線程中進(jìn)行的,這要根據(jù)配置參數(shù)而定
●垃圾回收步驟
●掛起(Suspension)
所有托管線程都被強(qiáng)行中止
●標(biāo)記(Mark)
從GC根對(duì)象開始,垃圾回收器沿著所有對(duì)象引用進(jìn)行遍歷并把所見對(duì)象記錄下來
●碎片整理(Compact)
將對(duì)象重新緊挨著存放并更新所有引用,以便減少內(nèi)存碎片。在小對(duì)象堆中,碎片整理會(huì)按需進(jìn)行,無法控制。在LOH中,碎片整理不會(huì)自動(dòng)進(jìn)行,但你可以在必要時(shí)通知垃圾回收器來上一次
●恢復(fù)(Resume)
托管線程恢復(fù)運(yùn)行
●垃圾回收的開銷
●在標(biāo)記階段并不需要遍歷內(nèi)存堆中的所有對(duì)象,只要訪問那些需要回收的部分即可。比如第0代回收只涉及到第0代內(nèi)存堆中的對(duì)象,第1代回收將會(huì)標(biāo)記第0代和第1代內(nèi)存堆中的對(duì)象
●而第2代回收和完全回收,則需遍歷內(nèi)存堆中所有存活的對(duì)象,這一過程的開銷有可能非常大
●高代內(nèi)存堆中的對(duì)象有可能是低代內(nèi)存堆對(duì)象的根對(duì)象。這樣就會(huì)導(dǎo)致垃圾回收器遍歷到一部分高代內(nèi)存堆的對(duì)象
●總結(jié)
●垃圾回收過程的耗時(shí)幾乎完全取決于所涉及“代”內(nèi)存堆中的對(duì)象數(shù)量,而不是你分配到的對(duì)象數(shù)量
這就是說,即使你分配了1棵包含100萬個(gè)對(duì)象的樹,只要在下一次垃圾回收之前把根對(duì)象的引用解除掉,這100萬個(gè)對(duì)象就不會(huì)增加垃圾回收的耗時(shí)
●垃圾回收的頻率取決于所涉及“代”內(nèi)存堆中已被占用的內(nèi)存大小。只要已分配內(nèi)存超過了某個(gè)內(nèi)部閾值,就會(huì)發(fā)生該“代”垃圾回收
這個(gè)閾值是持續(xù)變化的,GC會(huì)根據(jù)進(jìn)程的執(zhí)行情況進(jìn)行調(diào)整。如果某“代”回收足夠劃算(提升了很多對(duì)象所處的“代”),那垃圾回收就會(huì)發(fā)生得頻繁一些,反之亦然
●另一個(gè)觸發(fā)垃圾回收的因素是所有可用內(nèi)存,與你的應(yīng)用程序無關(guān)
如果可用內(nèi)存少于某個(gè)閾值,為了減少整個(gè)內(nèi)存堆的大小,垃圾回收可能會(huì)更為頻繁地發(fā)生
●通過控制內(nèi)存分配模式來控制垃圾回收的統(tǒng)計(jì)指標(biāo),就是一種最容易實(shí)現(xiàn)的優(yōu)化方法。這需要理解垃圾回收的工作機(jī)制、可用的配置參數(shù)、你的內(nèi)存分配率,還需要對(duì)對(duì)象的生存期有很好的控制能力
●2.2 配置參數(shù)
垃圾回收器的配置及調(diào)優(yōu),很大程度上由硬件配置、可用資源和程序的行為決定。屈指可數(shù)的幾個(gè)參數(shù)也是用于控制很高層的行為,且主要取決于程序的類型

●2.2.1 工作站模式Workstation還是服務(wù)器模式Server
●垃圾回收默認(rèn)采用工作站模式
在工作站模式下,所有的GC都運(yùn)行于觸發(fā)垃圾回收的線程中,優(yōu)先級(jí)(Priority)也相同
●服務(wù)器模式
GC會(huì)為每個(gè)邏輯處理器或處理器核心創(chuàng)建各自專用的線程。這些線程的優(yōu)先級(jí)是最高的(THREAD_PRIORITY_HIGHEST),但在需要進(jìn)行垃圾回收之前會(huì)一直保持掛起狀態(tài)。垃圾回收完成后,這些線程會(huì)再次進(jìn)入休眠(Sleep)狀態(tài)
●CLR還會(huì)為每個(gè)處理器創(chuàng)建各自獨(dú)立的內(nèi)存堆
每個(gè)處理器堆都包含1個(gè)小對(duì)象堆和1個(gè)LOH。從應(yīng)用程序角度來看,就只有一個(gè)邏輯內(nèi)存堆,你的代碼不清楚對(duì)象屬于哪一個(gè)堆,對(duì)象引用會(huì)在所有堆之間交叉進(jìn)行(這些引用共用相同的虛擬地址空間
●多個(gè)內(nèi)存堆的存在會(huì)帶來一些好處
●垃圾回收可以并行進(jìn)行
每個(gè)垃圾回收線程負(fù)責(zé)回收一個(gè)內(nèi)存堆。這可以讓垃圾回收的速度明顯快于工作站模式
●某些情況下,內(nèi)存分配的速度也會(huì)更快一些
特別是對(duì)LOH而言,因?yàn)闀?huì)在所有內(nèi)存堆中同時(shí)進(jìn)行分配
●服務(wù)器模式還有一點(diǎn)與工作站模式不同,就是擁有更大的內(nèi)存段,也就意味著垃圾回收的間隔時(shí)間可以更長一些
●
?
●到底是用工作站還是服務(wù)器模式進(jìn)行垃圾回收?
●如果應(yīng)用程序運(yùn)行于專為你準(zhǔn)備的多處理器主機(jī)上,那就無疑要選擇服務(wù)器模式。這樣在大部分情況下,都能讓垃圾回收占用的時(shí)間降至最低
●果需要與多個(gè)托管進(jìn)程共用一臺(tái)主機(jī),那么選擇就不那么明確了。服務(wù)器模式的垃圾回收會(huì)創(chuàng)建多個(gè)高優(yōu)先級(jí)的線程
●如果多個(gè)應(yīng)用程序都這么設(shè)置,那線程調(diào)度就會(huì)相互帶來負(fù)面影響。這時(shí)可能還是選用工作站模式垃圾回收更好
●如果你確實(shí)想讓同一臺(tái)主機(jī)上的多個(gè)應(yīng)用程序使用服務(wù)器模式的垃圾回收,還有一種做法,就是讓存在競爭關(guān)系的應(yīng)用程序都集中在指定的幾個(gè)處理器上運(yùn)行,這樣CLR只會(huì)為這些處理器創(chuàng)建自己的內(nèi)存堆

●2.2.2 后臺(tái)垃圾回收
后臺(tái)垃圾回收(Background GC)只會(huì)影響第2代內(nèi)存堆的垃圾回收行為。第0代和第1代的垃圾回收仍會(huì)采用前臺(tái)垃圾回收,也就是會(huì)阻塞所有應(yīng)用程序的線程。
●服務(wù)器模式的后臺(tái)垃圾回收
●每個(gè)邏輯處理器都擁有一個(gè)額外的后臺(tái)GC線程
●如果采用服務(wù)器模式垃圾回收和后臺(tái)垃圾回收,那每個(gè)處理器就會(huì)有兩個(gè)GC專用線程
●后臺(tái)垃圾回收與應(yīng)用程序的線程是并行發(fā)生的,但也有可能同時(shí)發(fā)生了阻塞式垃圾回收
后臺(tái)GC線程會(huì)和其他應(yīng)用程序線程一起暫停運(yùn)行,等待阻塞式垃圾回收的完成
●后臺(tái)垃圾回收的關(guān)閉
?
●提示建議
在實(shí)際應(yīng)用中,應(yīng)該很少會(huì)有關(guān)閉后臺(tái)垃圾回收的理由。如果你想阻止后臺(tái)垃圾回收的線程占用應(yīng)用程序的CPU時(shí)間,而且不介意完全垃圾回收和阻塞垃圾回收時(shí)可能增加的時(shí)間和頻次,那就可以把它關(guān)閉

●2.2.3 低延遲模式(Low Latency Mode)
●確保較高的性能,可以通知GC不要執(zhí)行開銷很大的第2代垃圾回收
●LowLatency——僅適用于工作站模式GC,禁止第2代垃圾回收
●SustainedLowLatency——適用于工作站和服務(wù)器模式的GC,禁止第2代完全垃圾回收,但允許第2代后臺(tái)垃圾回收。必須啟用后臺(tái)垃圾回收,本參數(shù)才會(huì)生效
●缺點(diǎn):
因?yàn)椴粫?huì)再進(jìn)行碎片整理了,所以這兩種參數(shù)都會(huì)顯著增加托管堆的大小。如果你的進(jìn)程需要大量內(nèi)存,就應(yīng)該避免使用這種低延遲模式
●建議:
●在即將進(jìn)入低延遲模式前,最好是能強(qiáng)制執(zhí)行一次完全垃圾回收,這通過調(diào)用GC.Collect(2, GCCollectionMode.Forced)即可完成。
●當(dāng)代碼離開低延遲模式后,馬上再做一次完全垃圾回收
●請(qǐng)勿將低延遲模式作為默認(rèn)模式來使用。低延遲模式確實(shí)是用于那些必須長時(shí)間不被中斷的應(yīng)用程序,但不是100%的時(shí)間都得如此
一個(gè)很好的例子就是股票交易,在開市期間,當(dāng)然不希望發(fā)生完全垃圾回收。而在休市時(shí)間里,就可以關(guān)閉低延遲模式并執(zhí)行完全垃圾回收,等到下一次開市時(shí)再切換回來
●開啟低延遲模式的條件
●完全垃圾回收的持續(xù)時(shí)間過長,是程序正常運(yùn)行時(shí)絕對(duì)不能接受的
●應(yīng)用程序的內(nèi)存占用量遠(yuǎn)低于可用內(nèi)存數(shù)
●無論是關(guān)閉低延遲模式期間、程序重啟,還是手動(dòng)執(zhí)行完全垃圾回收期間,應(yīng)用程序都可以保持存活狀態(tài)
●無論是關(guān)閉低延遲模式期間、程序重啟,還是手動(dòng)執(zhí)行完全垃圾回收期間,應(yīng)用程序都可以保持存活狀態(tài)

●2.3 減少內(nèi)存分配量
如果你減少了內(nèi)存分配數(shù)量,也就減輕了垃圾回收器的運(yùn)行壓力,同時(shí)還可以減少內(nèi)存碎片整理量和CPU占用率。要想減少內(nèi)存分配量,得動(dòng)些腦筋才行,還有可能與其他設(shè)計(jì)目標(biāo)發(fā)生沖突
●嚴(yán)格審查每個(gè)對(duì)象
●是否真的需要這個(gè)對(duì)象?
●對(duì)象中有沒有什么成員是可以摒棄的?
●數(shù)組能否減小一些?
●基元類型(Primitive)能否減小體積(比如Int64換成Int32)?
●有些對(duì)象是否很少用到,僅在必要時(shí)再行分配?
●有些類能否轉(zhuǎn)成“結(jié)構(gòu)”(Struct)?這樣就能存放在堆棧中,或者是成為其他對(duì)象的成員
●分配的內(nèi)存很多,是否只用了一小部分?
●能否用其他途徑獲取數(shù)據(jù)?

●2.4 首要規(guī)則
●只對(duì)第0代內(nèi)存堆中的對(duì)象進(jìn)行垃圾回收
垃圾回收器,存在一條基本的高性能編碼規(guī)則。其實(shí)垃圾回收器明顯就是按照這條規(guī)則進(jìn)行設(shè)計(jì)的
●對(duì)象的生存期應(yīng)該盡可能短暫,這樣垃圾回收器根本就不會(huì)去觸及它們
●或者做不到轉(zhuǎn)瞬即逝,那就讓對(duì)象盡快提升到第2代內(nèi)存堆并永遠(yuǎn)留在那里,再也不會(huì)被回收
通常這也意味著要把可重用的對(duì)象進(jìn)行池化(Pooling),特別是LOH中的所有對(duì)象
●內(nèi)存堆的代數(shù)越高,垃圾回收的代價(jià)就越大
●應(yīng)該避免大部分第1代回收的發(fā)生,因?yàn)閺牡?代提升到第1代的對(duì)象,往往會(huì)被適時(shí)提升到第2代。第1代內(nèi)存堆可以說是第2代堆的一種緩沖區(qū)
●編碼時(shí)的注意
●理想狀態(tài)下,所有對(duì)象都應(yīng)該在下一次第0代回收到來之前離開作用域(Scope)。你可以測算出兩次0代回收之間的間隔時(shí)間,并與數(shù)據(jù)在應(yīng)用程序中的存活時(shí)間進(jìn)行比較

●2.5 縮短對(duì)象的生存期
●對(duì)象的作用域越小,在垃圾回收時(shí)就越?jīng)]有機(jī)會(huì)被提升到下一代
●在使用對(duì)象時(shí),應(yīng)該確保對(duì)象盡快地離開作用域
●如果你的代碼要對(duì)某個(gè)對(duì)象進(jìn)行多次操作,請(qǐng)盡量縮短第一次和最后一次使用的間隔,這樣GC就能盡早地回收這個(gè)對(duì)象了
●如果某個(gè)對(duì)象的引用是一個(gè)長時(shí)間存活對(duì)象的成員,有時(shí)你得把這個(gè)引用顯式地設(shè)置為null
也許會(huì)稍微增加一點(diǎn)代碼的復(fù)雜度,因?yàn)槟愕秒S時(shí)準(zhǔn)備多檢查一下null值,并且還有可能導(dǎo)致功能有效性和完整性之間的矛盾,特別是在調(diào)試的時(shí)候
●另一種平衡功能性和完整性的做法,就是專為調(diào)試作出臨時(shí)修改,讓程序(或滿足特定需求的部分功能)運(yùn)行時(shí)不對(duì)引用設(shè)置null,盡可能保持存活

●2.6 減少對(duì)象樹的深度
●GC將會(huì)沿著對(duì)象引用遍歷
●在服務(wù)器模式GC中,一次會(huì)有多個(gè)線程同時(shí)遍歷
你肯定希望能盡可能地利用這種并發(fā)機(jī)制,但如果有某個(gè)線程陷入一條很長的嵌套對(duì)象鏈中,那么整個(gè)垃圾回收過程就得等這個(gè)線程完成工作后才會(huì)結(jié)束
●目前GC線程采用了work-stealing算法來更好地平衡負(fù)載
●提示:如果你懷疑代碼中有很深的對(duì)象樹存在,那么檢查一下還是有好處的

●2.7 減少對(duì)象間的引用
●如果對(duì)象引用了很多其他對(duì)象,垃圾收集器對(duì)其遍歷時(shí)就要耗費(fèi)更多的時(shí)間
如果垃圾回收引起的暫停時(shí)間較長,往往意味著有大型、復(fù)雜的對(duì)象間引用關(guān)系存在
●如果難以確定對(duì)象所有的被引用關(guān)系,那還有一個(gè)風(fēng)險(xiǎn)就是很難預(yù)測對(duì)象的生存期
減少對(duì)象引用的復(fù)雜度,不僅對(duì)提高代碼質(zhì)量有利,而且可以讓代碼調(diào)試和修正性能問題變得更加容易
●減少對(duì)象引用的復(fù)雜度,不僅對(duì)提高代碼質(zhì)量有利,而且可以讓代碼調(diào)試和修正性能問題變得更加容易
比如第2代內(nèi)存堆中有個(gè)對(duì)象包含了對(duì)第0代內(nèi)存堆對(duì)象的引用,這樣每次第0代垃圾回收時(shí),總有一部分第2代內(nèi)存堆中的對(duì)象不得不被遍歷到,以便確認(rèn)它們是否還持有對(duì)第0代對(duì)象的引用。這種遍歷的代價(jià)雖然沒有像完全垃圾回收那么高,但不必要的開銷還是能免則免

●2.8 避免對(duì)象固定(Pining)
對(duì)象固定(Pinning)是為了能夠安全地將托管內(nèi)存的引用傳遞給本機(jī)代碼。最常見的用處就是傳遞數(shù)組和字符串。如果不與本機(jī)代碼進(jìn)行交互,就完全不應(yīng)該有對(duì)象固定的需求
●內(nèi)存地址固定
對(duì)象固定會(huì)把內(nèi)存地址固定下來,垃圾回收器就無法移動(dòng)這類對(duì)象。雖然固定操作本身開銷并不大,但會(huì)給垃圾回收工作造成一定困擾,增加出現(xiàn)內(nèi)存碎片的可能。垃圾回收器是會(huì)記住那些被固定的對(duì)象,以便能利用固定對(duì)象之間的空閑內(nèi)存,但如果固定對(duì)象過多,還是會(huì)導(dǎo)致內(nèi)存碎片的產(chǎn)生和內(nèi)存堆的擴(kuò)大
●對(duì)象固定形式-顯示
對(duì)象固定既可能是顯式的,也可能是隱式的。使用GCHandleType.Pinned類型的GCHandle或者fixed關(guān)鍵字,可以完成顯式對(duì)象固定,代碼塊必須標(biāo)記為unsafe。用關(guān)鍵字fixed和GCHandle之間的區(qū)別類似于using和顯式調(diào)用Dispose的差別。fixed/using用起來更方便,但無法在異步環(huán)境下使用,因?yàn)楫惒綘顟B(tài)下不能傳遞handle,也不能在回調(diào)方法中銷毀handle
●隱士的對(duì)象固定
隱式的對(duì)象固定更為普遍,但也更難被發(fā)現(xiàn),消除則更困難。最明顯的來源就是通過P/Invoke傳給非托管代碼的所有對(duì)象。這種P/Invoke并不僅僅是由你編寫的代碼發(fā)起的,你調(diào)用的托管API可以而且經(jīng)常會(huì)調(diào)用本機(jī)代碼,也都需要對(duì)象固定
●建議:
●理想狀態(tài)下,應(yīng)該盡可能消除對(duì)象固定
●如果真的做不到,請(qǐng)參照縮短托管對(duì)象生存期的規(guī)則,盡可能地縮短固定對(duì)象的生存期
●如果對(duì)象只是暫時(shí)被固定,那影響下一次垃圾回收的機(jī)會(huì)就比較少
●你還應(yīng)該避免同時(shí)固定很多對(duì)象
●位于第2代堆或LOH中的固定對(duì)象一般不會(huì)有問題,因?yàn)橐苿?dòng)這些對(duì)象的可能性比較小

●2.9 避免使用終結(jié)方法
若非必要,永遠(yuǎn)不要實(shí)現(xiàn)終結(jié)方法(Finalizer)。終結(jié)方法是一段由垃圾回收器引發(fā)調(diào)用的代碼,用于清理非托管資源
●終結(jié)方法 Finalizer
終結(jié)方法由一個(gè)獨(dú)立的線程調(diào)用,排成隊(duì)列依次完成,而且只有在一次垃圾回收之后,對(duì)象被垃圾回收器聲明為已銷毀,才會(huì)進(jìn)行調(diào)用如果類實(shí)現(xiàn)了終結(jié)方法,對(duì)象就一定會(huì)滯留在內(nèi)存中,即便是在垃圾回收時(shí)應(yīng)該被銷毀的情況下。終結(jié)方法不僅會(huì)降低垃圾回收的整體效率,而且清理對(duì)象的過程肯定會(huì)占用CPU資源
●IDispossable
如果實(shí)現(xiàn)了終結(jié)方法,那就必須同時(shí)實(shí)現(xiàn)IDisposable接口以啟用顯式清理,還要在Dispose方法中調(diào)用GC.SuppressFinalize(this)來把對(duì)象從移除終結(jié)隊(duì)列中移除。只要能在下一次垃圾回收之前調(diào)用Dispose,那就能適時(shí)把對(duì)象清理干凈,也就不需要運(yùn)行終結(jié)方法了
●演示代碼
class Foo : IDisposable { ~Foo() { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { this.managedResource.Dispose(); } // 清理非托管資源 UnsafeClose(this.handle); // 如果基類是IDisposable // 請(qǐng)務(wù)必調(diào)用 //base.Dispose(disposing); } }
●注意:
●有些人以為終結(jié)方法肯定會(huì)被執(zhí)行到。一般情況下確實(shí)如此,但并不絕對(duì)
●如果程序被強(qiáng)行終止,就不會(huì)再運(yùn)行任何代碼,進(jìn)程也會(huì)立即被銷毀
●而且即便是在進(jìn)程正常關(guān)閉時(shí),所有終結(jié)方法的總運(yùn)行時(shí)間也是有限制的
●如果你的終結(jié)方法被排在了隊(duì)列的末尾,就有可能被忽略掉
●是逐個(gè)執(zhí)行的,如果某個(gè)終結(jié)方法陷入死循環(huán),那么排在后面的終結(jié)方法就都無法運(yùn)行了
●雖然終結(jié)方法不是運(yùn)行在GC線程中,但仍需由GC引發(fā)調(diào)用。如果沒有發(fā)生垃圾回收,那么終結(jié)方法就不會(huì)運(yùn)行

●2.10 避免分配大對(duì)象
大對(duì)象的界限被設(shè)為85 000字節(jié),判斷的依據(jù)是基于當(dāng)天的統(tǒng)計(jì)學(xué)分析。任何大于這個(gè)值的對(duì)象都被認(rèn)為是大對(duì)象,并在獨(dú)立的內(nèi)存堆中進(jìn)行分配
●應(yīng)該盡可能避免在LOH中分配內(nèi)存
不僅是因?yàn)?/span>LOH的垃圾回收開銷更大,更多原因是因?yàn)閮?nèi)存碎片會(huì)導(dǎo)致內(nèi)存用量不斷增長
●需要嚴(yán)格控制程序在LOH中的分配。
LOH中的對(duì)象應(yīng)該在整個(gè)程序的生存期都持續(xù)可用,并以池化的方式隨時(shí)待命。
●2.11 避免緩沖區(qū)復(fù)制
●任何時(shí)候都應(yīng)該避免復(fù)制數(shù)據(jù)
比如你已經(jīng)把文件數(shù)據(jù)讀入了MemoryStream(如果需要較大的緩沖區(qū),最好是用池化的流),一旦內(nèi)存分配完畢,就應(yīng)把此MemoryStream視為只讀流,所有需要訪問MemoryStream的組件都能從同一份數(shù)據(jù)備份中讀取數(shù)據(jù) 如果需要表示整個(gè)緩沖區(qū)的一段,請(qǐng)使用ArraySegment<T>類,可用來代表底層byte[]類型緩沖區(qū)的一部分區(qū)域。此ArraySegment可以傳給API,而與原來的流無關(guān),甚至可以被綁定到一個(gè)新的MemoryStream對(duì)象上。這些過程都不會(huì)發(fā)生數(shù)據(jù)復(fù)制
●代碼
var memoryStream = new MemoryStream(); var segment = new ArraySegment(memoryStream.GetBuffer(), 100, 1024); …… var blockStream = new MemoryStream(segment.Array, segment.Offset, segment.Count);
●優(yōu)化方法
內(nèi)存復(fù)制造成的最大影響肯定不是CPU,而是垃圾回收。如果你發(fā)現(xiàn)自己有復(fù)制緩沖區(qū)的需求,那就盡量把數(shù)據(jù)復(fù)制到另一個(gè)池化的或已存在的緩沖區(qū)中,以避免發(fā)生新的內(nèi)存分配
●2.12 對(duì)長期存活對(duì)象和大型對(duì)象進(jìn)行池化
●池化實(shí)際上是一種人工的內(nèi)存管理策略,但在這種場合卻真的收效甚佳
●強(qiáng)烈推薦池化的對(duì)象,就是在LOH中分配的對(duì)象,典型例子就是集合類對(duì)象
●池化的方法沒有一定之規(guī),也沒有標(biāo)準(zhǔn)的API可用,確實(shí)只能自己開發(fā),可以針對(duì)整個(gè)應(yīng)用,也可以只為特定的池化對(duì)象服務(wù)
●簡單的池化代碼
interface IPoolableObject : IDisposable { int Size { get; } void Reset(); void SetPoolManager(PoolManager poolManager); } class PoolManager { private class Pool { public int PooledSize { get; set; } public int Count { get { return this.Stack.Count; } } public Stack Stack { get; private set; } public Pool() { this.Stack = new Stack(); } } const int MaxSizePerType = 10 * (1 << 10); // 10 MB Dictionary pools = new Dictionary(); public int TotalCount { get { int sum = 0; foreach (var pool in this.pools.Values) { sum += pool.Count; } return sum; } } public T GetObject() where T : class, IPoolableObject, new() { Pool pool; T valueToReturn = null; if (pools.TryGetValue(typeof(T), out pool)) { if (pool.Stack.Count > 0) { valueToReturn = pool.Stack.Pop() as T; } } if (valueToReturn == null) { valueToReturn = new T(); } valueToReturn.SetPoolManager(this); return valueToReturn; } public void ReturnObject<T>(T value) where T : class, IPoolableObject, new() { Pool pool; if (!pools.TryGetValue(typeof(T), out pool)) { pool = new Pool(); pools[typeof(T)] = pool; } if (value.Size + pool.PooledSize < MaxSizePerType) { pool.PooledSize += value.Size; value.Reset(); pool.Stack.Push(value); } } } class MyObject : IPoolableObject { private PoolManager poolManager; public byte[] Data { get; set; } public int UsableLength { get; set; } public int Size { get { return Data!= null?Data.Length : 0; } } void IPoolableObject.Reset() { UsableLength = 0; } void IPoolableObject.SetPoolManager( PoolManager poolManager) { this.poolManager = poolManager; } public void Dispose() { this.poolManager.ReturnObject(this); } }
●注意事項(xiàng)
●在每次把池化對(duì)象歸還共享池時(shí),你的代碼必須把對(duì)象重置為已知的、安全的狀態(tài)
●池化對(duì)象的回收也是一件特別棘手的事情,因?yàn)槟悴皇钦娴囊N毀內(nèi)存(這也是池化的全部意義所在),但你必須能通過可用空間表示出“空集合”的概念
●還有一條策略就是,為你的可池化類實(shí)現(xiàn)終結(jié)方法,以作為保險(xiǎn)機(jī)制。如果終結(jié)方法得以運(yùn)行,就意味著Dispose沒被調(diào)用過,也就是存在錯(cuò)誤。這時(shí)可以把信息寫入日志,可以讓程序異常終止,或者是把錯(cuò)誤信息顯示出來
●共享池的尺寸應(yīng)該限定邊界(字節(jié)數(shù)或是對(duì)象數(shù)),只要超過了規(guī)定大小,就應(yīng)該把對(duì)象扔給GC進(jìn)行清理
●2.13 減少LOH的碎片整理
●如果做不到完全避免LOH分配,那你就應(yīng)該盡力避免碎片整理
●2.14 某些場合可以強(qiáng)制執(zhí)行完全回收
●默認(rèn)不應(yīng)該干擾GC
除了GC正常的調(diào)度計(jì)劃中安排的之外,你不應(yīng)該再強(qiáng)制執(zhí)行完全垃圾回收。那樣會(huì)干擾垃圾回收器的自動(dòng)調(diào)優(yōu)活動(dòng),還可能導(dǎo)致整體性能下降
●高性能系統(tǒng)中的考慮
為了避免以后發(fā)生不合時(shí)宜的完全垃圾回收過程,在某個(gè)更合適的時(shí)間段強(qiáng)制執(zhí)行一次完全回收也許會(huì)有所收益
●你采用了低延遲GC模式。這種模式下內(nèi)存堆的大小可能會(huì)一直增長,需要適時(shí)進(jìn)行一次完全垃圾回收。關(guān)于低延遲GC模式,請(qǐng)閱讀本章前面的相關(guān)內(nèi)容。
●偶爾你會(huì)創(chuàng)建大量對(duì)象,并會(huì)存活很長時(shí)間(理想狀態(tài)是一直保持存活)。這時(shí)最好是把這些對(duì)象盡快提升到第2代內(nèi)存堆中。如果這些對(duì)象覆蓋了即將成為垃圾的其他對(duì)象,通過一次強(qiáng)制垃圾回收就能立即銷毀這些垃圾對(duì)象
●你正處于要對(duì)LOH進(jìn)行碎片整理的狀態(tài)
●GC.Collect
參數(shù)為需要回收的代數(shù),即可執(zhí)行完全垃圾回收。此外還可以附帶一個(gè)參數(shù),值為GCCollectionMode枚舉,指明完全回收的時(shí)間由GC決定。參數(shù)值有3種可能
●Default——立即進(jìn)行強(qiáng)制完全回收
●Forced——由垃圾回收器立即啟動(dòng)完全回收
●Forced——由垃圾回收器立即啟動(dòng)完全回收
●GC.Collect(2); ?=》GC.Collect(2, GCCollectionMode.Forced);
●2.15 必要時(shí)對(duì)LOH進(jìn)行碎片整理
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
●2.16 在垃圾回收之前獲得通知
如果你的應(yīng)用程序絕對(duì)不能受到第2代垃圾回收的破壞,那么可以讓GC在即將執(zhí)行完全垃圾回收時(shí)通知你。這樣你就有機(jī)會(huì)暫停程序的運(yùn)行,也許是停止向這臺(tái)主機(jī)發(fā)送請(qǐng)求,或者是讓你的應(yīng)用程序進(jìn)入更合適的狀態(tài)
●以下條件成立時(shí),你才能從垃圾回收通知中受益
●完全垃圾回收的開銷過大,以至于程序在正常運(yùn)行期間無法承受
●你可以完全停止程序的運(yùn)行(也許這時(shí)的工作可以由其他計(jì)算機(jī)或處理器承擔(dān))
●你可以迅速停止程序運(yùn)行(停止運(yùn)行的過程不會(huì)比真正執(zhí)行垃圾回收的時(shí)間更久,你就不會(huì)浪費(fèi)更多的時(shí)間)
●第2代垃圾回收很少發(fā)生,因此執(zhí)行一次還是劃算的
●2.17 用弱引用作為緩存
弱引用(Weak Reference)指向的對(duì)象允許被垃圾回收器清理。與之相反,強(qiáng)引用(Strong Reference)會(huì)完全阻止所指對(duì)象被垃圾回收
●2.18 評(píng)估和研究垃圾回收性能









●2.18.9 內(nèi)存碎片的產(chǎn)生時(shí)機(jī)
●2.18.10 對(duì)象位于第幾代內(nèi)存堆中
●2.18.11 第0代內(nèi)存堆中存活著哪些對(duì)象
●2.18.12 誰在顯式調(diào)用GC.Collect方法
●2.18.13 進(jìn)程中存在哪些弱引用

●小結(jié)
●為了能讓應(yīng)用程序真正獲得性能的優(yōu)化,你需要深入了解垃圾回收的過程
●請(qǐng)為應(yīng)用程序選擇正確的配置參數(shù),比如在獨(dú)占主機(jī)時(shí)選用服務(wù)器模式的垃圾回收機(jī)制
●請(qǐng)盡量縮短對(duì)象的生存期,減少內(nèi)存分配次數(shù)。把那些生存期必須長于平均垃圾回收頻率的對(duì)象全部都進(jìn)行池化,或者讓它們?cè)诘?/span>2代內(nèi)存堆中永久性存活下去
●盡可能避免對(duì)象固定和使用終結(jié)方法。所有LOH中的內(nèi)存分配都應(yīng)該池化并維持永久存活,以避免發(fā)生完全垃圾回收
●讓對(duì)象維持統(tǒng)一大小,偶爾也適時(shí)進(jìn)行一次碎片整理,以減少LOH中的內(nèi)存碎片
●為了避免不合時(shí)宜的完全垃圾回收對(duì)應(yīng)用程序的影響,可以考慮使用垃圾回收通知
●垃圾回收器的行為是確定可控的,通過仔細(xì)調(diào)整對(duì)象分配頻率和生存期,你就可以控制垃圾回收器的行為

本文使用 文章同步助手 同步