1. 程式人生 > 其它 >Golang - 記憶體逃逸分析

Golang - 記憶體逃逸分析

面試必問:

1、什麼是記憶體逃逸

2、記憶體逃逸的場景有哪些

3、分析記憶體逃逸的意義

4、怎麼避免記憶體逃逸

1、什麼是記憶體逃逸

在瞭解什麼是記憶體逃逸之前,我們先來簡單地熟悉一下兩個概念。棧記憶體堆記憶體

Golang的GC主要是針對堆的,不是棧。

引用型別的全域性變數分配在堆上,值型別的全域性變數分配在棧上。

區域性變數記憶體分配可能在棧上也可能在堆上。

一個物件本應該分配在棧上面,結果分配在了堆上面,這就是記憶體逃逸。

2、記憶體逃逸的場景有哪些

分析記憶體逃逸的命令: go build -gcflags '-m' main.go

常見的場景有四種:區域性指標返回,棧空間不足,動態型別,閉包引用

1)、區域性指標返回

當我們在某個方法內定義了一個區域性指標,並且將這個指標作為返回值返回時,此時就發生了逃逸。這種型別的逃逸是比較常見的,如下:

2)、棧空間不足

眾所周知,在系統中棧空間相比與總的記憶體來說是非常小的。如下,我的Mac是16G*512G的,可是整個系統中棧空間大小也才8M。

而在我們的實際編碼過程中,大部分Goroutine的佔用空間不到10KB(這也是Golang能支援高併發的原因之一)。而其中分配給棧的更是少之又少。所以一旦某個物件體積過大時候就會發生逃逸,從棧上面轉到堆上面。

有兩個map,space1和space2,space1長度大小都是100,space2長度大小都是10000,結果space2發生了逃逸,space1沒有。如下:

3)、動態型別

這種記憶體逃逸應該是最多的,最常見的,而且還無法避免。

簡單地說就是被呼叫函式的入參是interface或者是不定引數,此時就會發生記憶體逃逸。如下:

一個簡簡單單的Println居然也會發生記憶體逃逸。那麼問題來了,這個是怎麼導致的呢?

廢話不多說,直接拔掉底褲擼原始碼。此處就是所謂的動態型別。

4)、閉包呼叫

這種場景是非常少的,一般沒有人寫這種可讀性這麼差的程式碼,我們只需要知道這種場景即可,大概率是碰不上的。

3、分析記憶體逃逸的意義

簡單的總結就是兩點:減輕GC的壓力,提高分配速度

前面已經提及,Golang的GC主要是針對堆的,而不是棧。

試想一下,如果大量的物件從棧逃逸到堆上,是不是就會增加GC的壓力。

在GC的過程中會佔用比較大的系統開銷(一般可達到CPU容量的25%)而且目前所有的GC都有STW這個死結,而STW會造成使用者直觀的“卡頓”,非常影響使用者體驗。

STW : "Stop the World" 階段,當前執行的所有程式將被暫停,掃描記憶體的 root 節點和新增寫屏障 (write barrier)。

此外,堆和棧相比,堆適合不可預知大小的記憶體分配。但是為此付出的代價是分配速度較慢,而且會形成記憶體碎片。棧記憶體分配則會非常快。棧分配記憶體只需要兩個CPU指令:“PUSH”和“RELEASE”,分配和釋放;而堆記憶體分配首先需要去找到一塊大小合適的記憶體塊,之後要通過垃圾回收才能釋放。

通過逃逸分析,可以儘量把那些不需要分配到堆上的變數直接分配到棧上,堆上的變數少了,會減輕分配堆記憶體的開銷,同時也會減少GC的壓力,提高程式的執行速度。

4、怎麼避免記憶體逃逸

首先需要注意的是,Golang在編譯的時候就可以確立逃逸,並不需要等到執行時。這樣就給了咱們避免記憶體逃逸的機會。另外明確一點,小編認為沒有任何方式能絕對避免記憶體逃逸

存在【動態型別】這種逃逸方式,幾乎所有的庫函式都是動態型別的

主要的避免原則分別針對上面幾種場景:

1)儘量減少外部指標引用,必要的時候可以使用值傳遞;

2)對於自己定義的資料大小,有一個基本的預判,儘量不要出現棧空間溢位的情況;

3)Golang中的介面型別的方法呼叫是動態排程,如果對於效能要求比較高且訪問頻次比較高的函式呼叫,應該儘量避免使用介面型別;

4)儘量不要寫閉包函式,可讀性差且發生逃逸。