go語言內(nèi)存管理

參考連接:

https://www.cnblogs.com/xumaojun/p/8547439.html

https://studygolang.com/articles/11904

https://making.pusher.com/golangs-real-time-gc-in-theory-and-practice/

如何測量GC

GODEBUG=gctrace=1這個環(huán)境變量可以開啟gc調(diào)試信息的打印

GODEBUG=gctrace=1 ./myserver

GOMAXPROCS

Go在運行時可能會創(chuàng)建很多線程,但任何時候僅有限的幾個線程參與并發(fā)任務(wù)執(zhí)行。該量默認(rèn)與處理器核數(shù)相等,可用runtime.GOMAXPROCS函數(shù)或者環(huán)境變量修改;

channel和鎖

通道并不是用來取代鎖的,它們有各自不同的使用場景。通道傾向于解決邏輯層次的并發(fā)處理架構(gòu),而鎖則用來保護(hù)局部范圍的數(shù)據(jù)安全

常見的垃圾回收方法

引用計數(shù)(reference counting)

每個對象維護(hù)一個引用計數(shù)器,記錄指向這個對象的引用數(shù)量。每次有一個新的引用指向這個對象,計數(shù)器加一;反之每次有一個指向這個對象引用被置空或者指向其他對象,計數(shù)器減一。當(dāng)計數(shù)器變?yōu)?0 的時候,自動刪除這個對象。c語言大多使用此方法,還有內(nèi)存泄漏掃描工具,但也難免會有疏漏

引用計數(shù)的優(yōu)點是:

* 算法易于實現(xiàn),相對簡單

* 內(nèi)存的回收及時,相比其他回收算法,堆耗盡或者達(dá)到某個閾值才會進(jìn)行垃圾回收,不會給正常程序的執(zhí)行帶來額外中斷。

缺點:

頻繁更新引用計數(shù)降低了性能?

原始的引用計數(shù)不能處理循環(huán)引用問題

標(biāo)記清除(mark and sweep)

從根變量來迭代遍歷所有被引用對象,標(biāo)記之后進(jìn)行清除操作,對未標(biāo)記對象進(jìn)行回收,這種方法解決了引用計數(shù)的不足,但是也有比較明顯的問題:每次垃圾回收的時候都會暫停所有的正常運行的代碼,系統(tǒng)的響應(yīng)能力會大大降低。當(dāng)然后續(xù)也出現(xiàn)了很多mark&sweep算法的變種(如三色標(biāo)記法)優(yōu)化了這個問題。

標(biāo)記清掃法在標(biāo)記和清理時需要停止所有的goroutine,來保證已經(jīng)被標(biāo)記的區(qū)域不會被用戶修改引用關(guān)系,造成清理錯誤

分代搜集(Generational Garbage Collection)

在面向?qū)ο缶幊陶Z言中,絕大多數(shù)對象的生命周期都非常短。分代收集的基本思想是,將堆劃分為兩個或多個稱為代(generation)的空間。新創(chuàng)建的對象存放在稱為新生代(young generation)中(一般來說,新生代的大小會比 老年代小很多),隨著垃圾回收的重復(fù)執(zhí)行,生命周期較長的對象會被提升(promotion)到老年代中(這里用到了一個分類的思路,這個是也是科學(xué)思考的一個基本思路)。新生代垃圾回收的速度非常快,比老年代快幾個數(shù)量級,即使新生代垃圾回收的頻率更高,執(zhí)行效率也仍然比老年代垃圾回收強,這是因為大多數(shù)對象的生命周期都很短,根本無需提升到老年代。

這樣的做法,相對于全區(qū)域掃描,分代提升了掃描的效率。另外,由于減少了需要掃描的區(qū)域大小,卡頓時間也會相對縮短。

缺點:實現(xiàn)復(fù)雜

三色標(biāo)記-清掃

???????? 白色:待回收對象,對象在這次GC中未標(biāo)記

???????? 灰色:處理中對象,對象在這次GC中已標(biāo)記, 但這個對象包含的子對象未標(biāo)記

???????? 黑色:活躍的對象,對象在這次GC中已標(biāo)記, 且這個對象包含的子對象也已標(biāo)記

在go內(nèi)部對象并沒有保存顏色的屬性, 三色只是對它們的狀態(tài)的描述,

白色的對象在它所在的span的gcmarkBits中對應(yīng)的bit為0,

灰色的對象在它所在的span的gcmarkBits中對應(yīng)的bit為1, 并且對象在標(biāo)記隊列中,

黑色的對象在它所在的span的gcmarkBits中對應(yīng)的bit為1, 并且對象已經(jīng)從標(biāo)記隊列中取出并處理.

gc完成后, gcmarkBits會移動到allocBits然后重新分配一個全部為0的bitmap, 這樣黑色的對象就變?yōu)榱税咨?


? ? 判斷一個對象是不是垃圾需不需要標(biāo)記,就看是否能從當(dāng)前棧或全局?jǐn)?shù)據(jù)區(qū)直接或間接的引用到這個對象。這個初始的當(dāng)前goroutine的棧和全局?jǐn)?shù)據(jù)區(qū)成為GC的root區(qū);通過markroot將所有的root區(qū)域的指針標(biāo)記為可達(dá),然后沿著這些指針掃描,標(biāo)記遇到的所有可達(dá)對象。

???????? 1)、起初所有對象都是白 色(雖然是白色,但是未標(biāo)記,不能直接回收);

???????? 2)、掃描找出所有可達(dá)對象,即全局對象或者棧對象(root集合)或者說全局指針和goroutine棧上的指針,標(biāo)記為灰色,放入待處理隊列(gcWork高性能緩存隊列);

???????? 3)、從隊列提取灰色對象,將其引用對象標(biāo)記為灰色放入隊列,自身標(biāo)記為黑色;

???????? 4)、寫屏障監(jiān)控對象內(nèi)存的修改 ,對白色對象的引用修改被寫屏障捕獲后,重新標(biāo)色或放回隊列。(re-scan全局指針和棧,因為mark和用戶程序是并行的,所以在過程1的時候可能會有新的對象分配,這個時候就需要通過寫屏障Write Barrier記錄下來。re-scan再完成檢查一下)

???????? 5)、當(dāng)完成全部掃描和標(biāo)記工作后,剩余的不是白色就是黑色,分別代表待回收和活躍對象,清理操作只需將白色對象內(nèi)存收回即可;

用戶程序和mark并發(fā)進(jìn)行,Stop The World有兩個過程:

a.第一次是Mark階段的開始,?這個時候主要是一些準(zhǔn)備工作,比如enable write barrier;第一次STW會準(zhǔn)備根對象的掃描, 啟動寫屏障(Write Barrier)和輔助GC(mutator assist).?

b.第二次是Mark Termination階段.?re-scan過程,如果這個時候沒有stw,那么mark將無休止,第二次STW會重新掃描部分根對象, 禁用寫屏障(Write Barrier)和輔助GC(mutator assist). 需要注意的是, 不是所有根對象的掃描都需要STW, 例如掃描棧上的對象只需要停止擁有該棧的G. 從go 1.9開始, 寫屏障的實現(xiàn)使用了Hybrid Write Barrier, 大幅減少了第二次STW的時間.

這里的寫屏障(write barrier)是因為在GC的時候用戶代碼可以同時運行,這樣在掃描的時候,對象的依賴樹可能被改變了,為了避免這個問題,Golang在GC中標(biāo)記階段會啟用寫屏障。

Go的垃圾回收:是一個非分代,非壓縮的,?寫屏障 ,并發(fā)的,三色標(biāo)記清掃垃圾回收;非分代是指沒有使用分代垃圾回收算法,非壓縮的是指沒有做內(nèi)存的整理和緊縮,這里的"并發(fā)"是指在垃圾回收的時候,用戶代碼可以同時運行。三色標(biāo)記清掃是一個經(jīng)典的垃圾回收算法

并發(fā)清理: 垃圾回收(清理過程)與用戶邏輯并發(fā)執(zhí)行?

三色并發(fā)標(biāo)記 : 標(biāo)記與用戶邏輯并發(fā)執(zhí)行

為什么markTermination需要rescan全局指針和棧。因為mark階段是跟用戶代碼并發(fā)的,所以有可能棧上都分了新的對象,這些對象通過write barrier記錄下來,在rescan的時候再檢查一遍。

golang中g(shù)c的總時間

Tgc = Tseq + Tmark + Tsweep(T表示time)

Tseq表示是停止用戶的 goroutine 和做一些準(zhǔn)備活動(通常很小)需要的時間

Tmark 是堆標(biāo)記時間,標(biāo)記發(fā)生在所有用戶 goroutine 停止時,因此可以顯著地影響處理的延遲

Tsweep 是堆清除時間,清除通常與正常的程序運行同時發(fā)生,所以對延遲來說是不太關(guān)鍵的

Go觸發(fā)GC機制(進(jìn)程內(nèi)存高居不下的問題)

1. gcTriggerHeap 在申請內(nèi)存的時候,檢查當(dāng)前已分配的內(nèi)存是否大于上次GC后的內(nèi)存的兩倍,若是則觸發(fā);默認(rèn)情況下是GOGC=100,即新增一倍就會觸發(fā),通過設(shè)大環(huán)境變量GOGC可以減少GC的觸發(fā),設(shè)置"GOGC=off"可以徹底關(guān)掉GC。

2. gcTriggerTime 監(jiān)控線程發(fā)現(xiàn)上次GC的時間已經(jīng)超過兩分鐘,觸發(fā);將一個G任務(wù)放到全局G隊列中去。這個值在Golang里面為兩分鐘var forcegcperiod int64 = 2 * 60 * 1e9。

3、gcTriggerCycle 主動調(diào)用GC來回收,有兩處可以實現(xiàn):runtime.GC()

出現(xiàn)內(nèi)存居高不下的問題

1.gcmark在每次標(biāo)記結(jié)束后重置閾值大小。當(dāng)前使用了4MB內(nèi)存,這時設(shè)置gc_trigger為2*4MB,也就是當(dāng)內(nèi)存分配到8MB時會再次觸發(fā)GC?;厥罩髢?nèi)存為5MB,那下一次要達(dá)到10MB才會觸發(fā)GC。這個比例triggerRatio是由gcpercent/100決定的。

如果系統(tǒng)啟動或短時間內(nèi)大量分配對象,會將垃圾回收的gc_trigger推高。當(dāng)服務(wù)正常后,活躍對象遠(yuǎn)小于這個閾值,造成垃圾回收無法觸發(fā)。它每隔2分鐘force觸發(fā)GC一次。

2.go語言在向系統(tǒng)交還內(nèi)存時只是告訴系統(tǒng)這些內(nèi)存不需要使用了,可以回收;同時操作系統(tǒng)會采取“拖延癥”策略,并不是立即回收,而是等到系統(tǒng)內(nèi)存緊張時才會開始回收這樣該程序又重新申請內(nèi)存時就可以獲得極快的分配速度。

gc時間長的問題

golang gc時過程會stop the world,我們對于應(yīng)該盡量避免頻繁創(chuàng)建臨時堆對象(如&abc{}, new, make等)以減少垃圾收集時的掃描時間,對于需要頻繁使用的臨時對象考慮直接通過數(shù)組緩存進(jìn)行重用

goroutine泄露的問題

在不使用協(xié)程后一定要把他依賴的channel close并通過 在協(xié)程中判斷channel是否關(guān)閉以保證其退出。

go語言提供了強大的測試工具,下面舉例簡單介紹一下

go test 單元測試

go test -bench=. 性能測試

* go tool pprof 性能監(jiān)控

a.生成web服務(wù)器性能監(jiān)控圖,如go程序是用http包啟動的web服務(wù)器,可以選擇引入包_”net/http/pprof” ,go run main.go 后就可以在瀏覽器中使用http://localhost:8080/debug/pprof/直接看到當(dāng)前web服務(wù)的狀態(tài),包括CPU占用情況和內(nèi)存使用情況等,

b.生成一般應(yīng)用程序性能監(jiān)控圖

如果只是一個應(yīng)用程序,你就不能使用net/http/pprof包了,你就需要使用到runtime/pprof。具體做法就是用到pprof.StartCPUProfile和pprof.StopCPUProfile

c.如果重新封裝了ServHTTP函數(shù),無法開啟go默認(rèn)web的pprof,則重新改造支持pprof,代碼如下所示

switch choice {

????default :pprof.Index(w, r)

????case "" : pprof.Index(w,r)

????case "cmdline": pprof.Cmdline(w, r)

????case "profile":pprof.Profile(w, r)

????case "symbol":? pprof.Symbol(w, r)

????case "trace": pprof.Trace(w,r)

}

d.uber開源的火焰圖go-torch,可以直觀顯示哪個方法調(diào)用耗時長了,然后不斷的修正代碼,重新采樣,不斷優(yōu)化。

什么時候從Heap分配對象

當(dāng)一個對象的內(nèi)容可能在生成該對象的函數(shù)結(jié)束后被訪問, 那么這個對象就會分配在堆上.

在堆上分配對象的情況包括:

* 返回對象的指針

* 傳遞了對象的指針到其他函數(shù)

* 在閉包中使用了對象并且需要修改對象

* 使用new,make

在C語言中函數(shù)返回在棧上的對象的指針是非常危險的事情, 但在go中卻是安全的, 因為這個對象會自動在堆上分配.

go決定是否使用堆分配對象的過程也叫"逃逸分析".

內(nèi)存優(yōu)化

1. 小對象合并成結(jié)構(gòu)體一次分配,減少內(nèi)存分配次數(shù),小對象在堆上頻繁地申請釋放,會造成內(nèi)存碎片(有的叫空洞),導(dǎo)致分配大的對象時無法申請到連續(xù)的內(nèi)存空間。

2. 緩存區(qū)內(nèi)容一次分配足夠大小空間,并適當(dāng)復(fù)用

????在協(xié)議編解碼時,需要頻繁地操作[]byte,可以使用bytes.Buffer或其它byte緩存區(qū)對象。

????建議:bytes.Buffert等通過預(yù)先分配足夠大的內(nèi)存,避免當(dāng)Grow時動態(tài)申請內(nèi)存,這樣可以減少內(nèi)存分配次數(shù)。同時對于byte緩存區(qū)對象考慮適當(dāng)?shù)貜?fù)用。

3. slice和map采make創(chuàng)建時,預(yù)估大小指定容量

????slice和map與數(shù)組不一樣,不存在固定空間大小,可以根據(jù)增加元素來動態(tài)擴容。

????slice初始會指定一個數(shù)組,當(dāng)對slice進(jìn)行append等操作時,當(dāng)容量不夠時,會自動擴容:重新分配一塊"夠大"的內(nèi)存,并把內(nèi)容從原來的內(nèi)存塊復(fù)制到新分配的內(nèi)存塊,這樣會產(chǎn)生明顯的CPU開銷

????map的擴容比較復(fù)雜,每次擴容會增加到上次容量的2倍。它的結(jié)構(gòu)體中有一個buckets和oldbuckets,用于實現(xiàn)增量擴容:

????正常情況下,直接使用buckets,oldbuckets為空;

????如果正在擴容,則oldbuckets不為空,buckets是oldbuckets的2倍,

建議:初始化時預(yù)估大小指定容量

4. 長調(diào)用棧避免申請較多的臨時對象

goroutine的調(diào)用棧默認(rèn)大小是4K(1.7修改為2K),它采用連續(xù)棧機制,當(dāng)??臻g不夠時,Go runtime會不斷擴容:

當(dāng)棧空間不夠時,按2倍增加,原有棧的變量崆直接copy到新的??臻g,變量指針指向新的空間地址;

退棧會釋放??臻g的占用,GC時發(fā)現(xiàn)棧空間占用不到1/4時,則??臻g減少一半。

比如棧的最終大小2M,則極端情況下,就會有10次的擴棧操作,這會帶來性能下降。

建議:

控制調(diào)用棧和函數(shù)的復(fù)雜度,不要在一個goroutine做完所有邏輯;

如查的確需要長調(diào)用棧,而考慮goroutine池化,避免頻繁創(chuàng)建goroutine帶來??臻g的變化。

5. 避免頻繁創(chuàng)建臨時對象

Go在GC時會引發(fā)stop the world,即整個情況暫停。暫停時間還是取決于臨時對象的個數(shù),臨時對象數(shù)量越多,暫停時間可能越長,并消耗CPU。

建議:GC優(yōu)化方式是盡可能地減少臨時對象的個數(shù):

盡量使用局部變量

所多個局部變量合并一個大的結(jié)構(gòu)體或數(shù)組,減少掃描對象的次數(shù),一次回盡可能多的內(nèi)存。

并發(fā)優(yōu)化

1 高并發(fā)的任務(wù)處理使用goroutine池

goroutine雖輕量,但對于高并發(fā)的輕量任務(wù)處理,頻繁來創(chuàng)建goroutine來執(zhí)行,執(zhí)行效率并不會太高效:過多的goroutine創(chuàng)建,會影響go runtime對goroutine調(diào)度,以及GC消耗;高并時若出現(xiàn)調(diào)用異常阻塞積壓,大量的goroutine短時間積壓可能導(dǎo)致程序崩潰。

2 高并發(fā)時避免共享對象互斥

傳統(tǒng)多線程編程時,當(dāng)并發(fā)沖突在4~8線程時,性能可能會出現(xiàn)拐點。Go中的推薦是不要通過共享內(nèi)存來通訊,Go創(chuàng)建goroutine非常容易,當(dāng)大量goroutine共享同一互斥對象時,也會在某一數(shù)量的goroutine出在拐點。建議:goroutine盡量獨立,無沖突地執(zhí)行;若goroutine間存在沖突,則可以采分區(qū)來控制goroutine的并發(fā)個數(shù),減少同一互斥對象沖突并發(fā)數(shù)。

其它優(yōu)化

1 避免使用CGO或者減少CGO調(diào)用次數(shù)

2 減少[]byte與string之間轉(zhuǎn)換,盡量采用[]byte來字符串處理

GO里面的string類型是一個不可變類型,而GO中[]byte與string底層兩個不同的結(jié)構(gòu),他們之間的轉(zhuǎn)換存在實實在在的值對象拷貝,所以盡量減少這種不必要的轉(zhuǎn)化建議:存在字符串拼接等處理,盡量采用[]byte

3. 字符串的拼接優(yōu)先考慮bytes.Buffer

由于string類型是一個不可變類型,但拼接會創(chuàng)建新的string。GO中字符串拼接常見有如下幾種方式:

string + 操作 :導(dǎo)致多次對象的分配與值拷貝

fmt.Sprintf :會動態(tài)解析參數(shù)

strings.Join :內(nèi)部是[]byte的append

bytes.Buffer :可以預(yù)先分配大小,減少對象分配與拷貝

建議:對于高性能要求,優(yōu)先考慮bytes.Buffer,預(yù)先分配大小。非關(guān)鍵路徑,視簡潔使用。fmt.Sprintf可以簡化不同類型轉(zhuǎn)換與拼接。

代碼覆蓋率可通過例如:

go test -cover -covermode count -coverprofile cover.out 命令來實現(xiàn),并且可以在瀏覽器上查看結(jié)果;

通過編寫測試代碼,使用命令:go test -bench . 進(jìn)行基準(zhǔn)測試,可以有針對性地測試出模塊某部分的性能瓶頸;

CPU的主頻

CPU內(nèi)核工作的時鐘頻率(CPU Clock Speed)。CPU的主頻的基本單位是赫茲(Hz),但更多的是以兆赫茲(MHz)或吉赫茲(GHz)為單位。時鐘頻率的倒數(shù)即為時鐘周期。時鐘周期的基本單位為秒(s),但更多的是以毫秒(ms)、微妙(us)或納秒(ns)為單位。在一個時鐘周期內(nèi),CPU執(zhí)行一條運算指令。也就是說,在1000 Hz的CPU主頻下,每1毫秒可以執(zhí)行一條CPU運算指令。在1 MHz的CPU主頻下,每1微妙可以執(zhí)行一條CPU運算指令。而在1 GHz的CPU主頻下,每1納秒可以執(zhí)行一條CPU運算指令。

在默認(rèn)情況下,Go語言的運行時系統(tǒng)會以100 Hz的的頻率對CPU使用情況進(jìn)行取樣。也就是說每秒取樣100次,即每10毫秒會取樣一次。為什么使用這個頻率呢?因為100 Hz既足夠產(chǎn)生有用的數(shù)據(jù),又不至于讓系統(tǒng)產(chǎn)生停頓。并且100這個數(shù)上也很容易做換算,比如把總?cè)佑嫈?shù)換算為每秒的取樣數(shù)。實際上,這里所說的對CPU使用情況的取樣就是對當(dāng)前的Goroutine的堆棧上的程序計數(shù)器的取樣。由此,我們就可以從樣本記錄中分析出哪些代碼是計算時間最長或者說最耗CPU資源的部分了。我們可以通過以下代碼啟動對CPU使用情況的記錄。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容