Go?語言垃圾回收機(jī)制從入門到理解
前言:為什么我們要聊 GC?
我們寫程序就像是在一個(gè)大房間里工作,每當(dāng)我們創(chuàng)建一個(gè)變量、一個(gè)對(duì)象,就等于往房間里放了一件家具。用完的家具,如果我們不及時(shí)處理,房間就會(huì)越來越亂,最終擠得連走路的地方都沒有。在編程世界里,這個(gè)“房間”就是內(nèi)存,而“垃圾”就是那些程序不再使用的內(nèi)存空間。
在 Go 語言中,我們不用自己去手動(dòng)清理這些“垃圾”,因?yàn)橛幸粋€(gè)勤勞的“清潔工”—— 垃圾回收器(Garbage Collector, GC) 會(huì)自動(dòng)完成這項(xiàng)工作。雖然它很勤快,但如果我們的程序?qū)懙貌粔蚝?,讓它忙不過來,也可能會(huì)影響程序的性能。所以,理解 GC 的工作原理,能幫助我們寫出更高效、更“干凈”的代碼。
1. 什么是垃圾回收?
在深入 Go 的 GC 之前,我們先來聊聊 GC 的基本概念。
什么是“垃圾”?
簡(jiǎn)單來說,“垃圾”就是 程序不再使用的內(nèi)存。
舉個(gè)例子:
package main import "fmt" func main() { var a int = 10 { var b int = 20 fmt.Println(b) // 在這里,變量 b 仍然有效 } // b 的作用域結(jié)束,b 占用的內(nèi)存成為“垃圾” // 另一個(gè)例子:當(dāng)一個(gè)變量不再被引用時(shí) s := "hello world" fmt.Println(s) s = "go language" // 字符串 "hello world" 不再被任何變量引用,成為“垃圾” fmt.Println(s) } // a 的作用域結(jié)束,a 占用的內(nèi)存成為“垃圾”
當(dāng)一個(gè)變量的作用域結(jié)束,或者沒有其他任何變量再指向它時(shí),它所占用的內(nèi)存就是可以被回收的“垃圾”了。
為什么需要 GC?
- 減輕開發(fā)者的負(fù)擔(dān):在沒有 GC 的語言(比如 C++)中,開發(fā)者需要手動(dòng)分配和釋放內(nèi)存。這很容易出錯(cuò),比如忘記釋放內(nèi)存導(dǎo)致 內(nèi)存泄漏,或者重復(fù)釋放內(nèi)存導(dǎo)致程序崩潰。
- 提高開發(fā)效率:有了 GC,開發(fā)者可以更專注于業(yè)務(wù)邏輯的實(shí)現(xiàn),而不用花費(fèi)大量精力去管理內(nèi)存,大大提升了開發(fā)效率。
2. Go GC 的核心原理:三色標(biāo)記法
Go GC 的核心算法叫做 “三色標(biāo)記法”。聽起來像是在畫畫,沒錯(cuò),它的原理就是給程序中的所有對(duì)象“涂上”三種顏色。
三種顏色代表什么?
我們可以把程序中所有的內(nèi)存對(duì)象想象成一個(gè)個(gè)小方塊,GC 的任務(wù)就是給這些小方塊“涂色”,然后把白色的方塊清理掉。
- 白色 (White): 初始狀態(tài),所有對(duì)象都是白色的。它們是 GC 眼中的 “潛在垃圾”。
- 灰色 (Gray): 對(duì)象被 標(biāo)記 了,但它里面包含的引用(比如一個(gè)結(jié)構(gòu)體里的指針)還沒有被檢查。我們可以把灰色對(duì)象看作是“待處理”的對(duì)象。
- 黑色 (Black): 對(duì)象被標(biāo)記了,并且它所引用的所有子對(duì)象也都被檢查過了。黑色對(duì)象就是 “確認(rèn)存活” 的對(duì)象。
三色標(biāo)記的流程
- 初始狀態(tài):所有對(duì)象都是白色的。
- 標(biāo)記階段 (Mark):GC 會(huì)從“根對(duì)象”開始遍歷,比如全局變量、當(dāng)前函數(shù)棧上的變量等。GC 會(huì)把這些根對(duì)象以及它們直接引用的對(duì)象標(biāo)記為灰色,并放入一個(gè)隊(duì)列。
- 循環(huán)檢查:GC 依次從灰色隊(duì)列中取出一個(gè)對(duì)象,把它標(biāo)記為黑色,然后檢查它所引用的所有對(duì)象。如果引用的對(duì)象是白色的,就把它標(biāo)記為灰色并加入隊(duì)列。
- 最終清理 (Sweep):當(dāng)灰色隊(duì)列變空,GC 就知道所有存活的對(duì)象都被標(biāo)記成黑色了。這時(shí),GC 就會(huì)遍歷整個(gè)內(nèi)存,把所有還停留在 白色 的對(duì)象全部回收掉。
3. Go GC 的演進(jìn):從“暫停世界”到“并發(fā)執(zhí)行”
早期的 GC 算法有一個(gè)很大的缺點(diǎn),叫做 STW (Stop-The-World)。
什么是 STW?
- 在 STW 模式下,GC 運(yùn)行時(shí),程序會(huì)完全暫停,不能做任何事情。
- 這就好比清潔工來打掃房間時(shí),你必須停下所有工作,坐在椅子上不動(dòng),等他打掃完了你才能繼續(xù)。
- 缺點(diǎn):如果你的程序內(nèi)存很大,GC 暫停的時(shí)間就會(huì)很長,這會(huì)嚴(yán)重影響程序的性能,尤其是在高并發(fā)的服務(wù)器應(yīng)用中,用戶可能會(huì)感受到明顯的卡頓。
Go 語言的 GC 團(tuán)隊(duì)也意識(shí)到了這個(gè)問題,并進(jìn)行了一系列優(yōu)化。
Go 1.5 之后:Go 語言引入了 并發(fā) GC。
- 什么是并發(fā) GC? 顧名思義,就是 GC 的大部分工作可以 和程序同時(shí)進(jìn)行。
- 這大大減少了 STW 的暫停時(shí)間,讓 GC 幾乎不會(huì)影響到程序的正常運(yùn)行。
Go 1.8 之后:Go GC 進(jìn)一步優(yōu)化,現(xiàn)在的 STW 暫停時(shí)間已經(jīng)非常短,通常在微秒級(jí)別,幾乎可以忽略不計(jì)。
4. 如何觀察和優(yōu)化 Go GC?
既然 GC 是自動(dòng)的,那我們還需要關(guān)心它嗎?當(dāng)然!理解 GC 的工作,可以幫助我們更好地診斷和解決性能問題。
如何查看 GC 信息?
- 你可以在運(yùn)行 Go 程序時(shí),通過設(shè)置環(huán)境變量來查看詳細(xì)的 GC 日志。
- 讓我們寫一個(gè)簡(jiǎn)單的程序,它會(huì)持續(xù)分配內(nèi)存:
package main import ( "fmt" "runtime" "time" ) func main() { // 打印 GC 狀態(tài) go func() { for { var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("當(dāng)前已分配內(nèi)存: %v MB, 下次GC內(nèi)存閾值: %v MB\n", m.Alloc/1024/1024, m.NextGC/1024/1024) time.Sleep(2 * time.Second) } }() // 持續(xù)分配內(nèi)存 var a []byte for i := 0; i < 10; i++ { // 每次分配 100MB 內(nèi)存 a = append(a, make([]byte, 100*1024*1024)...) fmt.Printf("第 %d 次分配內(nèi)存完成\n", i+1) time.Sleep(1 * time.Second) } }
運(yùn)行這個(gè)程序,同時(shí)設(shè)置 GODEBUG=gctrace=1
環(huán)境變量,例如: GODEBUG=gctrace=1 go run your_program.go
運(yùn)行后,除了我們自己打印的內(nèi)存信息,你還會(huì)看到類似下面的 GC 日志:
gc 1 @0.038s 0%: 0.054+1.4+0.007 ms clock, 0.43+0.32/1.4/0.38+0.05 ms cpu, 4->4 MB, 10->10 MB, 4 (2) objects, 1 (0) goroutines, 0/0/0/0 ms inter-sweep, 0/0(G)/0(H) MSpans, ...
- 日志中會(huì)包含 GC 的 ID、觸發(fā)時(shí)間、STW 暫停時(shí)間、回收的內(nèi)存量等關(guān)鍵信息。
如何減少 GC 壓力?
- 減少內(nèi)存分配:每次
new
或make
都會(huì)增加 GC 的工作量。盡量復(fù)用對(duì)象,減少不必要的內(nèi)存分配。 - 使用
sync.Pool
:對(duì)于那些創(chuàng)建和銷毀都非常頻繁的小對(duì)象,可以使用sync.Pool
來緩存和復(fù)用它們,大大減輕 GC 的壓力。
sync.Pool
示例:
package main import ( "fmt" "sync" "time" ) // 定義一個(gè)需要被頻繁創(chuàng)建和銷毀的對(duì)象 type Data struct { ID int Name string } func main() { // 創(chuàng)建一個(gè) sync.Pool,并定義 New 函數(shù),用于創(chuàng)建新的對(duì)象 dataPool := &sync.Pool{ New: func() interface{} { fmt.Println("創(chuàng)建了一個(gè)新的 Data 對(duì)象") return &Data{} }, } // 不使用 sync.Pool,每次都創(chuàng)建新對(duì)象 fmt.Println("--- 不使用 sync.Pool ---") for i := 0; i < 3; i++ { _ = &Data{ID: i} time.Sleep(100 * time.Millisecond) } // 使用 sync.Pool,從池中獲取對(duì)象 fmt.Println("\n--- 使用 sync.Pool ---") for i := 0; i < 3; i++ { // Get() 方法會(huì)嘗試從池中獲取一個(gè)對(duì)象,如果池為空,則會(huì)調(diào)用 New() obj := dataPool.Get().(*Data) obj.ID = i obj.Name = fmt.Sprintf("數(shù)據(jù) %d", i) fmt.Printf("使用對(duì)象: %+v\n", obj) // Put() 方法會(huì)將對(duì)象放回池中,供下次復(fù)用 dataPool.Put(obj) time.Sleep(100 * time.Millisecond) } // 再次獲取,這次會(huì)直接從池中復(fù)用,而不會(huì)調(diào)用 New() fmt.Println("\n--- 再次使用 sync.Pool ---") obj := dataPool.Get().(*Data) fmt.Printf("復(fù)用對(duì)象: %+v\n", obj) }
合理設(shè)置 GOGC
:GOGC
是一個(gè)環(huán)境變量,可以用來控制 GC 的觸發(fā)時(shí)機(jī)。默認(rèn)值為 100,表示當(dāng)新分配的內(nèi)存達(dá)到上次 GC 之后存活內(nèi)存的 100% 時(shí),就會(huì)觸發(fā)新一輪 GC。你可以根據(jù)程序的特點(diǎn),適當(dāng)調(diào)整這個(gè)值。
總結(jié)
Go GC 的設(shè)計(jì)理念是 并發(fā)、低延遲、自動(dòng)管理。作為 Go 開發(fā)者,雖然我們不用手動(dòng)管理內(nèi)存,但理解 GC 的工作原理依然非常重要。它能幫助我們寫出更高效的程序,更輕松地應(yīng)對(duì)各種性能挑戰(zhàn)。
到此這篇關(guān)于Go 語言垃圾回收機(jī)制從入門到理解的文章就介紹到這了,更多相關(guān)Go垃圾回收機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
簡(jiǎn)單了解Go語言中函數(shù)作為值以及函數(shù)閉包的使用
這篇文章主要介紹了簡(jiǎn)單了解Go語言中函數(shù)作為值以及函數(shù)閉包的使用,是golang入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-10-10Go-客戶信息關(guān)系系統(tǒng)的實(shí)現(xiàn)
這篇文章主要介紹了Go-客戶信息關(guān)系系統(tǒng)的實(shí)現(xiàn),本文章內(nèi)容詳細(xì),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,需要的朋友可以參考下2023-01-01go打包aar及flutter調(diào)用aar流程詳解
這篇文章主要為大家介紹了go打包aar及flutter調(diào)用aar流程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03