Golang——垃圾回收GC(2)
1 垃圾回收中的重要概念
1.1 定義
In computer science, garbage collection (GC) is a form of automatic memory management. The garbage collector, or just collector, attempts to reclaim garbage, or memory occupied by objects that are no longer in use by the program. Garbage collection was invented by John McCarthy around 1959 to simplify manual memory management in Lisp. (引用自維基百科))
1.2 GC性能的評(píng)價(jià)標(biāo)準(zhǔn)
-
吞吐量:是指單位時(shí)間內(nèi)是有多少時(shí)間是用來(lái)運(yùn)行user application的。GC占用的時(shí)間過(guò)多,就會(huì)導(dǎo)致吞吐量較低。
-
最大暫停時(shí)間:基本上所有的垃圾回收算法,都會(huì)在執(zhí)行GC的過(guò)程中,暫停user application。如果暫停時(shí)間過(guò)長(zhǎng),必然會(huì)影響用戶體驗(yàn),尤其是那些交互性較強(qiáng)的應(yīng)用。
-
堆使用效率:影響堆使用效率的主要有兩個(gè)因素,一個(gè)是對(duì)象頭部大小,一個(gè)是堆的用法。一般來(lái)說(shuō),堆的頭部越大,存儲(chǔ)的信息越多,那么GC的效率就會(huì)越高,吞吐量什么的也會(huì)有更佳的表現(xiàn)。但是,我們必須明白,對(duì)象頭越小越好。另外,不同的算法對(duì)于堆的不同用法,也會(huì)導(dǎo)致堆使用效率差距非常大。比如復(fù)制算法,用戶應(yīng)用只能使用一般的堆大小。GC是自動(dòng)管理內(nèi)存的,如果因?yàn)镚C導(dǎo)致過(guò)量占用堆,那么就是本末倒置了。
-
訪問(wèn)的局部性:具有引用關(guān)系的對(duì)象之間很可能存在連續(xù)訪問(wèn)的情況。因此,把具有引用關(guān)系的對(duì)象安排在堆中較勁的位置,可以充分利用內(nèi)存訪問(wèn)局部性。有的GC算法會(huì)根據(jù)引用關(guān)系重排對(duì)象,比如復(fù)制算法。
-
等等等等
?
設(shè)計(jì)垃圾回收算法時(shí),折中無(wú)處不在。較大的吞吐量和較短的最大暫停時(shí)間往往不可兼得。
2 常見的GC算法
?
2.1 標(biāo)記-清除算法 STW
?
mark_sweep(){????mark()sweep()}分兩個(gè)階段,整個(gè)過(guò)程需要STW:
?
-
mark phase:時(shí)間復(fù)雜度跟活動(dòng)對(duì)象數(shù)量成正比
-
sweep phase:時(shí)間復(fù)雜度跟堆的大小成正比
?
sweep階段回收的空間會(huì)連接到空閑鏈表上,分配空間時(shí)從空閑鏈表分配,因此分配空間時(shí)間復(fù)雜度可能會(huì)有點(diǎn)高,即需要遍歷整個(gè)空閑鏈表。
?
優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單,并且與保守式GC兼容(即沒(méi)有移動(dòng)對(duì)象)
?
缺點(diǎn)也非常明顯:
?
-
存在內(nèi)存碎片
-
分配速度較慢:使用多個(gè)空閑鏈表
-
與寫時(shí)復(fù)制技術(shù)不兼容:位圖標(biāo)記法
-
sweep操作時(shí)間復(fù)雜度同堆大小成正比:延遲清除法
?
延遲清除法:沒(méi)有空閑鏈表。定義一個(gè)全局變量sweeping,從sweeping開始遍歷分配新空間,遍歷的開始位置處于上一次lazy_sweep操作發(fā)現(xiàn)的分塊的右邊。如果當(dāng)前分塊mark=true,則取消標(biāo)記。如果mark=false并且分開大小大于申請(qǐng)空間,則分配給他。 當(dāng)遍歷到堆末尾時(shí),需要將sweeping設(shè)置為heap_start,并且需要重新標(biāo)記。
?
2.2 引用計(jì)數(shù)法
?
在標(biāo)記-清除等GC算法中,沒(méi)有分塊可用時(shí),mutator會(huì)調(diào)用下面的函數(shù)啟動(dòng)GC回收空間,以便進(jìn)行分配:
?
garbage_collect(){... }然而引用計(jì)數(shù)法是沒(méi)有啟動(dòng)GC的語(yǔ)句的,它與mutator的執(zhí)行密切相關(guān),它在mutator的執(zhí)行過(guò)程中通過(guò)增減引用計(jì)數(shù)器的值來(lái)實(shí)現(xiàn)實(shí)時(shí)的內(nèi)存管理和垃圾回收。
?
引用計(jì)數(shù)器的更新主要有兩個(gè)場(chǎng)景:
?
-
分配新對(duì)象時(shí)
-
更新指針時(shí)
?
如果引用計(jì)數(shù)值變?yōu)?,則會(huì)立即連接到空閑鏈表上去。分配空間時(shí),從空閑鏈表分配,分配失敗,則直接失敗返回。
?
優(yōu)點(diǎn)是:
?
-
最大暫停時(shí)間短
-
可即刻回收垃圾
?
缺點(diǎn)是:
?
-
引用計(jì)數(shù)值的增減處理繁重,吞吐量低
-
循環(huán)引用
-
計(jì)數(shù)器占用很多位
2.3 復(fù)制算法 STW
?
GC復(fù)制算法將堆空間分成大小相等的兩塊:from空間和to空間。新對(duì)象只能從from空間分配。
?
當(dāng)from空間沒(méi)有可用空間時(shí),則會(huì)啟動(dòng)GC,將from空間的活動(dòng)對(duì)象復(fù)制到to空間,同時(shí)將from和to的身份互換。因此其時(shí)間復(fù)雜度,與活動(dòng)對(duì)象的數(shù)量成正比,而與堆的大小無(wú)關(guān)。
?
復(fù)制時(shí),是從根對(duì)象遞歸遍歷復(fù)制過(guò)去的。因此,我們定義了一個(gè)free變量記錄下從這個(gè)位置分配可用空間,分配空間的時(shí)間復(fù)雜度為常數(shù)。
?
優(yōu)點(diǎn):
?
-
因?yàn)闀r(shí)間復(fù)雜度與活動(dòng)對(duì)象數(shù)量成正比,而與堆大小無(wú)關(guān)。所以,吞吐量?jī)?yōu)秀。
-
分配速度快
-
不會(huì)發(fā)生碎片化
-
訪問(wèn)的局部性原理
?
缺點(diǎn)是:
?
-
堆的使用效率低下
-
移動(dòng)對(duì)象,與保守式GC不兼容
?
2.4 標(biāo)記-壓縮算法 STW
?
標(biāo)記-壓縮算法時(shí)將標(biāo)記-清除和復(fù)制算法相結(jié)合的產(chǎn)物。
?
標(biāo)記階段跟標(biāo)記-清除算法一樣,從根引用的活動(dòng)變量開始遍歷。時(shí)間復(fù)雜度同活動(dòng)對(duì)象的數(shù)量成正比。
?
而壓縮階段則需要遍歷整個(gè)堆,按照之前的排列順序壓縮到堆的一側(cè)去。
?
壓縮階段需要遍歷三次堆:
?
-
第一次遍歷,需要找出計(jì)算出所有的活動(dòng)對(duì)象需要移動(dòng)到哪個(gè)位置去
-
第二次遍歷,需要重寫根指針,重寫活動(dòng)對(duì)象的指針
-
第三次遍歷,移動(dòng)對(duì)象到目標(biāo)位置
?
優(yōu)點(diǎn)是堆的利用率很高,沒(méi)有碎片。缺點(diǎn)也是灰常的明顯,壓縮的時(shí)間復(fù)雜度太高,而且與堆的大小成正比。
?
2.5 分代垃圾回收
?
分代垃圾回收在對(duì)象中引入了“年齡”的概念,通過(guò)優(yōu)先回收容易成為垃圾的對(duì)象,提高GC的效率。
?
我們把剛生成的對(duì)象稱為新生代對(duì)象,到達(dá)一定年齡的對(duì)象稱為老年代對(duì)象。
?
在新生代空間執(zhí)行的GC,稱為minor GC。在老年代空間執(zhí)行的GC,稱為major GC。
?
堆的結(jié)構(gòu):
?
?
?
新生成對(duì)象分配的空間都是從Eden區(qū)分配的。當(dāng)Eden區(qū)滿了的時(shí)候,就會(huì)觸發(fā)minor GC,將Eden區(qū)和From區(qū)的活動(dòng)對(duì)象都復(fù)制到To,然后交互To和From的身份。
?
復(fù)制的時(shí)候如果超過(guò)一定的年齡或者To空間不足(即Eden和From的活動(dòng)對(duì)象占用的空間超過(guò)了To),那么直接復(fù)制到老年代空間。如果老年代空間不足則會(huì)觸發(fā)major GC。
?
那些大于Eden空間的對(duì)象,一般也不會(huì)直接失敗,而是直接分配到老年代空間。實(shí)際的實(shí)現(xiàn),一般是超過(guò)一定大小,則會(huì)將其分配到老年代空間。
?
新生代空間和老年代空間采用不同的GC算法。新生代采用復(fù)制算法,老年代采用標(biāo)記-壓縮算法或者標(biāo)記-清除算法。
3 Golang的垃圾回收算法
?
3.1 三色標(biāo)記法
?
golang采用的是并發(fā)的三色標(biāo)記清除算法(Tri-color marking)。
?
-
白色:還沒(méi)搜索的對(duì)象
-
灰色:正在搜索的對(duì)象
-
黑色:搜索完成的對(duì)象
?
GC開始前所有的對(duì)象都是白色對(duì)象。GC開始時(shí),會(huì)將從根能夠直接引用的對(duì)象標(biāo)記為灰色,并且放到堆棧里。
?
然后,灰色對(duì)象會(huì)被依次彈出棧,其子對(duì)象也被涂成灰色,壓入棧。當(dāng)其所有的子對(duì)象都變成灰色后,就會(huì)把這個(gè)對(duì)象涂成黑色。
?
當(dāng)GC結(jié)束時(shí),活動(dòng)對(duì)象全部為黑色對(duì)象,垃圾則為白色對(duì)象,回收白色對(duì)象即可。
?
主要分為四個(gè)階段:
?
-
root_scan:STW
-
mark...mark...mark...mark...
-
mark termination:STW
-
sweep...sweep...sweep...
?
接下來(lái)分別介紹一下上述的四個(gè)不同階段。
?
3.1.1 root scan
?
根查找階段需要STW。找出能夠從根直接引用的對(duì)象,標(biāo)記為灰色,壓入棧。
?
root_scan_phase(){????for(r?:?$roots){mark(*r)}????$gc_phase?=?GC_MARK }mark(obj){????if(obj.mark?==?false){obj.mark?=?truepush(obj,$mark_stack)} }當(dāng)我們把所有直接從根引用的對(duì)象涂成了灰色時(shí),root scan階段就結(jié)束了,mutator(即user application)會(huì)繼續(xù)執(zhí)行。
?
3.1.2 mark 和 mark termination
?
mark是分多次運(yùn)行的,即增量式的mark,不需要STW,它和mutator交替運(yùn)行。它主要是彈出棧里面的對(duì)象,將其子對(duì)象涂成灰色,壓入棧,然后把這個(gè)對(duì)象涂成黑色。重復(fù)這個(gè)過(guò)程,直到棧為空。
?
mark termination則是需要STW的。它會(huì)root_rescan,然后重新執(zhí)行mark。
?
然而,mark階段與mutator并發(fā)運(yùn)行存在一個(gè)問(wèn)題,可能誤把活動(dòng)對(duì)象當(dāng)做垃圾對(duì)象回收。
?
比如下面的情況:
第二張圖,創(chuàng)建了從黑色對(duì)象到白色對(duì)象的引用。第三張圖,刪除了從灰色對(duì)象到白色對(duì)象的引用。這個(gè)時(shí)候就會(huì)導(dǎo)致C被誤認(rèn)為垃圾而回收。
?
為了避免這種情況的發(fā)生,需要引入write barrier。
?
最常用的write barrier是由Dijkstra提出的insertion style write barrier。
?
write_barrier(obj,field,newobj){????if(newobj.mark?==?false){newobj.mark?=?truepush(newobj,$mark_stack)}*field?=?newobj }即如果新引用的對(duì)象是白色對(duì)象,則直接把它涂為灰色:
?
mark和mark termination的偽代碼為:
?
incremental_mark_phase(){????for(i?:?1...MARK_MAX){????????//?分多次mark,不需要STWif(is_empty($mark_stack)?==?false){obj?=?pop($mark_stack)????????????for(child?:?children(obj)){mark(*child)}}else{????????????//?mark?termination,需要STWfor(r?:?roots){mark(*r)}????????????while(is_empty($mark_stack)?==?false){obj?=?pop($mark_stack)????????????????for(child?:?children(obj)){mark(*child)}}????????????$gc_phase?=?GC_SWEEP$sweeping?=?$heap_startreturn}} }3.1.3 sweep
?
sweep也是分多次的,增量式的回收垃圾,跟mutator交替運(yùn)行。跟標(biāo)記-清除算法的實(shí)現(xiàn)基本一致,也是需要遍歷整個(gè)堆,將白色對(duì)象掛到空閑鏈表上,黑色對(duì)象取消mark標(biāo)記。
?
3.1.4 分配新對(duì)象
?
從空閑鏈表分配。
?
3.2 golang為什么沒(méi)有采用壓縮算法和分代算法
?
有點(diǎn)高深,想深入了解的可以參考golang-nuts上面的討論。
4 Golang垃圾回收的相關(guān)參數(shù)
?
4.1 觸發(fā)GC
?
gc觸發(fā)的時(shí)機(jī):2分鐘或者內(nèi)存占用達(dá)到一個(gè)閾值(當(dāng)前堆內(nèi)存占用是上次gc后對(duì)內(nèi)存占用的兩倍,當(dāng)GOGC=100時(shí))
?
?#?表示當(dāng)前應(yīng)用占用的內(nèi)存是上次GC時(shí)占用內(nèi)存的兩倍時(shí),觸發(fā)GCexport?GOGC=1004.2 查看GC信息
?
export?GODEBUG=gctrace=1可以查看gctrace信息。
舉例:
?
gc?1?@0.008s?6%:?0.071+2.0+0.080?ms?clock,?0.21+0.22/1.9/1.9+0.24?ms?cpu,?4->4->3?MB,?5?MB?goal,?4?P#?command-line-argumentsgc?1?@0.001s?16%:?0.071+3.3+0.060?ms?clock,?0.21+0.17/2.9/0.36+0.18?ms?cpu,?4->4->4?MB,?5?MB?goal,?4?Pgc?2?@0.016s?8%:?0.020+6.0+0.070?ms?clock,?0.082+0.094/3.9/2.2+0.28?ms?cpu,?8->9->8?MB,?9?MB?goal,?4?Pgc?3?@0.046s?7%:?0.019+7.3+0.062?ms?clock,?0.076+0.089/7.1/7.0+0.24?ms?cpu,?14->16->14?MB,?17?MB?goal,?4?Pgc?4?@0.092s?8%:?0.015+24+0.10?ms?clock,?0.060+0.10/24/0.75+0.42?ms?cpu,?25->27->24?MB,?28?MB?goal,?4?P每個(gè)字段表示什么信息可以參考?golang doc
5 如何提高GC的性能
?
Golang的GC算法是固定的,用戶無(wú)法去配置采用什么算法,也沒(méi)法像Java一樣配置年輕代、老年代的空間比例等。golang的GC相關(guān)的配置參數(shù)只有一個(gè),即GOGC,用來(lái)表示觸發(fā)GC的條件。
?
目前來(lái)看,提高GC效率我們唯一能做的就是減少垃圾的產(chǎn)生。所以說(shuō),這一章稱為提高GC的性能也不太合適。下面我們就主要討論一下,在golang中如何減少垃圾的產(chǎn)生,有哪些需要注意的方面。
5.1 golang中的內(nèi)存分配
?
參考官網(wǎng)Frequently Asked Questions (FAQ)
?
How do I know whether a variable is allocated on the heap or the stack?
From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
?
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
?
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
我們看一個(gè)例子有個(gè)直觀的認(rèn)識(shí):
?
1?package?main2?3?import?()4?5?func?foo()?*int?{6?????var?x?int7?????return?&x8?}9?10?func?bar()?int?{11?????x?:=?new(int)12?????*x?=?113?????return?*x14?}15?16?func?big()?{17?????x?:=?make([]int,0,20)18?????y?:=?make([]int,0,20000)19?20?????len?:=?1021?????z?:=?make([]int,0,len)22?}23?24?func?main()?{25??26?} #?go?build?-gcflags='-m?-l'?test.go./test.go:7:12:?&x?escapes?to?heap ./test.go:6:9:?moved?to?heap:?x ./test.go:11:13:?bar?new(int)?does?not?escape ./test.go:18:14:?make([]int,?0,?20000)?escapes?to?heap ./test.go:21:14:?make([]int,?0,?len)?escapes?to?heap ./test.go:17:14:?big?make([]int,?0,?20)?does?not?escape ./test.go:17:23:?x?declared?and?not?used ./test.go:18:23:?y?declared?and?not?used ./test.go:21:23:?z?declared?and?not?used5.2 sync.Pool對(duì)象池
?
sync.Pool主要是為了重用對(duì)象,一方面縮短了申請(qǐng)空間的時(shí)間,另一方面,還減輕了GC的壓力。不過(guò)它是一個(gè)臨時(shí)對(duì)象池,為什么這么說(shuō)呢?因?yàn)閷?duì)象池中的對(duì)象會(huì)被GC回收。所以說(shuō),有狀態(tài)的對(duì)象,比如數(shù)據(jù)庫(kù)連接是不能夠用sync.Pool來(lái)實(shí)現(xiàn)的。
use sync.Pool if you frequently allocate many objects of the same type and you want to save some allocation and garbage collection overhead. However, in the current implementation, any unreferenced sync.Pool objects are removed at each garbage collection cycle, so you can't use this as a long-lived free-list of objects. If you want a free-list that maintains objects between GC runs, you'll still have to build that yourself. This is only to reuse allocated objects between garbage collection cycles. ?
sync.Pool主要有兩個(gè)方法:
?
func?(p?*Pool)?Get()?interface{}?{... }func?(p?*Pool)?Put(x?interface{})?{... }Get方法是指從臨時(shí)對(duì)象池中申請(qǐng)對(duì)象,put是指把不再使用的對(duì)象返回對(duì)象池,以便后續(xù)重用。如果我們?cè)谑褂肎et申請(qǐng)新對(duì)象時(shí)pool中沒(méi)有可用的對(duì)象,那么就會(huì)返回nil,除非設(shè)置了sync.Pool的New func:
?
type?Pool?struct?{...????//?New?optionally?specifies?a?function?to?generate//?a?value?when?Get?would?otherwise?return?nil.//?It?may?not?be?changed?concurrently?with?calls?to?Get.New?func()?interface{} }另外,我們不能對(duì)從對(duì)象池申請(qǐng)到的對(duì)象值做任何假設(shè),可能是New新生成的,可能是被某個(gè)協(xié)程修改過(guò)放回來(lái)的。
一個(gè)比較好的使用sync.Pool的例子:
var?DEFAULT_SYNC_POOL?*SyncPoolfunc?NewPool()?*SyncPool?{DEFAULT_SYNC_POOL?=?NewSyncPool(5,?????30000,?2,?????)return?DEFAULT_SYNC_POOL }func?Alloc(size?int)?[]int64?{return?DEFAULT_SYNC_POOL.Alloc(size) }func?Free(mem?[]int64)?{DEFAULT_SYNC_POOL.Free(mem) }//?SyncPool?is?a?sync.Pool?base?slab?allocation?memory?pool type?SyncPool?struct?{classes?????[]sync.PoolclassesSize?[]intminSize?????intmaxSize?????int }func?NewSyncPool(minSize,?maxSize,?factor?int)?*SyncPool?{n?:=?0for?chunkSize?:=?minSize;?chunkSize?<=?maxSize;?chunkSize?*=?factor?{n++}pool?:=?&SyncPool{make([]sync.Pool,?n),make([]int,?n),minSize,?maxSize,}n?=?0for?chunkSize?:=?minSize;?chunkSize?<=?maxSize;?chunkSize?*=?factor?{pool.classesSize[n]?=?chunkSizepool.classes[n].New?=?func(size?int)?func()?interface{}?{return?func()?interface{}?{buf?:=?make([]int64,?size)return?&buf}}(chunkSize)n++}return?pool }func?(pool?*SyncPool)?Alloc(size?int)?[]int64?{if?size?<=?pool.maxSize?{for?i?:=?0;?i?<?len(pool.classesSize);?i++?{if?pool.classesSize[i]?>=?size?{mem?:=?pool.classes[i].Get().(*[]int64)//?return?(*mem)[:size]return?(*mem)[:0]}}}return?make([]int64,?0,?size) }func?(pool?*SyncPool)?Free(mem?[]int64)?{if?size?:=?cap(mem);?size?<=?pool.maxSize?{for?i?:=?0;?i?<?len(pool.classesSize);?i++?{if?pool.classesSize[i]?>=?size?{pool.classes[i].Put(&mem)return}}} }有一個(gè)開源的通用golang對(duì)象池實(shí)現(xiàn),有興趣的可以研究一下:Go Commons Pool,在此不再贅述。
?
5.3 append
?
我們先看一下append的基本用法。
?
nums:=make([]int,0,10)創(chuàng)建切片,len=0,cap=10,底層實(shí)際上分配了10個(gè)元素大小的空間。在沒(méi)有append數(shù)據(jù)的情況下,不能直接使用nums[index]。
?
?
nums:=make([]int,5,10)創(chuàng)建切片,len=5,cap=10,底層實(shí)際上分配了10個(gè)元素大小的空間。在沒(méi)有append數(shù)據(jù)的情況下,可以直接使用nums[index],index的范圍是[0,4]。執(zhí)行append操作的時(shí)候是從index=5的位置開始存儲(chǔ)的。
?
?
nums?:=?make([]int,5)如果沒(méi)有指定capacity,那么cap與len相等。
?
nums?=?append(nums,10)執(zhí)行append操作的時(shí)候,nums的地址可能會(huì)改變,因此需要利用其返回值重新設(shè)置nums。至于nums的地址會(huì)不會(huì)改變,取決于還沒(méi)有空間來(lái)存儲(chǔ)新的數(shù)據(jù),如果沒(méi)有空閑空間了,那就需要申請(qǐng)cap*2的空間,將數(shù)據(jù)復(fù)制過(guò)去。
?
因此,我們?cè)谑褂胊ppend操作的時(shí)候,最好是設(shè)置一個(gè)比較合理的cap值,即根據(jù)自己的應(yīng)用場(chǎng)景預(yù)申請(qǐng)大小合適的空間,避免無(wú)謂的不斷重新申請(qǐng)新空間,這樣可以減少GC的壓力。
?
由append導(dǎo)致的內(nèi)存飆升和GC壓力過(guò)大這個(gè)問(wèn)題,需要特別注意一下。
?
總結(jié)
以上是生活随笔為你收集整理的Golang——垃圾回收GC(2)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 电力电子技术笔记-逆变电路
- 下一篇: 演化博弈论简介(转)