天天看點

golang 反射_golang 記憶體管理分析大綱

“ golang的記憶體分析工具怎麼用?記憶體和回收原理,這一篇就夠了”

大綱

  • 1. 目錄
  • 2. 由一個問題展開
  • 3. 名字說明
  • 4. 記憶體怎麼采樣?
    • 4.1 編譯期間逃逸分析
    • 4.2 采樣的簡單實作
    • 4.3 記憶體采樣的時機
    • 4.4 記憶體采樣的入口
    • 4.5 記憶體采樣的資訊
    • 4.6 golang的類型反射
  • 5. 記憶體配置設定
    • 5.1 C語言你配置設定和釋放記憶體怎麼做?
    • 5.2 記憶體配置設定設計考慮的幾個問題
    • 5.3 golang的記憶體配置設定
  • 6. 記憶體回收
    • 6.1 golang協程搶占執行
    • 6.2 STW是怎麼回事?
    • 6.3 垃圾回收要求
    • 6.4 golang版本疊代曆史
    • 6.5 GC觸發條件
    • 6.6 三色定義
    • 6.7 GC流程
    • 6.8 寫屏障
    • 6.9 記憶體可見性
    • 6.10 注意問題

1. 目錄

2. 由一個問題展開

golang從語言級别,就提供了完整的采樣和分析的機制。大家經常使用 pprof 分析記憶體占用。

golang 反射_golang 記憶體管理分析大綱

但是不清楚怎麼實作?不清楚怎麼看名額?不清楚 flat,cum的差別?我們就從這個問題展開。

3. 名字說明

記憶體分析的時候,有四個輸入選項:

  1. alloc_objects : 曆史總配置設定的累計
  2. alloc_space :曆史總配置設定累計
  3. inuse_objects:目前正在使用的對象數
    1. 堆上配置設定出來,業務正在使用的,也包括業務沒有使用但是還沒有垃圾回收掉的對象。
  4. inuse_space:目前正在使用的記憶體

兩個輸出選項:

  1. flat:平坦配置設定,非累加
  2. cum:累加

思考幾個問題:

  1. 上面說的對象是什麼概念?
  2. 經常使用記憶體分析,這個記憶體分析是否是精确的?性能消耗大不大
  3. 為啥顯示的是堆棧?不是說配置設定的對象嗎?為啥不直接顯示配置設定的對象結構名?

4. 記憶體怎麼采樣?

4.1 編譯期間逃逸分析

說明下,golang pprof是分析從堆上配置設定的記憶體。golang的記憶體在堆上,還是在棧上?這個不是我們決定的,就算你調用new這個關鍵字,也不一定是在堆上配置設定。

golang 反射_golang 記憶體管理分析大綱
golang 反射_golang 記憶體管理分析大綱

逃逸分析是golang的一個非常重要的一個點。對于記憶體配置設定,垃圾回收的設計都有非常重要的影響。

4.2 采樣的簡單實作

采樣的實作非常簡單。簡單描述流程:

  1. 用一個公共變量用來記錄
  2. 配置設定記憶體的時候,加alloc size,加alloc對象數
  3. 釋放記憶體的時候,加free size,加free對象數

累計配置設定:就是alloc 目前在用 inuse:就是

alloc-free

golang 反射_golang 記憶體管理分析大綱

4.3 記憶體采樣的時機

采樣的時機說3個點:

  1. 配置設定堆上記憶體的時候,累計配置設定
  2. 回收器釋放堆上記憶體的時候,累計釋放
  3. 每512KB打點采樣

但是注意一點:并不是每一次配置設定記憶體都會被采樣。也就是說這裡其實是有個權衡的。現在是每滿512KB才會采樣一次。這裡的考慮是性能和采樣效果的權衡。因為采樣是要耗費性能的,是要取堆棧的。

怎麼了解?舉個例子

理想情況下(不考慮其他任何影響):

golang 反射_golang 記憶體管理分析大綱

那麼有人會想,這樣豈不是會漏掉了很多記憶體?統計還能用來排查問題嗎?

這個是性能和效果的一個考慮,一般來講,我們是用pprof分析記憶體占用的時候,在整個golang程式跑起來後,時時刻刻都在配置設定釋放記憶體,每累計配置設定512KB,打點一次。雖然會漏掉一些記憶體配置設定釋放,但是對每個結構都是公平的。如果有一個記憶體洩露配置設定行為,那麼累計下來一定會被抓住的,并且是非常容易被抓住。

4.4 記憶體采樣的入口

記憶體采樣的入口,這個非常簡單了解。肯定是一個在配置設定記憶體的函數位置,一個是釋放記憶體的位置。這裡要特意提下上下文環境。因為golang是垃圾回收類型的語言,記憶體配置設定是完全交由golang自己管理,自己不能管理記憶體。

兩個入口函數:

  1. mProf_Malloc
  2. mProf_Free

這兩個是配套使用的采樣打點函數。而且一定是配套的。簡單說:

  • mProf_Malloc 是由業務程式行為(指派器)觸發的,配置設定記憶體嘛。比如你new了一個對象,這個對象在堆上,那麼會調用

    mallocgc

    配置設定記憶體,如果到了采樣點,那麼會調用

    mProf_Malloc

    采樣。
  • mProf_Free 是回收器在确定并且将要回收記憶體的時候調用的。是垃圾回收過程的一環。并且還要注意一點,隻有打過點的(mProf_Malloc計數過的對象,會有一個特殊處理),才會配套使用mProf_Free。
    • 不是說,任意給一個記憶體位址給你。你都知道這個是業務類型。

4.5 記憶體采樣的資訊

這裡問你的是,golang采樣是采樣啥?類型資訊?這裡也說過一點,記憶體這裡和類型系統是沒啥關系的。這裡采樣的是配置設定棧,也就是配置設定路徑。

4.5.1 flat,cum 分别是怎麼來的?

看個例子:

golang 反射_golang 記憶體管理分析大綱

大家可以先猜下,我們看alloc_space。這個記憶體會是怎麼累計到的。實際統計如下:

golang 反射_golang 記憶體管理分析大綱

和大家猜的一樣嗎?這些是怎麼看。

首先說幾個結論:

  1. flat統計到的,就是這個函數實際配置設定的。
  2. cum是累計的,包含自己配置設定的,也包含路過的。
  • cum和flat不相同的時候,代表這個函數除了自己配置設定記憶體,自己内部調用的别的函數也在配置設定記憶體。

重點提示:這個要了解這個,首先要知道,記憶體采樣的是什麼,記憶體采樣的是配置設定棧。

解釋說明

(圖中140M我們當150M看哈,這裡采樣少了第一次,細節原因可以看代碼,這裡提一下,不做闡述。):

  1. main函數裡,A函數調用了5次,B函數 5次,C函數5次。其中B會調用A,C會調用B。
  2. 調用一次A會配置設定10M記憶體,調用一次B會配置設定20M,調用一次C會配置設定30M。總累計配置設定記憶體是300M
  3. A函數實際調用次數是 15次;這個和flat的值是一緻的:150M
    1. (A) * 5
    2. (B -> A) * 5
    3. (C -> B -> A) * 5
  4. B函數函數實際調用10次;這個和flat的值也是一緻的:100M
    1. B * 5
    2. (C -> B) * 5
  5. C函數5次:這個和flat的值是一緻的:50M
    1. C * 5
  6. main函數300M,也是一緻的。

圖示

golang 反射_golang 記憶體管理分析大綱

記住一句話:采樣是記錄配置設定堆棧,而不是類型資訊。

4.6 golang的類型反射

思考幾個問題:

  1. 任意給一個記憶體位址給你,能知道這個對象類型嗎?
  2. golang的反射到底是怎麼回事?

先說結論:golang裡面,記憶體塊是沒有攜帶對象類型資訊的,這個跟C是一樣的。但是golang又有反射,golang的反射一定要基于interface使用。這個要仔細了解下。

因為,golang裡面interface的結構變量,是會記錄type類型的。

反射定律一:反射一定是基于接口的。是從接口到反射類型。

反射定律二:反射一定是基于接口的。是從反射類型到接口。

還是那句話,golang的反射一定是依賴接口類型的,一定是經過接口倒騰過的。

golang 反射_golang 記憶體管理分析大綱

因為目前接口這個類型對應了兩個内部結構:

struct iface

struct eface

,這兩個結構都是會存儲type類型。以後的一切都是基于這個類型的。

5. 記憶體配置設定

5.1 C語言你配置設定和釋放記憶體怎麼做?

思考一個問題,在C語言裡,我們配置設定記憶體:

配置設定記憶體的時候,傳入大小,拿到一個指針。

ptr = malloc(1024);
           

釋放記憶體的時候,直接傳入ptr,沒有任何其他參數:

free (ptr);
           

釋放的時候,怎麼确定釋放哪些位置?如果要你自己實作,有很多簡單的思路,說一個最簡單的:配置設定的時候,不止配置設定1024位元組,還配置設定了其他的資訊,帶head了。

golang 反射_golang 記憶體管理分析大綱

這種配置設定方式有什麼問題:

  1. 開銷大,在通用的記憶體配置設定器中,很多場景下,有可能meta資訊比自身還要大。

5.2 記憶體配置設定設計考慮的幾個問題

  1. 性能
    1. 局部性
  2. 碎片率
    1. 内部碎片率
    2. 外部碎片率

5.3 golang的記憶體配置設定

golang大方向的考慮就是基于局部性和碎片率來考慮的。使用的是和tcmalloc一緻的設計。

5.3.1 整體設計

首先,記憶體塊是不帶類型資訊的。像我們在C語言裡面,有時候實作的簡單的記憶體池,在不考慮一些開銷的時候,會把業務類型放到meta資訊裡,為的是排查問題友善。golang記憶體管理作為一個通用子產品,不會這麼搞。

5.3.1.1 位址空間設計

很多時候,你查golang的資料,會看到這張圖:

golang 反射_golang 記憶體管理分析大綱

這張圖有幾個資訊比較重要:

  1. 為什麼spans區域是512M,bitmap區是16G,arena是512G?先不要糾結值,我們先說這個比例關系:
    1. spans區域,一個指針大小(8Byte)對應arena的一個page(8KB),倍數是1024
    2. bitmap區域,一個位元組(8bit)對應arena的32Bytes,倍數是32倍
  2. 我們給使用者配置設定的記憶體就是arena區域的記憶體,spans區,bitmap區均為其他用途的中繼資料資訊。
    1. bitmap這個實作我們這次不談,不同通過這個你得知道一點:并不是所有的記憶體空間都會掃描一把,是有挑選判斷的。
    2. spans區域是一般用來根據一個記憶體位址查詢mspan結構的。調用函數:spanOf。
    3. bitmap是用來輔助垃圾回收用的區域。有這個bitmap資訊可以提高回收效率和精度。注意一點,這個不是辨別object是否配置設定的位圖,辨別是否配置設定object的問題是

      mspan.allocBits

      結構。這個可以了解為提高垃圾回收效率的實作。

注意幾個點:

  1. 很多文章都提到golang記憶體512GB這個事情。512GB說的是記憶體虛拟位址空間的限制,是最大能力,是最大的規劃利用。golang之前最大可以使用的記憶體位址空間。
  2. golang1.11 之後已經沒有512GB的限制了。基本上和系統的虛拟位址空間一緻
    1. 這個比例還是一樣的,1:1024,1:32
  3. 就算golang1.11之前,也不是說golang的程式上來就向系統申請這麼大塊虛拟位址。也是每64M的申請,管理對象單元是heapArea結構。
  4. 三個區域看着連續結在一起,但是其實不是連續的位址。
    1. 實際的實作中都是以64M(heapArena)的小機關進行的。

5.3.2 抽象對象概念

golang 反射_golang 記憶體管理分析大綱

實體偏向概念:

  1. heapArena:堆上實體空間管理的一個小單元,64M一個。
  2. page:實體記憶體最小機關,8KB一個。

邏輯偏向概念:

  1. span:span為記憶體配置設定的一個管理單元。span内按照固定大小size劃分,相同的size劃分為同一類。一個span管理一個連續的page。
  2. object:記憶體配置設定的最小單元。

管理結構層次概念:

mcache:每個M上的,管理記憶體用的。我們都知道GMP架構,每個M都有自己的記憶體cache管理,這樣是為了局部性。隻是一個cache管理。mcentral:mheap結構所有,也隻是一個cache管理,但是是為所有人服務的。mheap:是真正負責配置設定和釋放實體記憶體的。

5.3.3 局部性的設計

這個思路很簡單,就是設計成局部性的一個層次設計。

5.3.3.1 mcache

mcache由于隻歸屬自己的M,span一旦在這個結構管理下,其他人是不可見,不會去操作的。隻有這個m會操作。是以自然就不需要加鎖。

5.3.3.2 mcentral

mcentral是所有人可見的。是以操作自然要互斥,這個的作用也是一個cache的統一管理。

5.3.3.3 mheap

這個是負責真實記憶體配置設定和釋放的的一個結構。

5.3.4 針對碎片率的設計

golang的記憶體設計目标:碎片率平均12.5%左右。

golang 反射_golang 記憶體管理分析大綱

說明:

  1. tail wast實際是浪費的外部碎片
    1. 比如說,第一種size,8位元組。一個page 8KB,8位元組剛好對齊。外部碎片為0.
  2. max waste說的是最大的内部碎片率
    1. 怎麼算的?每一個放進該span的對象大小都是最小值的情況
    2. 比如說,第一種size,8位元組。最小的對象是1位元組,浪費7位元組,最大碎片率為 1-1/8 = 87.5%

怎麼的出來的這些值?經驗值吧,可能。

6. 記憶體回收

6.1 golang協程搶占執行

首先,golang沒有真正的搶占。golang排程機關為協程,所謂搶占,也就是強行剝奪執行權。但是有一點,golang本質上是非搶占的,不像作業系統那樣,有時鐘中斷和時間片的概念。golang雖然裡面是有一個搶占的概念,但是注意了,這個搶占是建議性質的搶占,也就是說,如果有協程不聽話,那是沒有辦法的,實作搶占的效果是要對方協程自己配合的。

一句話:系統想讓某個goroutine自己放棄執行權,會給這個協程設定一個魔數,協程在切排程,或者其他時機檢查到了的時候,會感覺到這一個行為。

目前的搶占實作是:

  1. 給這個協程設定一個的魔數(stackguard)。每個函數的入口會比較目前棧寄存器值和stackguard值來決定是否觸發morestack函數。(這是一個搶占排程點)
  2. 協程調用函數的時候,會檢查是否需要棧擴容。如果被設定了搶占标示,那麼就會首先調用到
  3. 調用newstack,在newstack裡面判斷是否是特殊值,這種特殊值,目的不在于擴容,而在于讓出排程。

是以,在golang裡面,隻要有函數調用,就會有感覺搶占的時機。stw就是基于這個實作的。

思考一個問題:

如果有一個猥瑣的函數:非常耗時,一直在做cpu操作,并且完全沒有函數調用。這種情況下,golang是沒有一點辦法的。那麼這種情況會影響到整個程式的能力。

是以,我們平時寫函數,一定要短小精悍,功能拆分合理。

6.2 STW是怎麼回事?

STW:stop the world,也就是說暫停說由協程的排程和執行。stw是怎麼實作?stw的基礎就是上面提到的搶占實作。stw調用的目的是為了讓整個程式(指派器停止),那麼就需要剝奪每一個協程的執行。

stw在垃圾回收的幾個關鍵操作裡是需要的,比如開啟垃圾回收,需要stw,做好準備工作。如果stw的時候,出現了猥瑣的函數,那麼會導緻整個系統的能力降低。因為大家都在等你一個人。

6.3 垃圾回收要求

  1. 正确性:絕對不能回收正在使用的的記憶體對象。
  2. 存活性:一輪回收過程一定是有邊界,可結束的。

6.4 golang版本疊代曆史

  1. go 1.3 以前,使用是标記-清掃的方式,整個過程需要stw
  2. go 1.3 版本分離了标記和清掃操作,标記過程stw,清掃過程并發執行
  3. go 1.5 版本在标記過程中,使用三色标記法。回收過程分為四個階段,其中,标記和清掃都并發執行的,但标記階段的前後需要stw一定時間來做gc的準備工作和棧的re-scan。
  4. go 1.8 版本引入了混合寫屏障機制,避免了對棧的re-scan,極大的減少了stw的時間。

6.5 GC觸發條件

  • gcTriggerHeap 當配置設定的記憶體達到一定值就觸發GC
  • gcTriggerTime 當一定時間沒有執行過GC就觸發
  • gcTriggerCycle 要求啟動新一輪的GC,一啟動則跳過,手動觸發GC的runtime.GC( )會使用這個條件

6.6 三色定義

6.6.1 強三色

黑色對象不允許指向白色對象。

6.6.2 弱三色

黑色對象可以指向白色對象,但是前提是,該白色對象一定是處于灰色保護鍊中。

6.7 GC流程

這裡不詳細闡述了。貼一張go1.8之前的圖:

golang 反射_golang 記憶體管理分析大綱

當下GC大概分為四個階段:

  1. GC準備階段
  2. 标記階段
  3. 标記結束階段
  4. 清理階段

6.8 寫屏障

如果标記和回收不用和應用程式并發,在标記和回收整個過程直接stw,那麼就簡單了。golang為了提供低延遲時間,就必須讓指派器和回收器并發起來。但是在并發的過程中,指派器和回收器對于引用樹的了解就會出現不一緻,這裡就一定要配合寫屏障技術。

寫屏障技術,是動态捕捉寫操作,維持回收正确性的技術。寫屏障就是一段 hook 代碼,編譯期間生成,運作期間跟進情況會調用到 hook 的代碼段,也就是寫屏障的代碼;

下面系統整體的讨論下寫屏障的技術。

6.8.1 插入寫屏障

(Dijkstra '78)

writePointer ( slot, ptr ):    // 無腦保護插入的新值    shade ( ptr )    *slot = ptr
           

這個是另外一個通用的屏障技術。這個維護的是強三色不變式來保證正确性,保證黑色對象一定不能指向白色對象。golang使用的是這個屏障,插入屏障。按照道理,是幾乎完全不需要stw的。但是golang有一個處理,由于棧上面使用屏障會導緻處理非常複雜,并且開銷會非常大。是以目前golang隻針對堆上的寫操作做了屏障。

那麼就會帶來一個問題:是以當一輪掃描完了之後,在标記結束的階段,還需要重新掃描一遍goroutine棧,并且棧引用到的所有對象也要掃描。因為goroutine有可能直接指向了白色對象。在掃描goroutine棧過程中,需要stw。這個也是go1.8以前的一個非常大的延遲來源。

(開始的時候,stw掃描棧,得到灰色對象)

圖表示範

堆上路徑指派:

step1:堆上對象指派的時候,插入寫屏障,保護強三色不變式

golang 反射_golang 記憶體管理分析大綱

step2:删除的時候,沒啥問題

golang 反射_golang 記憶體管理分析大綱

棧上對象指派:

step3:棧上對象指派的時候,沒有寫屏障。白色對象直接被黑色對象引用。

golang 反射_golang 記憶體管理分析大綱

step4:删除灰色保護路徑。

golang 反射_golang 記憶體管理分析大綱

是以才需要在mark terminato階段,重新掃描棧。

6.8.2 删除寫屏障

(Yuasa '90)

writePointer ( slot, ptr ):     // 删除之前,保護原先白色或者灰色指向的資料塊    if ( isGery ( slot )  || isWhite ( slot ) )        shade ( *slot )    *slot = ptr
           

這個是通用的一種寫屏障技術。golang并沒有實作,而是實作了插入寫屏障。原因就在于:這個在垃圾回收之前,必須做一個快照掃描,這個就會對使用者時延有比較嚴重的影響。下面詳述。

主要流程:

  1. 在标記之前,需要打一個引用關系的快照。是以,這個對于棧記憶體很大的時候,影響越大。
    1. 不需要完整的快照,隻需要在掃描堆對象之前,確定所有的棧對象是黑色的。引用都是灰色的,這樣就保證了一個前提:所有可達的對象都處于灰色保護狀态中。
    2. 對棧快照掃描需要stw,去掃描棧對象。這個時候,是需要暫停所有的使用者程式。
  2. 掃描堆對象的時候,可以和應用程式并發的。此後根一直保持黑色(黑色指派器),不用再掃描棧。
  3. 對象被删除的時候,删除寫屏障會捕捉到。置灰。
    1. 上面的僞代碼顯示有條件,其實第一版的時候是沒有條件的。
    2. 這裡加上條件是為了回收精度:當上遊之前是白色或者灰色才需要把這個置灰色。如果是黑?那麼一定是處于灰色保護狀态,因為這個是前提(了解這個非常重要)。

(開始的時候,stw掃描棧,得到灰色對象)

圖表示範

初始掃描快照後:

golang 反射_golang 記憶體管理分析大綱

step1: 指派。這裡指派是允許的,雖然是破壞了強三色不變式。但是還是符合弱三色不變式。

golang 反射_golang 記憶體管理分析大綱

step2:删除。這裡就攔截了,必須置灰色。保證弱三色不變式。

golang 反射_golang 記憶體管理分析大綱

回收精度:

删除寫屏障的精度比插入寫屏障的精度更低。删除的即使是最後一個指針,也會保留到下一輪,屬于一個浮動垃圾。這個比插入屏障精度還低。因為,對于插入屏障所保留的對象,回收器至少可以确定曾在其中執行了某些回收相關的操作(擷取或寫入對象的引用),但删除屏障所保留的對象卻不一定被指派器操作過。

為什麼需要打快照?

删除寫屏障,又叫快照屏障增量技術(或者說,一定要配合這個來做)。

  1. 首先,是需要stw,針對掃描整個棧根打做一遍掃描。相當于一個快照。這個過程掃描之後,就能保證目前(時刻)所有可達的對象都處于灰色保護狀态,滿足弱三色不變式。
  2. 然後,指派器和回收器就可以并發。但是并發有可能會破壞導緻弱三色不變式。這個時候,就需要删除寫屏障來時刻保護白色對象。

golang為啥沒有用這個?

  1. 一個是精度問題,這個精度要比插入寫屏障低;
  2. 考慮goroutine可能非常多,不适合上來就stw,掃描所有的記憶體棧。這個适合小記憶體的場景。
    1. 思考一個問題:這個和混合寫屏障有沒有差別?還是有差別的,這裡是要鎖整個棧,混合寫屏障是并發的,每次隻需要鎖單個棧。

6.8.3 混合寫屏障

混合屏障是結合插入屏障和删除屏障。

僞代碼:

writePointer (slot, ptr) :    // 保護原來的(被删除的)    shade ( *slot )    if current stack is grey:        // 如果對象為灰色,則還需要保護新指向的對象        shade ( ptr )    *slot = ptr
           

(開始的時候,stw掃描棧,得到黑色對象)

golang實際情況:

僞代碼如上。但是這裡提出來一點,golang根本不是和僞代碼說的這樣。沒有做條件判斷,是以現在的回收精度很低。這個算是一個TodoList。

golang 反射_golang 記憶體管理分析大綱

注意:使用了混合屏障,還是針對堆上的,棧上對象寫入還是沒有barrier。golang之前隻使用插入屏障,關鍵在于棧對象沒有,導緻棧上黑對象可能指向白對象。是以要rescan。因為如果不rescan,而且又破壞了弱三色不變式(沒有處于灰色保護鍊中),那麼就丢資料了。

混合屏障,就是結合删除屏障,保護這一個前提,代價就是進一步降低回收精度。

圖表示例:

混合屏障就是要解決:棧指向白色對象,stw重新掃描棧的問題。

step1:指派白對象到黑對象引用,這個不會阻止這個,也不會有寫屏障。就是一個正常的指派。

  1. 這個時候黑色指向了白色對象。破壞了強三色不變式。
  2. 但是這個白色對象還處于灰色狀态保護下。符合弱三色不變式。
golang 反射_golang 記憶體管理分析大綱

step2:删除指針的時候,意圖破壞弱三色不變式的時候,寫屏障就會把這個對象置灰色。

golang 反射_golang 記憶體管理分析大綱

問題一:如果有個還會想?由于棧上沒有寫屏障,這個删除的對象式根指向的呢?如果存在以下場景?

step1:堆上的白色對象引用指派給黑色棧對象。

golang 反射_golang 記憶體管理分析大綱

step2:如果删除指針,豈不是連弱三色不變式也破壞了?

golang 反射_golang 記憶體管理分析大綱

這個怎麼辦呢?

答案是:其實根本就不可能出現這個場景的引用圖。第一個圖就不會出現。因為雖然沒有stw,但是掃描某個g的時候,這個g是暫停的。相當于這個g棧是一個快照狀态。

混合寫屏障的棧,要麼全黑,要麼全白(單個棧)

那麼這個暫停g這個是怎麼做到的?

  1. 掃描的時候,會設定一個 _Gscan 狀态。
  2. casgstatus的時候,保證循環等待這個狀态完成。之前是直接吃cpu的,後面做了一個優化,加了一個yield,5us的間隔。
    1. 關于這段代碼的改動
    2. golang 反射_golang 記憶體管理分析大綱

問題二:如果是多個棧呢,那麼就不是原子的快照了。比如下圖?那麼就可能導緻這種情況。

golang 反射_golang 記憶體管理分析大綱

如果說A和前面的黑色對象不屬于同一個g棧。那麼是否可能會導緻這種場景出現?分析下:

  1. 這個場景是有這麼一個白色對象,先隻被G2棧根引用到。
  2. 目前G1已經被掃描完,G2還沒有掃描。
  3. 把這個白色對象指派給G1棧的黑色對象。
  4. 這個時候把G2對白色對象的引用删掉,這樣豈不是會出現黑色白色對象,且為唯一指針?

答案是:這裡的關鍵在于第三步。G1的棧對象接受指派,這個并不是憑空來的。那麼一定是G1自己找來的,可達的對象。這個是一個前提。是以,如果能接受這樣的指派,那麼這個白色對象一定是處于G1棧的灰色保護下,因為G1一定是可通路這個對象的。否則,根本就不能完成這個指派。

混合寫屏障的場景,白色對象處于灰色保護下,但是隻由堆上的灰色對象保護。注意了解這點;

屏障生成示例:

golang 反射_golang 記憶體管理分析大綱
  1. 寫堆上内容,才會在編譯期間生成寫屏障
  2. 棧上的寫,不會有寫屏障。

runtime.gcWriteBarrier :

golang 反射_golang 記憶體管理分析大綱
  1. 計算出wbBuf的next位置
  2. record ptr
    1. ptr指針放到wbBuf隊列中。
  3. *(slot)

    存到wbBuf隊列中 ( 置灰色,flush了就是灰色 )
    1. shade( *slot )

  4. 如果隊列沒有滿
    1. 那麼就指派寫(

      *(slot) = ptr

      ); 則傳回
  5. 如果隊列滿了,那麼跳到flush
    1. wbBufFlush就是把wbBufFlush裡的元屬flush到灰色隊列中。
    2. 調用完了 runtime.wbBufFlush 處理之後,傳回指派ret(

      *(slot) = ptr

      )

這麼看起來,就不存在 判斷stack是否為灰色的條件?

6.8.4 其他屏障

writePointer(slot, ptr):    shade(*slot)    shade(ptr)    *slot = ptr
           

優點:

  1. 這種無條件的屏障更加容易了解,直接把目标和源都置灰色保護
  2. heap上沒有黑色到白色的指針
  3. 唯一有可能出現黑色到白色的引用 隻可能出現在 被掃描了的stack
  4. 一旦 stack 被掃描過了,隻有一種辦法能得到白色對象指針(white pointer):通過transfer一個可達(reachable)對象
    1. 删除屏障和混合寫屏障,保護了

      shade(*slot)

      這個指針,就保護了一條路徑:這個來路一定是灰色的,下遊的白色都會收到保護。并且,我們知道,棧上得到的白色指針一定是可達的,那麼一定是有堆上灰色對象保護的。
  5. 任何一個白色對象(被黑色棧對象指向的)一定是被堆上灰色對象保護可達的。

缺點:

這種屏障會導緻比較多的屏障,兩倍。是以針對這個考慮權衡,會加一個stack條件判斷,就是我們看到的混合屏障的樣子。

6.9 記憶體可見性

提一下golang的記憶體可見性。在c裡面,如果是在多線程環境,并發操作一些變量,需要考慮一些可見性的問題。比如指派一個變量,這個線程還有可能在寄存器裡沒有刷下去,或者編譯器幫你優化到寄存器中,不去記憶體讀。是以有一個volatile關鍵字,強制去記憶體讀。

golang是否有這個記憶體可見性的問題?

一句話,golang裡面,隻要你保證順序性,那麼記憶體一緻性就沒有問題。具體可以搜尋happen-before的機制。

6.10 注意問題

6.10.1 千萬不要嘗試繞過golang的類型系統

千萬不要嘗試繞過golang的類型系統。golang官方在提到uintptr類型的時候,都說不要産生uintptr的臨時變量,因為很有可能會導緻gc的錯誤回收(這個做過一個簡單的驗證,1.13本的uintptr類型是不作為指針标記的)。

舉一個極端的例子,如果你new了一個對象,然後把這個對象的位址儲存在8個不連續的byte類型裡,那就等着coredump吧。

6.10.2 在golang裡按照c的思路實作一個記憶體池很容易踩到巨坑。

比如現在你配置設定一個大記憶體出來(1G的[ ]byte類型空間)。這是一個大記憶體塊。并且golang沒有任何辨別這個地方辨別指針。

// 配置設定一個大記憶體數組(1GB),數組元素是byte。那麼自然每個元素都是不含指針的。begin := make([]byte, 1024*1024*1024)
           
golang 反射_golang 記憶體管理分析大綱

那麼掃描是不會掃描這個内部的。

記憶體池配置設定器接口:

func (ac *Allocator) Alloc (size int) unsafe.Pointer

用來配置設定對象,使用可能會導緻莫名其妙的記憶體錯誤。假設用來配置設定對象T:

type T struct {    s *S}t := (*T) (ac.Alloc(sizeT))t.s = &S{}
           

T對象是從一個大數組裡劃出來的,垃圾回收其實并不知道T這個對象。不過隻要1G記憶體池本身不被回收,T對象還是安全的。但是T裡面的S,是golang走類型系統配置設定出來的,就會有問題。

假設發生垃圾回收了,GC會認為這個記憶體空間是一個Byte數組,而不會掃描,那麼t.s指向的對象認為未被任何對象引用到,它會被清理掉。最後t.s就成了一個懸挂指針。

golang裡面實作記憶體配置設定器,适用處理兩種情況:

  1. 一種是用于配置設定對象裡面不包含其他引用
  2. 另一種,包含的引用對象也在這個配置設定器裡

其實,沒必要自己搞通用記憶體池。一旦繞過了golang的類型系統,就會出現坑。

golang 反射_golang 記憶體管理分析大綱

堅持思考,方向比努力更重要。關注我:奇伢雲存儲

golang 反射_golang 記憶體管理分析大綱