linux內(nèi)存布局
要搞懂gc前我們需要知道gc到底在回收什么。而想到知道gc在回收什么不可避免的就必須要清楚進程的內(nèi)存布局了肤舞。
- kernel space 內(nèi)核空間可以操作任意空間纲刀,而用戶空間如果需要操縱內(nèi)核空間项炼,需要由操作系統(tǒng)來完成,調(diào)用操作稱為系統(tǒng)調(diào)用(system call)示绊。
- stack是棧區(qū)锭部,常稱為堆棧。它的分配由高地址往低地址擴展面褐。棸韬蹋空間用于分配函數(shù)的出入?yún)⒑途植孔兞?/li>
- memory mapping是映射區(qū),比如一些外部的動態(tài)鏈接庫等展哭。
- heap是堆湃窍,和數(shù)據(jù)結(jié)構(gòu)中的堆沒有關(guān)系,它的分配由低地址往高地址分配匪傍。它用于存儲應(yīng)用程序動態(tài)申請的對象您市。
- bss是Block Started by Symbol的簡稱,屬于靜態(tài)內(nèi)存分配役衡。一般用來存放程序中未初始化的全局變量墨坚。
- data用來存儲一些常量數(shù)據(jù),一般用來存儲程序中已經(jīng)初始化的全局變量映挂。
- text用于加載程序自身代碼段泽篮。
go內(nèi)存管理
像go這種自帶runtime的語言基本上拋棄了傳統(tǒng)的內(nèi)存分配方式,改為自動管理柑船。這樣可以自主地實現(xiàn)更好的內(nèi)存使用模式帽撑,比如內(nèi)存池、預(yù)分配等等鞍时。這樣亏拉,不會每次內(nèi)存分配都需要進行系統(tǒng)調(diào)用。go內(nèi)存沒有內(nèi)存碎片也是因為這種分配模式而避免的逆巍。
協(xié)程棧雖然在堆上但是也是不需要gc回收
gc負責回收堆內(nèi)存及塘,而不回收棧(協(xié)程棧)中的內(nèi)存。主要是原因是棧為函數(shù)執(zhí)行準備的锐极,存儲著函數(shù)的局部變量以及調(diào)用棧笙僚,這塊內(nèi)存用完會直接釋放所以不需要gc來管理。(go的協(xié)程棧也是在堆內(nèi)存分配的灵再,不是傳統(tǒng)的棧肋层,go的棧有個stack cache pool亿笤,管理棧對象,回收已銷毀的棧還給stack cache pool栋猖,棧仍舊是調(diào)用完畢即銷毀净薛,包括棧中的臨時對象)。
變量逃逸
逃逸分析基本原則
- 指向堆棧對象的指針不能存儲在堆中蒲拉。(堆上的指針不能指向棧)
- 指向堆棧對象的指針不能超過那個對象的壽命肃拜。
type User struct {
Name string
Age int
}
func main() {
GetUser()
}
func GetUser() *User {
user := User{}
return &user
}
/*由于 *user 這個指針被傳到 main 函數(shù)中使用,而 User 這個對象在 GetUser 方法中 New 出來雌团,如果不逃逸到堆上爆班,則指針的壽命比對象長。因此需要逃逸到堆上辱姨,讓對象的壽命比其指針長柿菩。因此上述例子發(fā)生了逃逸
*/
type User struct {
Name string
Age int
Car *Car
}
type Car struct {
}
func main() {
user := GetUser()
car := Car{}
user.Car = &car
}
func GetUser() *User {
return &User{}
}
/*
`GetUser` 返回的是一個逃逸的對象,即其已經(jīng)存在堆中雨涛。我們給其一個字段賦值 `*Car`, 如果把 Car 對象分配在棧中枢舶,發(fā)生了指向 棧中對象(Car)的指針存儲在堆中對象中(User) ,違背了原則 1替久,所以 Car 對象需要逃逸到堆凉泄。`[fmt.Println](https://link.zhihu.com/?target=https%3A//github.com/golang/go/issues/8618)` 也是這個原因存在逃逸
*/
GC主要流程
根對象
全局變量:程序在編譯期就能確定的那些存在于程序整個生命周期的變量。
執(zhí)行棧:每個 goroutine 都包含自己的執(zhí)行棧蚯根,這些執(zhí)行棧上包含棧上的變量及指向分配的堆內(nèi)存區(qū)塊的指針后众。
寄存器:寄存器的值可能表示一個指針,參與計算的這些指針可能指向某些賦值器分配的堆內(nèi)存區(qū)塊颅拦。
三色標記法
三色抽象只是一種描述追蹤式回收器的方法蒂誉,在實踐中并沒有實際含義,它的重要作用在于從邏輯上嚴密推導(dǎo)標記清理這種垃圾回收方法的正確性距帅。
當垃圾回收開始時右锨,只有白色對象。隨著標記過程開始進行時碌秸,灰色對象開始出現(xiàn)(著色)绍移,這時候波面便開始擴大。當一個對象的所有子節(jié)點均完成掃描時讥电,會被著色為黑色蹂窖。當整個堆遍歷完成時,只剩下黑色和白色對象恩敌,這時的黑色對象為可達對象瞬测,即存活;而白色對象為不可達對象,即死亡涣楷。這個過程可以視為以灰色對象為波面分唾,將黑色對象和白色對象分離抗碰,使波面不斷向前推進狮斗,直到所有可達的灰色對象都變?yōu)楹谏珜ο鬄橹沟倪^程。
GC流程圖
這應(yīng)該是比較老版本的gc流程弧蝇,但是大致正確碳褒。
- Off 代表gc當前未開啟,一輪完整的Gc總是從Off狀態(tài)開啟的看疗。
- Stack Scan 收集根對象(全局變量和goroutine棧上的變量)這個節(jié)點會開啟寫屏障 沙峻。
a. 不需要回收棧上的對象為什么需要掃描棧上的變量?
b. 開啟寫屏障需要進行stw两芳。(這里go 1.14 做了什么優(yōu)化摔寨?) - Mark 標記對象,知道標記完所有根對象和根對象可達對象怖辆,此時寫屏障會記錄所有指針的更改是复。
- Mark Termination 重新掃描部分全局變量和發(fā)生更改的棧變量,完成標記竖螃。(stw主要耗費時間)
- Sweep 并發(fā)清除為標記的對象淑廊。
GC的優(yōu)化迭代
上圖我們發(fā)現(xiàn)第二次的Stw需要很久,go是怎么優(yōu)化的呢特咆?答案是混合寫屏障
首先我們要知道為什么需要stw季惩?
因為標記工作和用戶邏輯代碼是并行的,標記完成后的棧重新執(zhí)行用戶代碼后可能會破壞當前的標記現(xiàn)場腻格。比如一個黑色對象因為業(yè)務(wù)代碼引用了堆上的白色對象画拾,如此垃圾回收的正確性就被破壞了。白色對象后續(xù)會被回收菜职,相當于棧上的一個對象引用了一個需要被回收的對象碾阁。為此解決這種情況有3種方案:
- 棧上使用寫屏障,(屏障就是用戶對變量操作時插入的特定代碼)些楣,被掃描后的棧就開啟寫屏障脂凶,所有引入的對象全部被標為黑色。
- 在gc過程中掃描后的棧正常運行愁茁,不做處理蚕钦,等掃描階段結(jié)束后,stw鹅很,重新對活躍的棧進行掃描嘶居。這也是上文做的方式。
- 對堆采用混合寫屏障,對棧不做處理邮屁。棧上運行
最終go采用了第三種方案整袁,性能和準確性上面都獲得了保證。