常見垃圾回收機制
引用計數(shù)
對每個對象維護一個引用計數(shù),當引用對象的對象被銷毀時贸伐,引用計數(shù)-1新蟆,如果引用計數(shù) 為0觅赊,則進行垃圾回收
優(yōu)點:對象可以很快的被回收,不會出現(xiàn)內(nèi)存耗盡或達到某個閥值時才回收琼稻。
缺點:不能很好的處理循環(huán)引用吮螺,而且實時維護引用計數(shù),有也一定的代價帕翻。
代表語言:Python鸠补、PHP、Swift
標記-清除
從根變量開始遍歷所有引用的對象熊咽,引用的對象標記為"被引用"莫鸭,沒有被標記的進行回 收。
優(yōu)點:解決了引用計數(shù)的缺點横殴。
缺點:需要STW被因,即要暫時停掉程序運行。
代表語言:Golang(其采用三色標記法)
分代收集
按照對象生命周期長短劃分不同的代空間衫仑,生命周期長的放入老年代梨与,而短的放入新生代,不同代有不能的回收算法和回收頻率文狱。
優(yōu)點:回收性能好
缺點:算法復(fù)雜
代表語言: JAVA
Golang的GC演變
Go V1.3之前的標記-清除(mark and sweep)算法
主要流程:
1粥鞋、暫停業(yè)務(wù)邏輯(啟動STW);
2、開始標記可達對象;
3瞄崇、清除未標記對象;
4呻粹、停止STW,讓程序繼續(xù)跑苏研。
將STW的步驟提前了異步等浊,因為在Sweep清除的時候,可以不需要STW停止摹蘑,因為這些對象已經(jīng)是不可達對象了筹燕,不會出現(xiàn)回收寫沖突等問題。但是無論怎么優(yōu)化,Go V1.3都面臨這個一個重要問題撒踪,就是mark-and-sweep 算法會暫停整個程序过咬。Go V1.3都面臨這個一個重要問題,就是mark-and-sweep 算法會暫停整個程序制妄。
Go V1.5的三色并發(fā)標記法
三色分別指的是:
白色標記表:所在的span的gcmarkBits中對應(yīng)的bit為0
灰色標記表:所在的span的gcmarkBits中對應(yīng)的bit為1掸绞,并且對象在標記隊列中
黑色標記表: 所在的span的gcmarkBits中對應(yīng)的bit為1,并且對象已經(jīng)從標記隊列中取出并處理
三色標記的過程
1.開始標記忍捡,只要是新創(chuàng)建的對象集漾,默認顏色都是標記為"白色",以上圖為例砸脊,當前對象1- 7都是白色
2.從程序(對象根節(jié)點)出發(fā)具篇,即從上圖的程序出發(fā),開始遍歷所有對象凌埂,把遍歷到的對象從白色集合放入到灰色集合驱显,當前對象1,對象4為灰瞳抓,其余為白
3.遍歷灰色集合埃疫,將灰色對象應(yīng)用的對象從白色集合放入灰色集合,之后將原灰色集合放 入黑色集合孩哑,當前1栓霜,4為黑,2横蜒,7為灰胳蛮,3,5丛晌,6為白
- 重復(fù)第三步仅炊,直到灰色表中無任何對象
- 回收所有白色標記的對象,也就是回收垃圾
沒有STW的三色標記法
基于上述的三色并發(fā)標記法來說, 它是一定要依賴STW的. 因為如果不暫停程序, 程序的邏輯 改變對象引用關(guān)系, 這種動作如果在標記階段做了修改澎蛛,會影響標記結(jié)果的正確性抚垄。來看看 一個場景,如果三色標記法, 標記過程不使用STW將會發(fā)生什么事情?
最后發(fā)現(xiàn)谋逻,本來是對象4合法引用的對象3呆馁,卻被GC給“誤殺”回收掉了。 可以看出毁兆,有兩種情況智哀,在三色標記法中,是不希望被發(fā)生的荧恍。
- 條件1: 一個白色對象被黑色對象引用(白色被掛在黑色下)
- 條件2: 灰色對象與它之間的可達關(guān)系的白色對象遭到破壞(灰色同時丟了該白色)
如果當以上兩個條件同時滿足時,就會出現(xiàn)對象丟失現(xiàn)象! 并且,如圖所示的場景中脯厨,如果示例中的白色對象3還有很多下游對象的話, 也會一并都清 理掉鸭津。 為了防止這種現(xiàn)象的發(fā)生黄锤,最簡單的方式就是STW,直接禁止掉其他用戶程序?qū)ο笠?關(guān)系的干擾次氨,但是STW的過程有明顯的資源浪費,對所有的用戶程序都有很大影響摘投。 那么是否可以在保證對象不丟失的情況下合理的盡可能的提高GC效率煮寡,減少STW時間呢? 答案是可以的,我們只要使用一種機制犀呼,嘗試去破壞上面的兩個必要條件就可以了幸撕。
屏障機制
GC的觸發(fā)
memstats.heap_live >= memstats.gc_trigger //heap_live is the number of bytes considered live by the GC
其中memstats.gc_trigger的計算公式是:
trigger = unit64(float64(memstats.heap_marked)*(1+triggerRatio))//heap_marked
is the number of bytes marked by the previous GC tiggerRatio 與goalGrowthRatio正相關(guān)
goalGrowthRatio = float64(gcpercent)/100
公式中的goalGrowthRatio“目標Heap增長率”通過設(shè)置環(huán)境變量GOGC(gcpercent)調(diào)整,默認值為100外臂。 比如當前程序使用4M堆內(nèi)存坐儿,即memstats.heap_marked內(nèi)存為4M,當程序占用的內(nèi)存 上升到memstats.heap_marked*(1+GOGC/100)=8M時候宋光,gc就會被觸發(fā)貌矿,開始進行相關(guān)的gc操作。
因此罪佳,可以通過GOGC 來做參數(shù)調(diào)優(yōu)逛漫。該參數(shù)主要控制的是下一次 gc開始的時候的內(nèi)存使用量。如何對 GOGC 的參數(shù)進行設(shè)置赘艳,要根據(jù)生產(chǎn)情況中的實際場景來定酌毡,比如 GOGC 參數(shù)提升,來減少 GC 的頻率第练。
主動:默認2min觸發(fā)一次gc阔馋,src/runtime/proc.go:forcegcperiod
被動:runtime.gc()
性能優(yōu)化
1、能返回實例值的函數(shù)就別返回指針娇掏。 go里有獨特的逃逸機制呕寝,指針的返回值一定會發(fā)生變量逃逸,逃逸的行為被理解為從棧中 跑到了堆中婴梧,而堆中的gc是相對較慢的下梢。返回值類型,會在內(nèi)聯(lián)處理后銷毀塞蹭,返回指針類型孽江,會逃逸到堆上,增加gc壓力番电。 這不僅應(yīng)用于函數(shù)返回值情況岗屏,如果有可能的話辆琅,盡量程序中還是要少用指針,因為指針變量在gc的時候會導(dǎo)致二次遍歷这刷,使得整個gc變慢婉烟。
逃逸分析:
一般來說,在程序中全局變量暇屋、內(nèi)存占用大的局部變量似袁、發(fā)生了逃逸的局部變量存在的地方就是堆,這一塊內(nèi)存沒有特定的結(jié)構(gòu)咐刨,也沒有固定的大小昙衅,可以根據(jù)需要進行調(diào)整。 簡單來說有大量數(shù)據(jù)要存的時候定鸟,就存在堆里面而涉。堆是進程級別的。當一個變量需要分配在堆上的時候仔粥,開銷會比較大婴谱,對于go這種帶GC的語言來說,也會增加gc壓力躯泰,同時也容易造成內(nèi)存碎片谭羔。
golang逃逸分析最基本的原則是:如果一個函數(shù)返回的是一個(局部)變量的地址,那么 這個變量就發(fā)生逃逸 麦向。
在golang里面瘟裸,變量分配在何處和是否使用new無關(guān),意味著程序猿無法手動指定某個變 量必須分配在棧上或者堆上诵竭,所以我們需要通過一些方法來確定某個變量到底是分配在了 棧上還是堆上话告。
package main
func main() {
a := f1()
*a++ }
//go:noinline func f1() *int {
i := 1
return &i }
附帶一個逃逸分析的指令: go build -gcflags '-m' main.go 以上逃逸分析結(jié)果:
go build -gcflags '-m' escape.go # command-line-arguments
./escape.go:3:6: can inline main
./escape.go:11:9: &i escapes to heap
./escape.go:10:2: moved to heap: i
能引起變量逃逸到堆上的典型情況
- 在方法內(nèi)把局部變量指針返回: 局部變量原本應(yīng)該在棧中分配,在棧中回收卵慰。但是由于 返回時被外部引用沙郭,因此其生命周期大于棧,則溢出裳朋。
- 發(fā)送指針或帶有指針的值到 channel 中病线。 在編譯時是沒有辦法知道哪個 goroutine 會在 channel 上接收數(shù)據(jù)。所以編譯器沒法知道變量什么時候才會被釋放鲤嫡。
- 在一個切片上存儲指針或帶指針的值送挑。 一個典型的例子就是 []*string 。這會導(dǎo)致切片的內(nèi)容逃逸暖眼。盡管其后面的數(shù)組可能是在棧上分配的惕耕,但其引用的值一定是在堆上。
- slice的背后數(shù)組被重新分配了诫肠,因為 append 時可能會超出其容量( cap )司澎。 slice 初始化的地方在編譯時是可以知道的欺缘,它最開始會在棧上分配。如果切片背后的存儲要基于運行時的數(shù)據(jù)進行擴充就會在堆上分配惭缰。
- 在 interface 類型上調(diào)用方法浪南。在 interface 類型上調(diào)用方法都是動態(tài)調(diào)度的 —— 方 法的真正實現(xiàn)只能在運行時知道。想像一個 io.Reader 類型的變量 r , 調(diào)用 r.Read(b) 會使得 r 的值和切片b 的背后存儲都逃逸掉漱受,所以會在堆上分配。
逃逸分析
package main import "fmt"
func main() {
}
ch := make(chan *int, 10) f1(ch)
f2(ch)
func f1(ch chan *int) { a := 1
ch <- &a }
func f2(ch chan *int) { data := <- ch
fmt.Println(data)
}
./escape.go:7:4: &a escapes to heap 驗證2 ./escape.go:8:4: data escapes to heap 驗證5
###對象重用
深度很大的循環(huán)或遞歸中骡送,防止爆棧昂羡。
for i:=1;i<100000;i++{ tmp := t{}
} return
var tmp t
for i:=1;i<100000;i++{
tmp = t{} }
return
在循環(huán)和遞歸里,聲明臨時變量會被存進棧中摔踱,只要不涉及到指針的逃逸虐先,棧上的內(nèi)存在該函數(shù)return后就會釋放。那么如果是已知深度比較淺的循環(huán)(遞歸)內(nèi)部派敷,是不介意出現(xiàn):= 操作的蛹批。
但是在無法預(yù)估for 或者遞歸的深度時,如果深度很大篮愉,循環(huán)了千萬次腐芍,重點推薦第二種方法,雖然函數(shù)return后釋放該函數(shù)在棧上開辟的變量试躏,但是降低了爆棧的風(fēng)險猪勇。 原因很簡單,對于GC開發(fā)者不可能讓一個對象占用100M內(nèi)存跟一萬個對象占用100M 內(nèi)存同樣消耗性能颠蕴,顯然那一個占用100M內(nèi)存的對象泣刹,當發(fā)現(xiàn)它不需要回收的話,我就不需要做什么事情了犀被,而那一萬個對象椅您,我需要逐個檢查是否還有被引用,所以該場景下內(nèi)存大小不是關(guān)鍵寡键,對象數(shù)量才是關(guān)鍵掀泳。
總而言之就是盡量減少對象分配,盡量做到對象的重用昌腰。
對于做到對象重用這一點开伏,為了減少GC,golang提供了對象重用的機制遭商,也就是 sync.Pool對象池固灵。 sync.Pool是可伸縮的,并發(fā)安全的劫流。其大小僅受限于內(nèi)存的大小巫玻,可以被看作是一個存放可重用對象的值的容器丛忆。 設(shè)計的目的是存放已經(jīng)分配的但是暫時不用的對象,在需要用到的時候直接從pool中取仍秤。 任何存放區(qū)其中的值可以在任何時候被刪除而不通知熄诡,在高負載下可以動態(tài)的擴容,在不活躍時對象池會收縮诗力。
合理的選擇數(shù)據(jù)結(jié)構(gòu)
我們知道在有數(shù)據(jù)頻繁插入和刪除的場景下凰浮,slice可以擴容但是要收縮就比較麻煩了,于是想到了鏈表苇本,鏈表要刪除單個節(jié)點的時候袜茧,只需要把節(jié)點從鏈表上斷開,不需要復(fù)制數(shù) 據(jù)瓣窄,效率高于數(shù)組結(jié)構(gòu)笛厦。這里直觀的表示一下兩種數(shù)據(jù)結(jié)構(gòu)的區(qū)別:
type MyData1 struct {
next *MyData
Id int
Name string }
var mydata1 *MyData1
type MyData2 struct {
Id int
Name string }
var mydata2 []MyData2
面示例代碼的mydata1用的是鏈表結(jié)構(gòu),每個節(jié)點都有一個指向下一個節(jié)點的指針俺夕,想像下存儲1萬個對象到mydata1裳凸,是不是需要創(chuàng)建1萬個MyData1類型的對象。 示例中的mydata2用的是slice結(jié)構(gòu)劝贸,一個slice就是一個對象姨谷,其中的元素都是這一塊內(nèi)存中的值,而不是對象悬荣,需要注意 []MyData2 和 []*MyData2 是不一樣的菠秒,如果換用第二種 寫法,那么每個元素一樣都是一個對象氯迂,因為這時候slice存的不是值而是指向?qū)ο蟮闹羔樇@些指針每一個都分別指到一個對象。
所以有些情況下嚼蚀,采用 []MyData2這種方式比使用鏈表會大大減少對象數(shù)目禁灼,雖然可能帶來內(nèi)存占用增大,但是會大大降低GC帶來的性能損耗轿曙。
排查和定位GC問題
GODEBUG gctrace =1 go run main.go