GO語(yǔ)言內(nèi)存逃逸和GC機(jī)制的實(shí)現(xiàn)
一、背景
Go語(yǔ)言雖然語(yǔ)法上類(lèi)似C語(yǔ)言,但是也是一種“高級(jí)語(yǔ)言”,有一套內(nèi)存管理系統(tǒng),不需要向C語(yǔ)言去動(dòng)態(tài)malloc/free堆內(nèi)存,而是語(yǔ)言編譯時(shí)根據(jù)具體使用情況來(lái)決定使用棧還是使用堆,堆內(nèi)存也不需要程序員手動(dòng)free內(nèi)存,后臺(tái)有一套gc機(jī)制,根據(jù)內(nèi)存對(duì)象的生命周期(引用關(guān)系)決定是否回收內(nèi)存。Go語(yǔ)言默認(rèn)使用棧內(nèi)存,在一些特定的情況會(huì)內(nèi)存逃逸使用堆內(nèi)存,本文會(huì)重點(diǎn)介紹內(nèi)存逃逸以及GC機(jī)制。
二、內(nèi)存逃逸
2.1 內(nèi)存逃逸情況
Go在如下情況會(huì)使用堆內(nèi)存,然后由GC完成內(nèi)存回收。
| 逃逸類(lèi)型 | 產(chǎn)生原因 | 具體場(chǎng)景/示例 |
|---|---|---|
| 指針逃逸 | 函數(shù)返回了局部變量的指針,其生命周期需在函數(shù)外部延續(xù) | func f() *int { x := 10; return &x } |
| 接口類(lèi)型逃逸 | 將值賦值給 interface{}類(lèi)型,編譯期無(wú)法確定其動(dòng)態(tài)類(lèi)型 | fmt.Println(123)或 var i interface{} = "hello" |
| 閉包引用逃逸 | 閉包函數(shù)引用了外部變量,該變量的生命周期需與閉包一致 | func() func() int { n:=0; return func() int { n++; return n } }() |
| ??臻g不足逃逸 | 變量過(guò)大,超過(guò)當(dāng)前棧幀的承載能力 | s := make([]int, 0, 100000) |
| 動(dòng)態(tài)分配逃逸 | 切片或數(shù)組的長(zhǎng)度在編譯期無(wú)法確定 | s := make([]int, n)(n為變量) |
| 發(fā)送指針到 channel | 指針被發(fā)送到 channel,其生命周期可能跨越 goroutine | ch <- &myStruct{} |
| 在集合中存儲(chǔ)指針 | 在 map 或 slice 等集合中存儲(chǔ)指針,且集合本身發(fā)生逃逸 | m["key"] = &value |
2.2 內(nèi)存逃逸分析
Go 編譯器在編譯階段會(huì)進(jìn)行逃逸分析,并提供了編譯選項(xiàng)可以查看分析結(jié)果。
查看逃逸信息:在構(gòu)建或運(yùn)行 Go 代碼時(shí),使用
-gcflags='-m'選項(xiàng)即可。為了獲得更詳細(xì)的信息,通常還會(huì)加上-l選項(xiàng)來(lái)禁止內(nèi)聯(lián)優(yōu)化:go build -gcflags='-m -l' main.go
命令執(zhí)行后,編譯器會(huì)輸出代碼的逃逸分析信息,如果看到某行代碼提示
escapes to heap或moved to heap,就表明該處的變量發(fā)生了內(nèi)存逃逸。
2.3 內(nèi)存逃逸優(yōu)化思路
內(nèi)存逃逸最直接的影響是性能。堆分配比棧分配慢,因?yàn)樯婕案鼜?fù)雜的內(nèi)存管理。同時(shí),堆上的對(duì)象需要垃圾回收器(GC)來(lái)管理,過(guò)多的逃逸會(huì)增加 GC 的壓力,可能導(dǎo)致程序出現(xiàn)延遲。雖然無(wú)法也必要完全避免內(nèi)存逃逸,但在編寫(xiě)高性能代碼時(shí),可以有所優(yōu)化:
- 值傳遞替代指針傳遞:對(duì)于小結(jié)構(gòu)體,有時(shí)直接返回值比返回指針更高效,可以避免逃逸。
- 預(yù)分配切片/映射:如果知道數(shù)據(jù)的大致規(guī)模,使用 make([]T, len, cap)預(yù)分配足夠的容量,比讓切片動(dòng)態(tài)擴(kuò)容更好。
- 謹(jǐn)慎使用 interface{}:在性能敏感的路徑上,避免使用空接口,使用具體類(lèi)型可以避免不必要的逃逸。
三、內(nèi)存分配器
Go的堆內(nèi)存分配器借鑒了TCMalloc的思想,采用多級(jí)緩存模式,這種設(shè)計(jì)通過(guò)本地緩存(mcache) 實(shí)現(xiàn)了絕大多數(shù)情況下無(wú)鎖的快速分配,并通過(guò)尺寸規(guī)格(size class) 精細(xì)化管理,有效減少了內(nèi)存碎片。
Go 將對(duì)象按大小分為三類(lèi):
- 微小對(duì)象(<16 字節(jié)):使用微分配器(mcache.tiny)
- 小對(duì)象(16 字節(jié)-32KB):使用固定大小的 span (mcache)
- 大對(duì)象(>32KB):直接從堆分配
四、GC機(jī)制
GC機(jī)制可能會(huì)導(dǎo)致業(yè)務(wù)暫停,即Stop-The-World,那么Go語(yǔ)言是如何如何在保證內(nèi)存安全的同時(shí),最大限度地減少垃圾回收對(duì)程序性能的影響?Go使用并發(fā)三色標(biāo)記清除算法,構(gòu)建一個(gè)低延遲、并發(fā)執(zhí)行、三色標(biāo)記的垃圾回收器,將 STW 時(shí)間從早期版本的幾百毫秒降低到亞毫秒級(jí)別。
4.1 垃圾回收(GC)時(shí)機(jī)
- 內(nèi)存分配觸發(fā):當(dāng)堆內(nèi)存達(dá)到上次 GC 后的 2 倍時(shí)
- 定時(shí)觸發(fā):默認(rèn) 2 分鐘強(qiáng)制觸發(fā)一次
- 手動(dòng)觸發(fā):調(diào)用
runtime.GC()
4.2 標(biāo)記階段
標(biāo)記階段會(huì)停止所有用戶 goroutine,啟動(dòng)標(biāo)記 worker進(jìn)行掃描。首先會(huì)將所有對(duì)象視為白色,然后從根對(duì)象(如全局變量、Goroutine棧上的變量等)開(kāi)始遍歷,其標(biāo)記過(guò)程可以概括為:
- 從根對(duì)象開(kāi)始:從根對(duì)象開(kāi)始,將它們直接引用的對(duì)象標(biāo)記為灰色,放入待處理隊(duì)列。
- 處理灰色對(duì)象:從隊(duì)列中取出一個(gè)灰色對(duì)象,將其標(biāo)記為黑色,然后檢查這個(gè)黑色對(duì)象引用的其他對(duì)象。將被黑色對(duì)象引用且仍是白色的對(duì)象標(biāo)記為灰色,加入隊(duì)列。
- 循環(huán)處理:重復(fù)步驟2,直到灰色對(duì)象隊(duì)列為空。此時(shí),剩下的白色對(duì)象就是沒(méi)有任何根對(duì)象引用的垃圾,可以在清除階段被回收。
為了保證在標(biāo)記過(guò)程中,因用戶程序并發(fā)執(zhí)行導(dǎo)致對(duì)象引用關(guān)系變化時(shí)不會(huì)錯(cuò)誤地回收仍被引用的對(duì)象,Go使用了寫(xiě)屏障技術(shù)。詳細(xì)步驟如下:
階段1:標(biāo)記準(zhǔn)備(STW)
- 停止所有用戶 goroutine
- 啟動(dòng)寫(xiě)屏障
- 掃描棧和全局變量,將根對(duì)象標(biāo)記為灰色
- 啟動(dòng)標(biāo)記 worker
階段 2:并發(fā)標(biāo)記
這是最耗時(shí)的階段,但與用戶程序并發(fā)執(zhí)行:
- 標(biāo)記 worker 并發(fā)處理灰色對(duì)象
- 用戶程序繼續(xù)執(zhí)行,寫(xiě)屏障保證正確性
- 當(dāng)沒(méi)有更多灰色對(duì)象時(shí),進(jìn)入標(biāo)記終止階段
階段 3:標(biāo)記終止(STW)
這是第二個(gè) STW 階段,主要工作:
- 停止所有用戶 goroutine 和標(biāo)記 worker
- 完成最后的標(biāo)記工作
- 關(guān)閉寫(xiě)屏障
- 計(jì)算下次 GC 的觸發(fā)條件
4.3 清除標(biāo)記(并發(fā))
當(dāng)標(biāo)記階段完成后,清除階段會(huì)遍歷堆內(nèi)存,將標(biāo)記為白色的不可達(dá)對(duì)象所占用的內(nèi)存回收,以便后續(xù)分配使用。這個(gè)清除工作也是與用戶程序并發(fā)執(zhí)行的。
4.4 優(yōu)化實(shí)踐
- 減少不必要的堆分配
- 利用逃逸分析:使用 go build -gcflags="-m -l"命令可以查看變量的逃逸情況
- 預(yù)分配切片和Map:在使用make初始化切片或map時(shí),如果能夠預(yù)估元素?cái)?shù)量,就指定一個(gè)足夠的容量(cap)。這可以避免在添加元素時(shí)因擴(kuò)容而導(dǎo)致的多次內(nèi)存分配和數(shù)據(jù)拷貝。
- 重用對(duì)象以降低GC壓力
- 使用 sync.Pool:對(duì)于需要頻繁創(chuàng)建和銷(xiāo)毀的臨時(shí)對(duì)象(如緩沖區(qū)、解析用的臨時(shí)結(jié)構(gòu)體等),可以使用 sync.Pool來(lái)緩存這些對(duì)象。它可以顯著減少垃圾回收器需要處理的對(duì)象數(shù)量,從而降低GC開(kāi)銷(xiāo)。但需要注意,sync.Pool中的對(duì)象可能被隨時(shí)回收,不適合用于保存有狀態(tài)的長(zhǎng)效對(duì)象。
- 監(jiān)控與診斷
- 使用 runtime.ReadMemStats:這個(gè)函數(shù)可以獲取詳細(xì)的內(nèi)存統(tǒng)計(jì)信息,如堆內(nèi)存分配大小、垃圾回收次數(shù)、暫停時(shí)間等,幫助你對(duì)程序的內(nèi)存使用情況有宏觀了解。
- 使用 pprof性能剖析:Go內(nèi)置了強(qiáng)大的性能剖析工具。通過(guò) net/http/pprof包可以輕松地通過(guò)HTTP服務(wù)暴露程序的性能數(shù)據(jù),然后使用 go tool pprof命令進(jìn)行分析,能夠精準(zhǔn)定位到分配內(nèi)存最多的函數(shù)調(diào)用鏈,是診斷內(nèi)存泄漏和優(yōu)化熱點(diǎn)的利器。
4.5 GC 調(diào)優(yōu)參數(shù)
Go 提供了幾個(gè)重要的 GC 調(diào)優(yōu)參數(shù),控制 GC 觸發(fā)的頻率,默認(rèn)值為 100:
# 設(shè)置GOGC為200,減少GC頻率但增加內(nèi)存使用 export GOGC=200 # 設(shè)置GOGC為50,增加GC頻率但減少內(nèi)存使用 export GOGC=50 # 禁用GC(僅用于測(cè)試) export GOGC=off
到此這篇關(guān)于GO語(yǔ)言內(nèi)存逃逸和GC機(jī)制的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)GO語(yǔ)言內(nèi)存逃逸和GC機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang項(xiàng)目搭配nginx部署反向代理負(fù)載均衡講解
這篇文章主要為大家介紹了Golang項(xiàng)目搭配nginx部署正反向代理負(fù)載均衡講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04
Go string 字符串處理與格式化詳解從底層原理到工程實(shí)踐
這篇文章給大家介紹Go string字符串處理與格式化詳解從底層原理到工程實(shí)踐,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2026-05-05
Go語(yǔ)言為什么不支持前綴自增運(yùn)算符原理解析
這篇文章主要為大家介紹了Go語(yǔ)言為什么不支持前綴自增運(yùn)算符原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
Go?基本數(shù)據(jù)類(lèi)型與字符串相互轉(zhuǎn)換方法小結(jié)
這篇文章主要介紹了Go基本數(shù)據(jù)類(lèi)型與字符串相互轉(zhuǎn)換,將string類(lèi)型轉(zhuǎn)換成基本類(lèi)型時(shí),必須確保string類(lèi)型是有效的,文中補(bǔ)充介紹了Go基本數(shù)據(jù)類(lèi)型和其字符串表示之間轉(zhuǎn)換,結(jié)合實(shí)例代碼給大家講解的非常詳細(xì),需要的朋友可以參考下2024-01-01
Go中select多路復(fù)用的實(shí)現(xiàn)示例
Go的select用于多通道通信,實(shí)現(xiàn)多路復(fù)用,支持隨機(jī)選擇、超時(shí)控制及非阻塞操作,建議合理使用以避免協(xié)程泄漏和死循環(huán),感興趣的可以了解一下2025-08-08

