自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一篇文章把 Go 中的內(nèi)存分配扒得干干凈凈

存儲 存儲軟件
Go 在程序啟動時,首先會向操作系統(tǒng)申請一大塊內(nèi)存,并交由mheap結(jié)構(gòu)全局管理。

[[420841]]

本文轉(zhuǎn)載自微信公眾號「Go編程時光」,作者寫代碼的明哥 。轉(zhuǎn)載本文請聯(lián)系Go編程時光公眾號。

大家好,我是明哥~

今天給大家盤一盤 Go 中關(guān)于內(nèi)存管理比較常問幾個知識點。

 1. 分配內(nèi)存三大組件

Go 分配內(nèi)存的過程,主要由三大組件所管理,級別從上到下分別是:

mheap

Go 在程序啟動時,首先會向操作系統(tǒng)申請一大塊內(nèi)存,并交由mheap結(jié)構(gòu)全局管理。

具體怎么管理呢?mheap 會將這一大塊內(nèi)存,切分成不同規(guī)格的小內(nèi)存塊,我們稱之為 mspan,根據(jù)規(guī)格大小不同,mspan 大概有 70類左右,劃分得可謂是非常的精細,足以滿足各種對象內(nèi)存的分配。

那么這些 mspan 大大小小的規(guī)格,雜亂在一起,肯定很難管理對吧?

因此就有了 mcentral 這下一級組件

mcentral

啟動一個 Go 程序,會初始化很多的 mcentral ,每個 mcentral 只負責管理一種特定規(guī)格的 mspan。

相當于 mcentral 實現(xiàn)了在 mheap 的基礎(chǔ)上對 mspan 的精細化管理。

但是 mcentral 在 Go 程序中是全局可見的,因此如果每次協(xié)程來 mcentral 申請內(nèi)存的時候,都需要加鎖。

可以預(yù)想,如果每個協(xié)程都來 mcentral 申請內(nèi)存,那頻繁的加鎖釋放鎖開銷是非常大的。

因此需要有一個 mcentral 的二級代理來緩沖這種壓力

mcache

在一個 Go 程序里,每個線程M會綁定給一個處理器P,在單一粒度的時間里只能做多處理運行一個goroutine,每個P都會綁定一個叫 mcache 的本地緩存。

當需要進行內(nèi)存分配時,當前運行的goroutine會從mcache中查找可用的mspan。從本地mcache里分配內(nèi)存時不需要加鎖,這種分配策略效率更高。

mspan 供應(yīng)鏈

mcache 的 mspan 數(shù)量并不總是充足的,當供不應(yīng)求的時候,mcache 會從 mcentral 再次申請更多的 mspan,同樣的,如果 mcentral 的 mspan 數(shù)量也不夠的話,mcentral 也會向它的上級 mheap 申請 mspan。再極端一點,如果 mheap 里的 mspan 也無法滿足程序的內(nèi)存申請,那該怎么辦?

那就沒辦法啦,mheap 只能厚著臉皮跟操作系統(tǒng)這個老大哥申請了。

以上的供應(yīng)流程,只適用于內(nèi)存塊小于 64KB 的場景,原因在于Go 沒法使用工作線程的本地緩存mcache和全局中心緩存 mcentral 上管理超過 64KB 的內(nèi)存分配,所以對于那些超過 64KB 的內(nèi)存申請,會直接從堆上(mheap)上分配對應(yīng)的數(shù)量的內(nèi)存頁(每頁大小是 8KB)給程序。

 2. 什么是堆內(nèi)存和棧內(nèi)存?

根據(jù)內(nèi)存管理(分配和回收)方式的不同,可以將內(nèi)存分為 堆內(nèi)存 和 棧內(nèi)存。

那么他們有什么區(qū)別呢?

堆內(nèi)存:由內(nèi)存分配器和垃圾收集器負責回收

棧內(nèi)存:由編譯器自動進行分配和釋放

一個程序運行過程中,也許會有多個棧內(nèi)存,但肯定只會有一個堆內(nèi)存。

每個棧內(nèi)存都是由線程或者協(xié)程獨立占有,因此從棧中分配內(nèi)存不需要加鎖,并且棧內(nèi)存在函數(shù)結(jié)束后會自動回收,性能相對堆內(nèi)存好要高。

而堆內(nèi)存呢?由于多個線程或者協(xié)程都有可能同時從堆中申請內(nèi)存,因此在堆中申請內(nèi)存需要加鎖,避免造成沖突,并且堆內(nèi)存在函數(shù)結(jié)束后,需要 GC (垃圾回收)的介入?yún)⑴c,如果有大量的 GC 操作,將會吏程序性能下降得歷害。

3. 逃逸分析的必要性

由此可以看出,為了提高程序的性能,應(yīng)當盡量減少內(nèi)存在堆上分配,這樣就能減少 GC 的壓力。

在判斷一個變量是在堆上分配內(nèi)存還是在棧上分配內(nèi)存,雖然已經(jīng)有前人已經(jīng)總結(jié)了一些規(guī)律,但依靠程序員能夠在編碼的時候時刻去注意這個問題,對程序員的要求相當之高。

好在 Go 的編譯器,也開放了逃逸分析的功能,使用逃逸分析,可以直接檢測出你程序員所有分配在堆上的變量(這種現(xiàn)象,即是逃逸)。

方法是執(zhí)行如下命令

  1. go build -gcflags '-m -l' demo.go  
  2.  
  3. # 或者再加個 -m 查看更詳細信息 
  4. go build -gcflags '-m -m -l' demo.go  

內(nèi)存分配位置的規(guī)律

如果逃逸分析工具,其實人工也可以判斷到底有哪些變量是分配在堆上的。

那么這些規(guī)律是什么呢?

經(jīng)過總結(jié),主要有如下四種情況

  1. 根據(jù)變量的使用范圍
  2. 根據(jù)變量類型是否確定
  3. 根據(jù)變量的占用大小
  4. 根據(jù)變量長度是否確定

接下來我們一個一個分析驗證

根據(jù)變量的使用范圍

當你進行編譯的時候,編譯器會做逃逸分析(escape analysis),當發(fā)現(xiàn)一個變量的使用范圍僅在函數(shù)中,那么可以在棧上為它分配內(nèi)存。

比如下邊這個例子

  1. func foo() int { 
  2.     v := 1024 
  3.     return v 
  4.  
  5. func main() { 
  6.     m := foo() 
  7.     fmt.Println(m) 

我們可以通過 go build -gcflags '-m -l' demo.go 來查看逃逸分析的結(jié)果,其中 -m 是打印逃逸分析的信息,-l 則是禁止內(nèi)聯(lián)優(yōu)化。

從分析的結(jié)果我們并沒有看到任何關(guān)于 v 變量的逃逸說明,說明其并沒有逃逸,它是分配在棧上的。

  1. $ go build -gcflags '-m -l' demo.go  
  2. # command-line-arguments 
  3. ./demo.go:12:13: ... argument does not escape 
  4. ./demo.go:12:13: m escapes to heap 

而如果該變量還需要在函數(shù)范圍之外使用,如果還在棧上分配,那么當函數(shù)返回的時候,該變量指向的內(nèi)存空間就會被回收,程序勢必會報錯,因此對于這種變量只能在堆上分配。

比如下邊這個例子,返回的是指針

  1. func foo() *int { 
  2.     v := 1024 
  3.     return &v 
  4.  
  5. func main() { 
  6.     m := foo() 
  7.     fmt.Println(*m) // 1024 

從逃逸分析的結(jié)果中可以看到 moved to heap: v ,v 變量是從堆上分配的內(nèi)存,和上面的場景有著明顯的區(qū)別。

  1. $ go build -gcflags '-m -l' demo.go  
  2. # command-line-arguments 
  3. ./demo.go:6:2: moved to heap: v 
  4. ./demo.go:12:13: ... argument does not escape 
  5. ./demo.go:12:14: *m escapes to heap 

除了返回指針之外,還有其他的幾種情況也可歸為一類:

第一種情況:返回任意引用型的變量:Slice 和 Map

  1. func foo() []int { 
  2.     a := []int{1,2,3} 
  3.     return a 
  4.  
  5. func main() { 
  6.     b := foo() 
  7.     fmt.Println(b) 

逃逸分析結(jié)果

  1. $ go build -gcflags '-m -l' demo.go  
  2. # command-line-arguments 
  3. ./demo.go:6:12: []int literal escapes to heap 
  4. ./demo.go:12:13: ... argument does not escape 
  5. ./demo.go:12:13: b escapes to heap 

第二種情況:在閉包函數(shù)中使用外部變量

  1. func Increase() func() int { 
  2.     n := 0 
  3.     return func() int { 
  4.         n++ 
  5.         return n 
  6.     } 
  7.  
  8. func main() { 
  9.     in := Increase() 
  10.     fmt.Println(in()) // 1 
  11.     fmt.Println(in()) // 2 

逃逸分析結(jié)果

  1. $ go build -gcflags '-m -l' demo.go  
  2. # command-line-arguments 
  3. ./demo.go:6:2: moved to heap: n 
  4. ./demo.go:7:9: func literal escapes to heap 
  5. ./demo.go:15:13: ... argument does not escape 
  6. ./demo.go:15:16: in() escapes to heap 

根據(jù)變量類型是否確定

在上邊例子中,也許你發(fā)現(xiàn)了,所有編譯輸出的最后一行中都是 m escapes to heap 。

奇怪了,為什么 m 會逃逸到堆上?

其實就是因為我們調(diào)用了 fmt.Println() 函數(shù),它的定義如下

  1. func Println(a ...interface{}) (n int, err error) { 
  2.     return Fprintln(os.Stdout, a...) 

可見其接收的參數(shù)類型是 interface{} ,對于這種編譯期不能確定其參數(shù)的具體類型,編譯器會將其分配于堆上。

根據(jù)變量的占用大小

最開始的時候,就介紹到,以 64KB 為分界線,我們將內(nèi)存塊分為 小內(nèi)存塊 和 大內(nèi)存塊。

小內(nèi)存塊走常規(guī)的 mspan 供應(yīng)鏈申請,而大內(nèi)存塊則需要直接向 mheap,在堆區(qū)申請。

以下的例子來說明

  1. func foo() { 
  2.     nums1 := make([]int, 8191) // < 64KB 
  3.     for i := 0; i < 8191; i++ { 
  4.         nums1[i] = i 
  5.     } 
  6.  
  7. func bar() { 
  8.     nums2 := make([]int, 8192) // = 64KB 
  9.     for i := 0; i < 8192; i++ { 
  10.         nums2[i] = i 
  11.     } 

給 -gcflags 多加個 -m 可以看到更詳細的逃逸分析的結(jié)果

  1. $ go build -gcflags '-m -l' demo.go  
  2. # command-line-arguments 
  3. ./demo.go:5:15: make([]int, 8191) does not escape 
  4. ./demo.go:12:15: make([]int, 8192) escapes to heap 

那為什么是 64 KB 呢?

我只能說是試出來的 (8191剛好不逃逸,8192剛好逃逸),網(wǎng)上有很多文章千篇一律的說和 ulimit -a 中的 stack size 有關(guān),但經(jīng)過了解這個值表示的是系統(tǒng)棧的最大限制是 8192 KB,剛好是 8M。

  1. $ ulimit -a 
  2. -t: cpu time (seconds)              unlimited 
  3. -f: file size (blocks)              unlimited 
  4. -d: data seg size (kbytes)          unlimited 
  5. -s: stack size (kbytes)             8192 

我個人實在無法理解這個 8192 (8M) 和 64 KB 是如何對應(yīng)上的,如果有朋友知道,還請指教一下。

根據(jù)變量長度是否確定

由于逃逸分析是在編譯期就運行的,而不是在運行時運行的。因此避免有一些不定長的變量可能會很大,而在棧上分配內(nèi)存失敗,Go 會選擇把這些變量統(tǒng)一在堆上申請內(nèi)存,這是一種可以理解的保險的做法。

  1. func foo() { 
  2.     length := 10 
  3.     arr := make([]int, 0 ,length)  // 由于容量是變量,因此不確定,因此在堆上申請 
  4.  
  5. func bar() { 
  6.     arr := make([]int, 0 ,10)  // 由于容量是常量,因此是確定的,因此在棧上申請 

# 參考文章

 

https://xie.infoq.cn/article/ee1d2416d884b229dfe57bbcc

 

責任編輯:武曉燕 來源: Go編程時光
相關(guān)推薦

2021-09-27 09:51:03

擴容Go Map賦值

2021-08-12 14:19:14

Slice數(shù)組類型內(nèi)存

2019-07-26 15:01:42

SparkShuffle內(nèi)存

2019-06-06 15:22:07

SparkShuffle內(nèi)存

2025-02-14 09:53:50

2020-10-09 08:15:11

JsBridge

2020-12-16 08:07:28

語言基礎(chǔ)反射

2020-12-23 08:39:11

Go語言基礎(chǔ)技術(shù)

2020-11-05 09:58:16

Go語言Map

2021-09-15 10:00:33

Go語言Modules

2021-09-29 10:00:07

Go語言基礎(chǔ)

2021-10-13 10:00:52

Go語言基礎(chǔ)

2022-02-16 10:03:06

對象接口代碼

2020-10-22 08:33:22

Go語言

2021-05-29 10:20:54

GoModules語言

2020-10-22 11:15:47

Go語言變量

2020-11-11 10:52:54

Go語言C語言

2021-05-18 09:00:28

Pythonclass

2017-09-05 08:52:37

Git程序員命令

2022-02-21 09:44:45

Git開源分布式
點贊
收藏

51CTO技術(shù)棧公眾號