前言
很久以前就聽過過內存逃逸這個詞, 最近了解了一下, 才發現是個很簡單的概念. 只要把前言部分看完, 就已經瞭解了. 來吧...
在介紹內存逃逸之前, 我們先用C
語言來引出這個概念.
我們的進程在內存中有棧內存
和堆內存
的概念, 棧內存
是函數執行的局部內存, 會隨着函數的結束而全部釋放, 而堆內存
是需要手動申請和管理的一部分內存. 這個概念大家都比較熟悉了, 在此就不再贅述.
c語言版本
在C
中, 如果我們在函數中想要返回一個整形數組, 怎麼寫呢? 比如這樣?
#include "stdio.h"
int* test(){
int a[2] = {1, 3};
return a;
}
int main() {
int* a= test();
printf("address: %p, %d", a, a[1]);
}
如果你這樣做了, 可能會發現讀到的數組數據是正確的, 但在使用gcc
編譯的時候會報警, 提示返回的a
變量是一個棧內存地址. 這是因爲test
執行結束後, 這部分內存未來就會被其他地方使用, 結果正確僅僅是因爲內存中的內容還沒有被修改.
那麼正確的寫法應該是什麼呢? 比如這樣:
#include "stdio.h"
#include "stdlib.h"
int* test(){
int *a = (int*) malloc(2);
a[0] = 1;
a[1] = 2;
return a;
}
int main() {
int* a = test();
printf("address: %p, %d", a, a[1]);
free(a);
}
在test
函數中申請一段內存, 並將內存的指針返回. 申請的內存就保存在堆內存
中. 但是, 這樣一來, 就不能享受棧內存
的自動釋放了, 需要再使用後調用free
釋放內存, 以便後續使用.
Go版本
那麼在Go
中如果我們想在函數中返回一個數組, 怎麼寫呢?
package main
import "fmt"
func test() *[3]int {
var a [3]int
a = [3]int{1, 2, 3}
return &a
}
func main() {
a := test()
fmt.Printf("address: %p, %d", a, a[1])
}
這段代碼和上面C
版本的功能相同, 都是返回了數組的地址. 那麼問題來了, 爲什麼同樣是局部變量, Go
就可以在函數返回之後仍能讀到呢?
原因很簡單, Go
的編譯器在檢測到數組指針會在函數外部使用時, 自行將其放到了堆內存
中. 而這, 就是Go
中所說的內存逃逸現象了. 是不是看過之後感覺只是一個很簡單的道理換了個名詞而已.
其實到這裏, Go
的內存逃逸已經介紹完了, 一句話介紹就是: 局部變量被放到了堆內存中.
逃逸情況
因爲內存逃逸後會放到堆內存
中, 需要依賴GC
進行釋放, 而棧內存
會自動釋放, 無需GC
參與. 因此在開發中減少內存逃逸, 可以減輕GC
壓力.
既如此, 有沒有辦法在一個Go
程序中檢查哪裏會發生內存逃逸呢? (逃逸是發生在編譯期的呦). 就是build
命令:
go build -gcflags '-m -l' main.go
-m
: 打印逃逸分析內容. 最多的添加4個-m
, 獲取更詳細的信息all=-m
: 若編譯時不止一個文件, 對所有文件應用-m
-l
: 禁用函數內聯. 可以更準確的定位逃逸位置.all=-l
: 同理
好, 基於此, 我們簡單介紹幾種內存逃逸的情況, 更多的情況可自行摸索. (以下所有情況, 可自行通過build
命令分析查看)
返回局部變量指針
比如前言中的情況, 再或者:
func test() *int {
a := 5
return &a
}
超出棧大小
若對象在棧中放不下了, 也會發生逃逸. 棧的大小可通過命令查看: ulimit -a | grep stack
func test() {
// 當內存申請超出棧大小時, 逃逸
_ = make([]int, 8192*1024/8)
// 當使用變量進行初始化時, 因爲無法預知變量的大小, 也會逃逸
// 如果可以的話, 將 n 改爲 const, 就可以避免內存逃逸
n := 2
_ = make([]int, n)
}
閉包
閉包也很好理解, 因爲變量在函數返回之後仍需要訪問, 因此需要逃逸到堆上.
func test() func() int {
a := 0
return func() int {
return a
}
}
fmt 包
當使用fmt
包中的大部分函數時, 均會發生內存逃逸. 相關isuse
: 8618 7218
func main() {
// 沒有發生內存逃逸
_ = reflect.TypeOf("1")
// string kind 等發發會發生內存逃逸
_ = reflect.TypeOf("1").String()
_ = reflect.TypeOf("1").Kind()
// 會發生內存逃逸, 因爲其內部調用了 reflect.TypeOf("223").String()
// 調用鏈: Println->Fprintln->doPrintln->printArg->reflect.TypeOf(arg).String()
fmt.Println("223")
}
具體原因未做分析, 感興趣的可以查看其內部實現. 期待後續版本可以優化吧.
其他情況
- 切片擴容後棧空間不足
channel
發送指針變量. stackoverflow- 等等
總結
綜上, 介紹了內存逃逸的概念及常見情況. 當發生逃逸的時候, 會增加GC
的壓力. 變量放在哪裏簡單來說就是:
- 若在函數外部使用了, 則必放在堆中
- 若在函數外部沒有使用, 則優先放到棧中, 若棧中放不下, 則放到堆中
那麼我們在函數返回結構體使經常碰到的疑問: 返回"值類型"還是"指針類型"??
如果返回"值類型"就不會發生逃逸, 但是會觸發內存複製. 如果返回"指針類型"就無需內存複製, 但是會發生逃逸. 因此就需要在GC
與內存複製之間進行平衡, 判斷哪個開銷比較大. 一般來說, 若變量佔用內存較小, 傳值更爲合適. 若內存較大, 則傳遞指針更爲合適. (不過, 一般的項目都沒有到"需要考慮 GC"的情況吧???)