網站首頁 編程語言 正文
在開始剖析Go逃逸分析前,我們要先清楚什么是堆棧。數據結構中有堆棧,內存分配中也有堆棧,兩者在定義和用途上雖不同,但也有些許關聯,內存分配中棧的壓棧和出棧操作,類似于數據結構中的棧的操作方式
內存分配中的堆棧
程序在運行過程中,必不可少的會使用變量、函數和數據,變量和數據在內存中存儲的位置可以分為:堆區(Heap)和棧區(Stack),一般由C或C++編譯的程序占用內存為:
- 棧區
- 堆區
- 全局區
- 常量區
- 程序代碼區
軟件程序中的數據和變量都會被分配到程序所在的虛擬內存空間中
棧
每個函數都有自己獨立的??臻g,函數的調用參數、返回值以及局部變量大都被分配到該函數的??臻g中, 這部分內存由編譯器進行管理,編譯時確定分配內存的大小。棧空間有特定的結構和尋址方式,所以尋址十分迅速、開銷小,只需要2條 CPU 指令,即壓棧出棧?PUSH
?和?RELEASE
,由于函數棧內存的大小在編譯時確定, 所以當局部變量數據太大就會發生棧溢出(Stack Overflow)。當函數執行完畢后, 函數的??臻g被回收, 無需手動去釋放。
區別于堆空間,通過?malloc
?出來的內存,函數執行完畢后需要“手動”釋放,“手動”釋放在有垃圾回收的語言中,表現為垃圾回收系統,比如 Golang 語言的 GC 系統,GC 系統通過標記等手段,識別出需要回收的空間。
堆
堆空間沒有特定的結構,也沒有固定的大小,可以動態進行分配和調整,所以內存占用較大的局部變量會放在堆空間上,在編譯時不知道該分配多少大小的變量,在運行時也會分配到堆上,在堆上分配內存開銷比在棧上大,而且堆上分配的內存需要手動釋放,對于 Golang 這種有 GC 機制的語言, 也會增加 GC 壓力, 也容易造成內存碎片。
注:棧是線程級的,堆是進程級的
內存逃逸
所謂內存逃逸,就是本該分配于??臻g的變量,被分配到了堆空間,過多的內存逃逸會導致GC壓力變大,堆空間碎片化。
Go語言中,變量不能顯示的指定分配在棧空間還是堆空間,但是官方回復中大致表示了一個原則:如果局部變量被其他函數捕獲,那么就分配在堆上。
逃逸分析
在編程語言的編譯優化原理中,分析指針動態范圍的方法稱之為逃逸分析,通俗來說,當一個對象的指針被多個方法或線程引用時,我們稱這個指針發生了逃逸。逃逸分析有兩個基本的不變性:
- 指向棧對象的指針不能存儲在堆中
- 指向棧對象的指針不能超過該棧對象的存活期(即指針不能在棧對象被銷毀后依舊存活)
分析工具
通過編譯工具查看詳細的逃逸分析過程 go build -gcflags '-m -l' xxx.go
編譯參數(-gcflags):
- -N:禁止編譯優化
- -l:禁止內聯
- -m:逃逸分析
- -benchmem:壓測時打印內存分配統計
通過逃逸分析判斷一個變量到底是分配在堆上還是棧上
逃逸場景
指針逃逸
指針逃逸應該是最容易理解的一種情況了,即在函數中創建了一個對象,返回了這個對象的指針。這種情況下,函數雖然退出了,但是因為指針的存在,對象的內存不能隨著函數結束而回收,因此只能分配在堆上。
// 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) }
在這個例子中,函數createDemo
的局部變量d發生了逃逸,d作為返回值在main函數中繼續使用,因此d指向的內存不能分配在棧上,只能分配在堆上,借助分析工具查看逃逸情況
$ 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{}動態類型逃逸 ./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
表示逃逸到堆上了
動態反射interface{}變量
在 Go 語言中,接口即?interface{}
?可以表示任意的類型,如果函數參數為?interface{}
,編譯期間很難確定其參數的具體類型,也會發生逃逸。仍以上面的例子
func main() { demo := createDemo("demo") fmt.Println(demo) } ./main.go:18:13: demo escapes to heap
demo
是main函數的一個局部變量,該變量作為實參傳遞給fmt.Println()
,但是因為fmt.Println()
的參數類型是interface{}
,因此也發生了逃逸
解釋:fmt.Println
?之類的底層系統函數,實現邏輯會基于interface{}
做反射,通過 reflect.TypeOf(arg).Kind()
獲取接口對象的底層數據類型,創建具體類型對象時,會發生內存逃逸。由于 interface{}
的變量,編譯時無法確定變量類型以及申請空間大小,所以不能在棧空間上申請內存,需要在 runtime
時動態申請,理所應當地發生內存逃逸。
申請??臻g過大
棧空間大小是有限的,如果編譯時發現局部變量申請的空間過大,則會發生內存逃逸,在堆空間上給大變量分配內存
func main() { num := make([]int, 0, 10000) _ = num } .\main.go:404:13: make([]int, 0, 10000) escapes to heap //發生逃逸
經過測試,num := make([]int, 0, 8193)
?時剛好發生內存逃逸。在 64 位機上?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
時,slice自身未發生逃逸,slice的元素發生逃逸。因此slice會動態擴容,編譯器不知道容量大小,無法提前在棧空間分配內存,擴容后slice的元素可能會被分配到堆空間,所以slice容器自身也不能被分配到??臻g
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的長度即array,數組本身和元素均在棧上分配,均未發生逃逸
閉包
所謂閉包,就是函數與其所處環境捆綁的組合,也就是說,閉包可以讓你在一個內部函數中訪問到其外部函數的作用域
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()
?返回值是一個閉包函數,該閉包函數訪問了外部變量 n,那變量 n 將會一直存在,直到?in
?被銷毀。很顯然,變量 n 占用的內存不能隨著函數?Increase()
?的退出而回收,因此將會逃逸到堆上。
.\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
逃逸分析的作用
- 通過逃逸分析能確定哪些變量分配到??臻g,哪些分配到堆空間,對空間需要 GC 系統回收資源,GC 系統會有微秒級的 STW,降低 GC 的壓力能提高系統的運行效率。
- 棧空間的分配比堆空間更快性能更好,對于熱點數據分配到棧上能提高接口的響應。
- ??臻g分配的內存,在函數執行完畢后由系統回收資源,不需要 GC 系統參與,也不需要 GC 標記清除,可降低內存的占用
原文鏈接:https://juejin.cn/post/7193607980046041146
相關推薦
- 2022-03-24 C++數組和指針的區別與聯系_C 語言
- 2022-03-30 C#中的out參數、ref參數和params可變參數用法介紹_C#教程
- 2022-11-15 TypeScript數組實現棧與對象實現棧的區別詳解_其它
- 2022-05-13 系統分區卷GUID
- 2023-11-20 【ROS】用roslibpy庫在windows上用python 連接Ubuntu ROS
- 2023-01-05 Go單例模式與Once源碼實現_Golang
- 2022-10-18 Qt?TCP實現簡單通信功能_C 語言
- 2022-01-04 微信小程序內部A頁面向內嵌H5頁面跳轉,并且傳參
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支