编写高性能 .NET 代码 第二章:垃圾回收 基本操作
基本操作
垃圾回收的算法細(xì)節(jié)還在不斷完善中,性能還會(huì)有進(jìn)一步的提升。下文介紹的內(nèi)容在不同的.NET版本里會(huì)略有不同,但大方向是不會(huì)有變動(dòng)的。
在.net進(jìn)程里會(huì)管理2個(gè)類(lèi)型的內(nèi)存堆:托管和非托管。本地代碼申請(qǐng)的,以及由CLR申請(qǐng)的都是非托管內(nèi)存,使用Windows API 的 VirtualAlloc 方法進(jìn)行申請(qǐng)。CLR里分配的托管對(duì)象則分配在托管堆里,這些對(duì)象可以被垃圾回收處理。
在托管堆里有還進(jìn)一步分為小對(duì)象對(duì)和大對(duì)象堆(LOH)。每個(gè)對(duì)象類(lèi)型都有自己的一段堆內(nèi)存段。每段的大小根據(jù)你的配置或者硬件配置有關(guān),對(duì)于一個(gè)大型的應(yīng)用,一個(gè)內(nèi)存段可到幾百M(fèi)。
小對(duì)象還可以進(jìn)一步分為3個(gè)世代。0代和1代總是在一個(gè)內(nèi)存段里,2代則可以跨越多個(gè)內(nèi)存段。包含0代和1代的內(nèi)存段稱(chēng)為暫存段。
下圖是堆的圖形,分別是A段和B段
A段是小對(duì)象堆,B段是大對(duì)象堆。2代和1代開(kāi)始只有幾個(gè)字節(jié)大小,因?yàn)樗麄兊侥壳盀橹故强盏摹?/p>
在小對(duì)象堆里分配對(duì)象存在一個(gè)3世代的生命周期,CLR在小對(duì)象堆里分配小于85000字節(jié)的對(duì)象(8.5k)。0代內(nèi)存通常在段內(nèi)存的結(jié)尾開(kāi)始分配。這就是為什么前面看到的.NET內(nèi)存分配得很快。如果快速分配失敗,則在0代邊界范圍里找一個(gè)合適的分配地址。如果也沒(méi)有合適的位置,則分配器會(huì)擴(kuò)大0代的在內(nèi)存段里邊界范圍。如果擴(kuò)展范圍時(shí)超過(guò)了內(nèi)存段的范圍,則會(huì)觸發(fā)垃圾回收。
對(duì)象創(chuàng)建后都是0代。只要對(duì)象還存在,在GC時(shí)會(huì)將它們標(biāo)記到下一代。0代和1代稱(chēng)為臨時(shí)集合。
當(dāng)觸發(fā)GC時(shí),可能會(huì)同時(shí)觸發(fā)壓縮,在這種情況下,GC會(huì)將對(duì)象移動(dòng)到另外一個(gè)內(nèi)存地址里,釋放當(dāng)前段的內(nèi)存空間。如果沒(méi)有觸發(fā)壓縮,也僅僅只是重新劃分邊界。在一些沒(méi)有觸發(fā)過(guò)壓縮的GC后,結(jié)果如下圖:
雖然對(duì)象沒(méi)有移動(dòng),但邊界有重新劃分。
壓縮可以發(fā)生在任何一代的GC里,這是一個(gè)相當(dāng)耗時(shí)的過(guò)程,因?yàn)樗枰匦赂聦?duì)象的引用關(guān)系,這需要暫停所有的線(xiàn)程操作。正因?yàn)榇鷥r(jià)高,壓縮操作只會(huì)在必要的時(shí)候才會(huì)進(jìn)行。
一旦對(duì)象到達(dá)2代,在剩下的生命周期里會(huì)一直保持。這并不意味著2代對(duì)象會(huì)一直存在,如果所有的2代對(duì)象被回收,并且整段內(nèi)存里都沒(méi)有對(duì)象,則這段內(nèi)存可以被操作系統(tǒng)回收,或者作為其他內(nèi)存段的附屬段。這通常出現(xiàn)在全回收階段。
那么對(duì)象的活著是什么概念呢?在GC時(shí)可以通過(guò)任何已知的root節(jié)點(diǎn)到達(dá)對(duì)象,能找到對(duì)象的引用關(guān)系,就說(shuō)明這個(gè)對(duì)象還活著。root節(jié)點(diǎn)可以是程序里的某個(gè)靜態(tài)變量,線(xiàn)程里正在執(zhí)行的方法(指局部對(duì)象),GC句柄(被pinned的句柄)以及在finalizer queue里的對(duì)象。請(qǐng)注意,如果你的對(duì)象在2代,并且可以被回收,但你在做0代GC時(shí),它也不會(huì)被回收。他們需要等到一個(gè)完整回收時(shí)才會(huì)被干掉。
如果0代對(duì)象已經(jīng)填充滿(mǎn)一個(gè)內(nèi)存段,再做壓縮也不能獲得足夠空間時(shí),gc會(huì)重新分配一個(gè)新的內(nèi)存段。新的段將用于放置新分配的1代和0代對(duì)象。而之前段的對(duì)象都將轉(zhuǎn)為2代對(duì)象。所有的0代對(duì)象轉(zhuǎn)為1代,1代對(duì)象同樣降為2代對(duì)象(因?yàn)椴恍枰獜?fù)制所以很容易實(shí)現(xiàn))。這個(gè)新的內(nèi)存段看起來(lái)如下:
如果2代內(nèi)存不斷增加,那么它可以存放在多個(gè)內(nèi)存段里。LOH也一樣可以跨越多個(gè)內(nèi)存段存放。但不管有多少個(gè)內(nèi)存段,0代和1代對(duì)象始終在一個(gè)內(nèi)存段里。了解到這些知識(shí)對(duì)后面的學(xué)習(xí)會(huì)很有幫助。大對(duì)象堆使用另外一種規(guī)則。通常來(lái)說(shuō)一些字符串或者數(shù)組,大小在8500直接以上會(huì)自動(dòng)分配到LOH堆里,它不會(huì)走上面的代紀(jì)模型。出于性能考慮,LOH分配的對(duì)象不會(huì)在回收的時(shí)候做壓縮過(guò)程,但從.net4.5.1開(kāi)始,你可以按需做壓縮了。就行2代對(duì)象那樣,當(dāng)對(duì)象不再LOH里需要時(shí),你還是可以回收它使用的內(nèi)存空間,但稍后我們會(huì)說(shuō)到,在垃圾回收里,最理想的狀態(tài)是不要將對(duì)象創(chuàng)建到大對(duì)象堆上。
在LOH里,分配器都是使用空閑列表的方式來(lái)尋找最合適的位置來(lái)分配,在本章的后面將探索一些技術(shù)來(lái)減少在大數(shù)據(jù)堆上減少內(nèi)存碎片。
垃圾回收特定代時(shí),會(huì)順帶回收它下面的代。例如回收1代時(shí),會(huì)把0代的也回收。如果是2代,那么就是把所有的都回收了(包括LOH里的)。如果是0代或者1代回收時(shí),程序會(huì)暫停執(zhí)行到GC過(guò)程結(jié)束。如果是回收2代,這一部分操作可以可能會(huì)在另外一個(gè)后臺(tái)線(xiàn)程里執(zhí)行,這取決于系統(tǒng)配置。
垃圾回收分為4個(gè)階段:
暫停--所有托管線(xiàn)程需要在回收開(kāi)始前暫停。
標(biāo)記--從每個(gè)root節(jié)點(diǎn)開(kāi)始,回收器會(huì)對(duì)每個(gè)對(duì)象的引用做標(biāo)記
壓縮--移動(dòng)內(nèi)存對(duì)象,并重新修改引用路徑以便釋放內(nèi)存碎片。它通常發(fā)生在小對(duì)象堆上,并在系統(tǒng)認(rèn)為需要的時(shí)候進(jìn)行,你不能手動(dòng)控制。在大對(duì)象堆上,壓縮不會(huì)自動(dòng)發(fā)生,但你可以配置垃圾回收器按需壓縮他。
恢復(fù)--托管線(xiàn)程回復(fù)執(zhí)行
標(biāo)記階段實(shí)際上不需要遍歷堆上的每個(gè)對(duì)象,它只會(huì)處理需要回收的堆。舉個(gè)栗子,做0代收集時(shí)只會(huì)考慮0代的對(duì)象,做1代收集時(shí)則會(huì)標(biāo)記0代和1代的對(duì)象。在做2代或者一次完整回收時(shí),才需要遍歷每一個(gè)活著的對(duì)象,當(dāng)然這個(gè)開(kāi)銷(xiāo)就比較高了。另外需要考慮的是,一個(gè)高代的對(duì)象可能是低代對(duì)象的root節(jié)點(diǎn),這會(huì)導(dǎo)致在做遍歷時(shí),也會(huì)遍歷相關(guān)的高代對(duì)象,當(dāng)然這個(gè)開(kāi)銷(xiāo)會(huì)比全回收階段時(shí)小一些。
上面描述的過(guò)程會(huì)導(dǎo)致以下問(wèn)題。
首先,垃圾回收所消耗的時(shí)間,取決于當(dāng)前還活著的對(duì)象數(shù)量,而不是已經(jīng)分配出去的數(shù)量。這就意味著,如果分配了100w個(gè)對(duì)象在一個(gè)root節(jié)點(diǎn)上,只要下次GC前,你把它與root切斷引用關(guān)系,這100w個(gè)對(duì)象對(duì)你的回收耗時(shí)不會(huì)造成太大影響。
其次,垃圾回收的頻率取決于特定的一代里分配了多少內(nèi)存。一旦超過(guò)內(nèi)部的一個(gè)閾值,GC將回收這一代的對(duì)象。這個(gè)過(guò)程GC會(huì)根據(jù)你的程序做動(dòng)態(tài)調(diào)整。如果在某代的回收卓有成效(回收了很多對(duì)象),則在這一代的回收會(huì)頻繁觸發(fā),反之則減少。另外一個(gè)觸發(fā)因素就是你的程序在電腦里的可用內(nèi)存。如果可用內(nèi)存低于某個(gè)閾值,GC也會(huì)頻繁發(fā)生,用來(lái)減少堆的總體體積。
從上面的描述里,你可能覺(jué)得GC已經(jīng)超過(guò)了你的控制范圍。但這其實(shí)已經(jīng)離真相不遠(yuǎn)了。最簡(jiǎn)單的優(yōu)化方式就是,你可以通過(guò)控制內(nèi)存的分配模式來(lái)達(dá)到。你在了解GC是如何工作后,你可以根據(jù)分配速率,對(duì)象的生命周期,來(lái)選擇合適的配置。
相關(guān)文章:
[翻譯]編寫(xiě)高性能 .NET 代碼 第一章:性能測(cè)試與工具 -- 選擇什么來(lái)衡量
[翻譯]編寫(xiě)高性能 .NET 代碼 第一章:性能測(cè)試與工具 -- 平均值 vs 百分比
[翻譯]編寫(xiě)高性能 .NET 代碼 第一章:工具介紹 -- Visual Studio
編寫(xiě)高性能 .NET 代碼 第一章:工具介紹 -- Performance Counters(性能計(jì)數(shù)器)
編寫(xiě)高性能 .NET 代碼 第二章:垃圾回收
原文地址:http://www.cnblogs.com/yahle/p/6552457.html
.NET社區(qū)新聞,深度好文,微信中搜索dotNET跨平臺(tái)或掃描二維碼關(guān)注
總結(jié)
以上是生活随笔為你收集整理的编写高性能 .NET 代码 第二章:垃圾回收 基本操作的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 读《代码不朽:编写可维护软件的10大要则
- 下一篇: .Net中的AOP系列之《AOP实现类型