網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
在開(kāi)始剖析Go逃逸分析前,我們要先清楚什么是堆棧。數(shù)據(jù)結(jié)構(gòu)中有堆棧,內(nèi)存分配中也有堆棧,兩者在定義和用途上雖不同,但也有些許關(guān)聯(lián),內(nèi)存分配中棧的壓棧和出棧操作,類似于數(shù)據(jù)結(jié)構(gòu)中的棧的操作方式
內(nèi)存分配中的堆棧
程序在運(yùn)行過(guò)程中,必不可少的會(huì)使用變量、函數(shù)和數(shù)據(jù),變量和數(shù)據(jù)在內(nèi)存中存儲(chǔ)的位置可以分為:堆區(qū)(Heap)和棧區(qū)(Stack),一般由C或C++編譯的程序占用內(nèi)存為:
- 棧區(qū)
- 堆區(qū)
- 全局區(qū)
- 常量區(qū)
- 程序代碼區(qū)
軟件程序中的數(shù)據(jù)和變量都會(huì)被分配到程序所在的虛擬內(nèi)存空間中
棧
每個(gè)函數(shù)都有自己獨(dú)立的棧空間,函數(shù)的調(diào)用參數(shù)、返回值以及局部變量大都被分配到該函數(shù)的棧空間中, 這部分內(nèi)存由編譯器進(jìn)行管理,編譯時(shí)確定分配內(nèi)存的大小。棧空間有特定的結(jié)構(gòu)和尋址方式,所以尋址十分迅速、開(kāi)銷小,只需要2條 CPU 指令,即壓棧出棧?PUSH
?和?RELEASE
,由于函數(shù)棧內(nèi)存的大小在編譯時(shí)確定, 所以當(dāng)局部變量數(shù)據(jù)太大就會(huì)發(fā)生棧溢出(Stack Overflow)。當(dāng)函數(shù)執(zhí)行完畢后, 函數(shù)的棧空間被回收, 無(wú)需手動(dòng)去釋放。
區(qū)別于堆空間,通過(guò)?malloc
?出來(lái)的內(nèi)存,函數(shù)執(zhí)行完畢后需要“手動(dòng)”釋放,“手動(dòng)”釋放在有垃圾回收的語(yǔ)言中,表現(xiàn)為垃圾回收系統(tǒng),比如 Golang 語(yǔ)言的 GC 系統(tǒng),GC 系統(tǒng)通過(guò)標(biāo)記等手段,識(shí)別出需要回收的空間。
堆
堆空間沒(méi)有特定的結(jié)構(gòu),也沒(méi)有固定的大小,可以動(dòng)態(tài)進(jìn)行分配和調(diào)整,所以內(nèi)存占用較大的局部變量會(huì)放在堆空間上,在編譯時(shí)不知道該分配多少大小的變量,在運(yùn)行時(shí)也會(huì)分配到堆上,在堆上分配內(nèi)存開(kāi)銷比在棧上大,而且堆上分配的內(nèi)存需要手動(dòng)釋放,對(duì)于 Golang 這種有 GC 機(jī)制的語(yǔ)言, 也會(huì)增加 GC 壓力, 也容易造成內(nèi)存碎片。
注:棧是線程級(jí)的,堆是進(jìn)程級(jí)的
內(nèi)存逃逸
所謂內(nèi)存逃逸,就是本該分配于棧空間的變量,被分配到了堆空間,過(guò)多的內(nèi)存逃逸會(huì)導(dǎo)致GC壓力變大,堆空間碎片化。
Go語(yǔ)言中,變量不能顯示的指定分配在棧空間還是堆空間,但是官方回復(fù)中大致表示了一個(gè)原則:如果局部變量被其他函數(shù)捕獲,那么就分配在堆上。
逃逸分析
在編程語(yǔ)言的編譯優(yōu)化原理中,分析指針動(dòng)態(tài)范圍的方法稱之為逃逸分析,通俗來(lái)說(shuō),當(dāng)一個(gè)對(duì)象的指針被多個(gè)方法或線程引用時(shí),我們稱這個(gè)指針發(fā)生了逃逸。逃逸分析有兩個(gè)基本的不變性:
- 指向棧對(duì)象的指針不能存儲(chǔ)在堆中
- 指向棧對(duì)象的指針不能超過(guò)該棧對(duì)象的存活期(即指針不能在棧對(duì)象被銷毀后依舊存活)
分析工具
通過(guò)編譯工具查看詳細(xì)的逃逸分析過(guò)程 go build -gcflags '-m -l' xxx.go
編譯參數(shù)(-gcflags):
- -N:禁止編譯優(yōu)化
- -l:禁止內(nèi)聯(lián)
- -m:逃逸分析
- -benchmem:壓測(cè)時(shí)打印內(nèi)存分配統(tǒng)計(jì)
通過(guò)逃逸分析判斷一個(gè)變量到底是分配在堆上還是棧上
逃逸場(chǎng)景
指針逃逸
指針逃逸應(yīng)該是最容易理解的一種情況了,即在函數(shù)中創(chuàng)建了一個(gè)對(duì)象,返回了這個(gè)對(duì)象的指針。這種情況下,函數(shù)雖然退出了,但是因?yàn)橹羔樀拇嬖冢瑢?duì)象的內(nèi)存不能隨著函數(shù)結(jié)束而回收,因此只能分配在堆上。
// main.go package main import "fmt" type Demo struct { name string } func createDemo(name string) *Demo { d := new(Demo) // 局部變量 d 逃逸到堆 d.name = name return d } func main() { demo := createDemo("demo") fmt.Println(demo) }
在這個(gè)例子中,函數(shù)createDemo
的局部變量d發(fā)生了逃逸,d作為返回值在main函數(shù)中繼續(xù)使用,因此d指向的內(nèi)存不能分配在棧上,只能分配在堆上,借助分析工具查看逃逸情況
$ go build -gcflags=-m main.go ./main.go:10:6: can inline createDemo ./main.go:17:20: inlining call to createDemo ./main.go:18:13: inlining call to fmt.Println ./main.go:10:17: leaking param: name ./main.go:11:10: new(Demo) escapes to heap ./main.go:17:20: new(Demo) escapes to heap //指針逃逸 ./main.go:18:13: demo escapes to heap //interface{}動(dòng)態(tài)類型逃逸 ./main.go:18:13: main []interface {} literal does not escape ./main.go:18:13: io.Writer(os.Stdout) escapes to heap <autogenerated>:1: (*File).close .this does not escape
escapes to heap
表示逃逸到堆上了
動(dòng)態(tài)反射interface{}變量
在 Go 語(yǔ)言中,接口即?interface{}
?可以表示任意的類型,如果函數(shù)參數(shù)為?interface{}
,編譯期間很難確定其參數(shù)的具體類型,也會(huì)發(fā)生逃逸。仍以上面的例子
func main() { demo := createDemo("demo") fmt.Println(demo) } ./main.go:18:13: demo escapes to heap
demo
是main函數(shù)的一個(gè)局部變量,該變量作為實(shí)參傳遞給fmt.Println()
,但是因?yàn)?code>fmt.Println()的參數(shù)類型是interface{}
,因此也發(fā)生了逃逸
解釋:fmt.Println
?之類的底層系統(tǒng)函數(shù),實(shí)現(xiàn)邏輯會(huì)基于interface{}
做反射,通過(guò) reflect.TypeOf(arg).Kind()
獲取接口對(duì)象的底層數(shù)據(jù)類型,創(chuàng)建具體類型對(duì)象時(shí),會(huì)發(fā)生內(nèi)存逃逸。由于 interface{}
的變量,編譯時(shí)無(wú)法確定變量類型以及申請(qǐng)空間大小,所以不能在棧空間上申請(qǐng)內(nèi)存,需要在 runtime
時(shí)動(dòng)態(tài)申請(qǐng),理所應(yīng)當(dāng)?shù)匕l(fā)生內(nèi)存逃逸。
申請(qǐng)棧空間過(guò)大
棧空間大小是有限的,如果編譯時(shí)發(fā)現(xiàn)局部變量申請(qǐng)的空間過(guò)大,則會(huì)發(fā)生內(nèi)存逃逸,在堆空間上給大變量分配內(nèi)存
func main() { num := make([]int, 0, 10000) _ = num } .\main.go:404:13: make([]int, 0, 10000) escapes to heap //發(fā)生逃逸
經(jīng)過(guò)測(cè)試,num := make([]int, 0, 8193)
?時(shí)剛好發(fā)生內(nèi)存逃逸。在 64 位機(jī)上?int
?類型為 8B,即 8192 * 8B = 64KB
func main() { num1 := make([]int, 0, 8192) _ = num1 num2 := make([]int, 0, 8193) _ = num2 } .\main.go:404:14: make([]int, 0, 8192) does not escape .\main.go:407:14: make([]int, 0, 8193) escapes to heap
切片變量自身和元素的逃逸
1.未指定slice的len
和cap
時(shí),slice自身未發(fā)生逃逸,slice的元素發(fā)生逃逸。因此slice會(huì)動(dòng)態(tài)擴(kuò)容,編譯器不知道容量大小,無(wú)法提前在棧空間分配內(nèi)存,擴(kuò)容后slice的元素可能會(huì)被分配到堆空間,所以slice容器自身也不能被分配到棧空間
type person struct { Name string } func main() { var num []*person p1 := &person{ Name: "ss", } num = append(num, p1) } .\main.go:409:8: &person{...} escapes to heap
2.只指定slice的長(zhǎng)度即array,數(shù)組本身和元素均在棧上分配,均未發(fā)生逃逸
閉包
所謂閉包,就是函數(shù)與其所處環(huán)境捆綁的組合,也就是說(shuō),閉包可以讓你在一個(gè)內(nèi)部函數(shù)中訪問(wèn)到其外部函數(shù)的作用域
func Increase() func() int { n := 0 return func() int { n++ return n } } func main() { in := Increase() fmt.Println(in()) // 1 fmt.Println(in()) // 2 }
Increase()
?返回值是一個(gè)閉包函數(shù),該閉包函數(shù)訪問(wèn)了外部變量 n,那變量 n 將會(huì)一直存在,直到?in
?被銷毀。很顯然,變量 n 占用的內(nèi)存不能隨著函數(shù)?Increase()
?的退出而回收,因此將會(huì)逃逸到堆上。
.\main.go:408:2: moved to heap: n .\main.go:409:9: func literal escapes to heap .\main.go:417:13: ... argument does not escape .\main.go:417:16: in() escapes to heap .\main.go:418:13: ... argument does not escape .\main.go:418:16: in() escapes to heap
逃逸分析的作用
- 通過(guò)逃逸分析能確定哪些變量分配到棧空間,哪些分配到堆空間,對(duì)空間需要 GC 系統(tǒng)回收資源,GC 系統(tǒng)會(huì)有微秒級(jí)的 STW,降低 GC 的壓力能提高系統(tǒng)的運(yùn)行效率。
- 棧空間的分配比堆空間更快性能更好,對(duì)于熱點(diǎn)數(shù)據(jù)分配到棧上能提高接口的響應(yīng)。
- 棧空間分配的內(nèi)存,在函數(shù)執(zhí)行完畢后由系統(tǒng)回收資源,不需要 GC 系統(tǒng)參與,也不需要 GC 標(biāo)記清除,可降低內(nèi)存的占用
原文鏈接:https://juejin.cn/post/7193607980046041146
相關(guān)推薦
- 2024-04-06 MyBatis的一級(jí)(同SqlSession會(huì)話),二級(jí)(不同SqlSession會(huì)話)緩存使用
- 2022-12-15 C語(yǔ)言利用鏈表實(shí)現(xiàn)學(xué)生成績(jī)管理系統(tǒng)_C 語(yǔ)言
- 2023-01-08 Python?SQLAlchemy建立模型基礎(chǔ)關(guān)系模式過(guò)程詳解_python
- 2022-04-28 Python的命令行參數(shù)實(shí)例詳解_python
- 2022-08-29 Python如何利用pandas讀取csv數(shù)據(jù)并繪圖_python
- 2022-11-23 使用Xshell建立連接并操縱服務(wù)器的方法_Linux
- 2022-02-11 SQL中ISNULL函數(shù)使用介紹_數(shù)據(jù)庫(kù)其它
- 2022-03-16 Docker安裝Nginx問(wèn)題及錯(cuò)誤分析_docker
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支