“ 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 分析記憶體占用。

但是不清楚怎麼實作?不清楚怎麼看名額?不清楚 flat,cum的差別?我們就從這個問題展開。
3. 名字說明
記憶體分析的時候,有四個輸入選項:
- alloc_objects : 曆史總配置設定的累計
- alloc_space :曆史總配置設定累計
- inuse_objects:目前正在使用的對象數
- 堆上配置設定出來,業務正在使用的,也包括業務沒有使用但是還沒有垃圾回收掉的對象。
- inuse_space:目前正在使用的記憶體
兩個輸出選項:
- flat:平坦配置設定,非累加
- cum:累加
思考幾個問題:
- 上面說的對象是什麼概念?
- 經常使用記憶體分析,這個記憶體分析是否是精确的?性能消耗大不大
- 為啥顯示的是堆棧?不是說配置設定的對象嗎?為啥不直接顯示配置設定的對象結構名?
4. 記憶體怎麼采樣?
4.1 編譯期間逃逸分析
說明下,golang pprof是分析從堆上配置設定的記憶體。golang的記憶體在堆上,還是在棧上?這個不是我們決定的,就算你調用new這個關鍵字,也不一定是在堆上配置設定。
逃逸分析是golang的一個非常重要的一個點。對于記憶體配置設定,垃圾回收的設計都有非常重要的影響。
4.2 采樣的簡單實作
采樣的實作非常簡單。簡單描述流程:
- 用一個公共變量用來記錄
- 配置設定記憶體的時候,加alloc size,加alloc對象數
- 釋放記憶體的時候,加free size,加free對象數
累計配置設定:就是alloc 目前在用 inuse:就是
alloc-free
4.3 記憶體采樣的時機
采樣的時機說3個點:
- 配置設定堆上記憶體的時候,累計配置設定
- 回收器釋放堆上記憶體的時候,累計釋放
- 每512KB打點采樣
但是注意一點:并不是每一次配置設定記憶體都會被采樣。也就是說這裡其實是有個權衡的。現在是每滿512KB才會采樣一次。這裡的考慮是性能和采樣效果的權衡。因為采樣是要耗費性能的,是要取堆棧的。
怎麼了解?舉個例子
理想情況下(不考慮其他任何影響):
那麼有人會想,這樣豈不是會漏掉了很多記憶體?統計還能用來排查問題嗎?
這個是性能和效果的一個考慮,一般來講,我們是用pprof分析記憶體占用的時候,在整個golang程式跑起來後,時時刻刻都在配置設定釋放記憶體,每累計配置設定512KB,打點一次。雖然會漏掉一些記憶體配置設定釋放,但是對每個結構都是公平的。如果有一個記憶體洩露配置設定行為,那麼累計下來一定會被抓住的,并且是非常容易被抓住。
4.4 記憶體采樣的入口
記憶體采樣的入口,這個非常簡單了解。肯定是一個在配置設定記憶體的函數位置,一個是釋放記憶體的位置。這裡要特意提下上下文環境。因為golang是垃圾回收類型的語言,記憶體配置設定是完全交由golang自己管理,自己不能管理記憶體。
兩個入口函數:
- mProf_Malloc
- mProf_Free
這兩個是配套使用的采樣打點函數。而且一定是配套的。簡單說:
- mProf_Malloc 是由業務程式行為(指派器)觸發的,配置設定記憶體嘛。比如你new了一個對象,這個對象在堆上,那麼會調用
配置設定記憶體,如果到了采樣點,那麼會調用mallocgc
采樣。mProf_Malloc
- mProf_Free 是回收器在确定并且将要回收記憶體的時候調用的。是垃圾回收過程的一環。并且還要注意一點,隻有打過點的(mProf_Malloc計數過的對象,會有一個特殊處理),才會配套使用mProf_Free。
- 不是說,任意給一個記憶體位址給你。你都知道這個是業務類型。
4.5 記憶體采樣的資訊
這裡問你的是,golang采樣是采樣啥?類型資訊?這裡也說過一點,記憶體這裡和類型系統是沒啥關系的。這裡采樣的是配置設定棧,也就是配置設定路徑。
4.5.1 flat,cum 分别是怎麼來的?
看個例子:
大家可以先猜下,我們看alloc_space。這個記憶體會是怎麼累計到的。實際統計如下:
和大家猜的一樣嗎?這些是怎麼看。
首先說幾個結論:
- flat統計到的,就是這個函數實際配置設定的。
- cum是累計的,包含自己配置設定的,也包含路過的。
- cum和flat不相同的時候,代表這個函數除了自己配置設定記憶體,自己内部調用的别的函數也在配置設定記憶體。
重點提示:這個要了解這個,首先要知道,記憶體采樣的是什麼,記憶體采樣的是配置設定棧。
解釋說明
(圖中140M我們當150M看哈,這裡采樣少了第一次,細節原因可以看代碼,這裡提一下,不做闡述。):
- main函數裡,A函數調用了5次,B函數 5次,C函數5次。其中B會調用A,C會調用B。
- 調用一次A會配置設定10M記憶體,調用一次B會配置設定20M,調用一次C會配置設定30M。總累計配置設定記憶體是300M
- A函數實際調用次數是 15次;這個和flat的值是一緻的:150M
- (A) * 5
- (B -> A) * 5
- (C -> B -> A) * 5
- B函數函數實際調用10次;這個和flat的值也是一緻的:100M
- B * 5
- (C -> B) * 5
- C函數5次:這個和flat的值是一緻的:50M
- C * 5
- main函數300M,也是一緻的。
圖示
記住一句話:采樣是記錄配置設定堆棧,而不是類型資訊。
4.6 golang的類型反射
思考幾個問題:
- 任意給一個記憶體位址給你,能知道這個對象類型嗎?
- golang的反射到底是怎麼回事?
先說結論:golang裡面,記憶體塊是沒有攜帶對象類型資訊的,這個跟C是一樣的。但是golang又有反射,golang的反射一定要基于interface使用。這個要仔細了解下。
因為,golang裡面interface的結構變量,是會記錄type類型的。
反射定律一:反射一定是基于接口的。是從接口到反射類型。
反射定律二:反射一定是基于接口的。是從反射類型到接口。
還是那句話,golang的反射一定是依賴接口類型的,一定是經過接口倒騰過的。
因為目前接口這個類型對應了兩個内部結構:
struct iface
,
struct eface
,這兩個結構都是會存儲type類型。以後的一切都是基于這個類型的。
5. 記憶體配置設定
5.1 C語言你配置設定和釋放記憶體怎麼做?
思考一個問題,在C語言裡,我們配置設定記憶體:
配置設定記憶體的時候,傳入大小,拿到一個指針。
ptr = malloc(1024);
釋放記憶體的時候,直接傳入ptr,沒有任何其他參數:
free (ptr);
釋放的時候,怎麼确定釋放哪些位置?如果要你自己實作,有很多簡單的思路,說一個最簡單的:配置設定的時候,不止配置設定1024位元組,還配置設定了其他的資訊,帶head了。
這種配置設定方式有什麼問題:
- 開銷大,在通用的記憶體配置設定器中,很多場景下,有可能meta資訊比自身還要大。
5.2 記憶體配置設定設計考慮的幾個問題
- 性能
- 局部性
- 碎片率
- 内部碎片率
- 外部碎片率
5.3 golang的記憶體配置設定
golang大方向的考慮就是基于局部性和碎片率來考慮的。使用的是和tcmalloc一緻的設計。
5.3.1 整體設計
首先,記憶體塊是不帶類型資訊的。像我們在C語言裡面,有時候實作的簡單的記憶體池,在不考慮一些開銷的時候,會把業務類型放到meta資訊裡,為的是排查問題友善。golang記憶體管理作為一個通用子產品,不會這麼搞。
5.3.1.1 位址空間設計
很多時候,你查golang的資料,會看到這張圖:
這張圖有幾個資訊比較重要:
- 為什麼spans區域是512M,bitmap區是16G,arena是512G?先不要糾結值,我們先說這個比例關系:
- spans區域,一個指針大小(8Byte)對應arena的一個page(8KB),倍數是1024
- bitmap區域,一個位元組(8bit)對應arena的32Bytes,倍數是32倍
- 我們給使用者配置設定的記憶體就是arena區域的記憶體,spans區,bitmap區均為其他用途的中繼資料資訊。
- bitmap這個實作我們這次不談,不同通過這個你得知道一點:并不是所有的記憶體空間都會掃描一把,是有挑選判斷的。
- spans區域是一般用來根據一個記憶體位址查詢mspan結構的。調用函數:spanOf。
- bitmap是用來輔助垃圾回收用的區域。有這個bitmap資訊可以提高回收效率和精度。注意一點,這個不是辨別object是否配置設定的位圖,辨別是否配置設定object的問題是
結構。這個可以了解為提高垃圾回收效率的實作。mspan.allocBits
注意幾個點:
- 很多文章都提到golang記憶體512GB這個事情。512GB說的是記憶體虛拟位址空間的限制,是最大能力,是最大的規劃利用。golang之前最大可以使用的記憶體位址空間。
- golang1.11 之後已經沒有512GB的限制了。基本上和系統的虛拟位址空間一緻
- 這個比例還是一樣的,1:1024,1:32
- 就算golang1.11之前,也不是說golang的程式上來就向系統申請這麼大塊虛拟位址。也是每64M的申請,管理對象單元是heapArea結構。
- 三個區域看着連續結在一起,但是其實不是連續的位址。
- 實際的實作中都是以64M(heapArena)的小機關進行的。
5.3.2 抽象對象概念
實體偏向概念:
- heapArena:堆上實體空間管理的一個小單元,64M一個。
- page:實體記憶體最小機關,8KB一個。
邏輯偏向概念:
- span:span為記憶體配置設定的一個管理單元。span内按照固定大小size劃分,相同的size劃分為同一類。一個span管理一個連續的page。
- 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%左右。
說明:
- tail wast實際是浪費的外部碎片
- 比如說,第一種size,8位元組。一個page 8KB,8位元組剛好對齊。外部碎片為0.
- max waste說的是最大的内部碎片率
- 怎麼算的?每一個放進該span的對象大小都是最小值的情況
- 比如說,第一種size,8位元組。最小的對象是1位元組,浪費7位元組,最大碎片率為 1-1/8 = 87.5%
怎麼的出來的這些值?經驗值吧,可能。
6. 記憶體回收
6.1 golang協程搶占執行
首先,golang沒有真正的搶占。golang排程機關為協程,所謂搶占,也就是強行剝奪執行權。但是有一點,golang本質上是非搶占的,不像作業系統那樣,有時鐘中斷和時間片的概念。golang雖然裡面是有一個搶占的概念,但是注意了,這個搶占是建議性質的搶占,也就是說,如果有協程不聽話,那是沒有辦法的,實作搶占的效果是要對方協程自己配合的。
一句話:系統想讓某個goroutine自己放棄執行權,會給這個協程設定一個魔數,協程在切排程,或者其他時機檢查到了的時候,會感覺到這一個行為。
目前的搶占實作是:
- 給這個協程設定一個的魔數(stackguard)。每個函數的入口會比較目前棧寄存器值和stackguard值來決定是否觸發morestack函數。(這是一個搶占排程點)
- 協程調用函數的時候,會檢查是否需要棧擴容。如果被設定了搶占标示,那麼就會首先調用到
- 調用newstack,在newstack裡面判斷是否是特殊值,這種特殊值,目的不在于擴容,而在于讓出排程。
是以,在golang裡面,隻要有函數調用,就會有感覺搶占的時機。stw就是基于這個實作的。
思考一個問題:
如果有一個猥瑣的函數:非常耗時,一直在做cpu操作,并且完全沒有函數調用。這種情況下,golang是沒有一點辦法的。那麼這種情況會影響到整個程式的能力。
是以,我們平時寫函數,一定要短小精悍,功能拆分合理。
6.2 STW是怎麼回事?
STW:stop the world,也就是說暫停說由協程的排程和執行。stw是怎麼實作?stw的基礎就是上面提到的搶占實作。stw調用的目的是為了讓整個程式(指派器停止),那麼就需要剝奪每一個協程的執行。
stw在垃圾回收的幾個關鍵操作裡是需要的,比如開啟垃圾回收,需要stw,做好準備工作。如果stw的時候,出現了猥瑣的函數,那麼會導緻整個系統的能力降低。因為大家都在等你一個人。
6.3 垃圾回收要求
- 正确性:絕對不能回收正在使用的的記憶體對象。
- 存活性:一輪回收過程一定是有邊界,可結束的。
6.4 golang版本疊代曆史
- go 1.3 以前,使用是标記-清掃的方式,整個過程需要stw
- go 1.3 版本分離了标記和清掃操作,标記過程stw,清掃過程并發執行
- go 1.5 版本在标記過程中,使用三色标記法。回收過程分為四個階段,其中,标記和清掃都并發執行的,但标記階段的前後需要stw一定時間來做gc的準備工作和棧的re-scan。
- 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之前的圖:
當下GC大概分為四個階段:
- GC準備階段
- 标記階段
- 标記結束階段
- 清理階段
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:堆上對象指派的時候,插入寫屏障,保護強三色不變式
step2:删除的時候,沒啥問題
棧上對象指派:
step3:棧上對象指派的時候,沒有寫屏障。白色對象直接被黑色對象引用。
step4:删除灰色保護路徑。
是以才需要在mark terminato階段,重新掃描棧。
6.8.2 删除寫屏障
(Yuasa '90)
writePointer ( slot, ptr ): // 删除之前,保護原先白色或者灰色指向的資料塊 if ( isGery ( slot ) || isWhite ( slot ) ) shade ( *slot ) *slot = ptr
這個是通用的一種寫屏障技術。golang并沒有實作,而是實作了插入寫屏障。原因就在于:這個在垃圾回收之前,必須做一個快照掃描,這個就會對使用者時延有比較嚴重的影響。下面詳述。
主要流程:
- 在标記之前,需要打一個引用關系的快照。是以,這個對于棧記憶體很大的時候,影響越大。
- 不需要完整的快照,隻需要在掃描堆對象之前,確定所有的棧對象是黑色的。引用都是灰色的,這樣就保證了一個前提:所有可達的對象都處于灰色保護狀态中。
- 對棧快照掃描需要stw,去掃描棧對象。這個時候,是需要暫停所有的使用者程式。
- 掃描堆對象的時候,可以和應用程式并發的。此後根一直保持黑色(黑色指派器),不用再掃描棧。
- 對象被删除的時候,删除寫屏障會捕捉到。置灰。
- 上面的僞代碼顯示有條件,其實第一版的時候是沒有條件的。
- 這裡加上條件是為了回收精度:當上遊之前是白色或者灰色才需要把這個置灰色。如果是黑?那麼一定是處于灰色保護狀态,因為這個是前提(了解這個非常重要)。
(開始的時候,stw掃描棧,得到灰色對象)
圖表示範
初始掃描快照後:
step1: 指派。這裡指派是允許的,雖然是破壞了強三色不變式。但是還是符合弱三色不變式。
step2:删除。這裡就攔截了,必須置灰色。保證弱三色不變式。
回收精度:
删除寫屏障的精度比插入寫屏障的精度更低。删除的即使是最後一個指針,也會保留到下一輪,屬于一個浮動垃圾。這個比插入屏障精度還低。因為,對于插入屏障所保留的對象,回收器至少可以确定曾在其中執行了某些回收相關的操作(擷取或寫入對象的引用),但删除屏障所保留的對象卻不一定被指派器操作過。
為什麼需要打快照?
删除寫屏障,又叫快照屏障增量技術(或者說,一定要配合這個來做)。
- 首先,是需要stw,針對掃描整個棧根打做一遍掃描。相當于一個快照。這個過程掃描之後,就能保證目前(時刻)所有可達的對象都處于灰色保護狀态,滿足弱三色不變式。
- 然後,指派器和回收器就可以并發。但是并發有可能會破壞導緻弱三色不變式。這個時候,就需要删除寫屏障來時刻保護白色對象。
golang為啥沒有用這個?
- 一個是精度問題,這個精度要比插入寫屏障低;
- 考慮goroutine可能非常多,不适合上來就stw,掃描所有的記憶體棧。這個适合小記憶體的場景。
- 思考一個問題:這個和混合寫屏障有沒有差別?還是有差別的,這裡是要鎖整個棧,混合寫屏障是并發的,每次隻需要鎖單個棧。
6.8.3 混合寫屏障
混合屏障是結合插入屏障和删除屏障。
僞代碼:
writePointer (slot, ptr) : // 保護原來的(被删除的) shade ( *slot ) if current stack is grey: // 如果對象為灰色,則還需要保護新指向的對象 shade ( ptr ) *slot = ptr
(開始的時候,stw掃描棧,得到黑色對象)
golang實際情況:
僞代碼如上。但是這裡提出來一點,golang根本不是和僞代碼說的這樣。沒有做條件判斷,是以現在的回收精度很低。這個算是一個TodoList。
注意:使用了混合屏障,還是針對堆上的,棧上對象寫入還是沒有barrier。golang之前隻使用插入屏障,關鍵在于棧對象沒有,導緻棧上黑對象可能指向白對象。是以要rescan。因為如果不rescan,而且又破壞了弱三色不變式(沒有處于灰色保護鍊中),那麼就丢資料了。
混合屏障,就是結合删除屏障,保護這一個前提,代價就是進一步降低回收精度。
圖表示例:
混合屏障就是要解決:棧指向白色對象,stw重新掃描棧的問題。
step1:指派白對象到黑對象引用,這個不會阻止這個,也不會有寫屏障。就是一個正常的指派。
- 這個時候黑色指向了白色對象。破壞了強三色不變式。
- 但是這個白色對象還處于灰色狀态保護下。符合弱三色不變式。
step2:删除指針的時候,意圖破壞弱三色不變式的時候,寫屏障就會把這個對象置灰色。
問題一:如果有個還會想?由于棧上沒有寫屏障,這個删除的對象式根指向的呢?如果存在以下場景?
step1:堆上的白色對象引用指派給黑色棧對象。
step2:如果删除指針,豈不是連弱三色不變式也破壞了?
這個怎麼辦呢?
答案是:其實根本就不可能出現這個場景的引用圖。第一個圖就不會出現。因為雖然沒有stw,但是掃描某個g的時候,這個g是暫停的。相當于這個g棧是一個快照狀态。
混合寫屏障的棧,要麼全黑,要麼全白(單個棧)
那麼這個暫停g這個是怎麼做到的?
- 掃描的時候,會設定一個 _Gscan 狀态。
- casgstatus的時候,保證循環等待這個狀态完成。之前是直接吃cpu的,後面做了一個優化,加了一個yield,5us的間隔。
- 關于這段代碼的改動
-
golang 反射_golang 記憶體管理分析大綱
問題二:如果是多個棧呢,那麼就不是原子的快照了。比如下圖?那麼就可能導緻這種情況。
如果說A和前面的黑色對象不屬于同一個g棧。那麼是否可能會導緻這種場景出現?分析下:
- 這個場景是有這麼一個白色對象,先隻被G2棧根引用到。
- 目前G1已經被掃描完,G2還沒有掃描。
- 把這個白色對象指派給G1棧的黑色對象。
- 這個時候把G2對白色對象的引用删掉,這樣豈不是會出現黑色白色對象,且為唯一指針?
答案是:這裡的關鍵在于第三步。G1的棧對象接受指派,這個并不是憑空來的。那麼一定是G1自己找來的,可達的對象。這個是一個前提。是以,如果能接受這樣的指派,那麼這個白色對象一定是處于G1棧的灰色保護下,因為G1一定是可通路這個對象的。否則,根本就不能完成這個指派。
混合寫屏障的場景,白色對象處于灰色保護下,但是隻由堆上的灰色對象保護。注意了解這點;
屏障生成示例:
- 寫堆上内容,才會在編譯期間生成寫屏障
- 棧上的寫,不會有寫屏障。
runtime.gcWriteBarrier :
- 計算出wbBuf的next位置
- record ptr
- ptr指針放到wbBuf隊列中。
- 把
存到wbBuf隊列中 ( 置灰色,flush了就是灰色 )*(slot)
-
shade( *slot )
-
- 如果隊列沒有滿
- 那麼就指派寫(
); 則傳回*(slot) = ptr
- 那麼就指派寫(
- 如果隊列滿了,那麼跳到flush
- wbBufFlush就是把wbBufFlush裡的元屬flush到灰色隊列中。
- 調用完了 runtime.wbBufFlush 處理之後,傳回指派ret(
)*(slot) = ptr
這麼看起來,就不存在 判斷stack是否為灰色的條件?
6.8.4 其他屏障
writePointer(slot, ptr): shade(*slot) shade(ptr) *slot = ptr
優點:
- 這種無條件的屏障更加容易了解,直接把目标和源都置灰色保護
- heap上沒有黑色到白色的指針
- 唯一有可能出現黑色到白色的引用 隻可能出現在 被掃描了的stack
- 一旦 stack 被掃描過了,隻有一種辦法能得到白色對象指針(white pointer):通過transfer一個可達(reachable)對象
- 删除屏障和混合寫屏障,保護了
這個指針,就保護了一條路徑:這個來路一定是灰色的,下遊的白色都會收到保護。并且,我們知道,棧上得到的白色指針一定是可達的,那麼一定是有堆上灰色對象保護的。shade(*slot)
- 删除屏障和混合寫屏障,保護了
- 任何一個白色對象(被黑色棧對象指向的)一定是被堆上灰色對象保護可達的。
缺點:
這種屏障會導緻比較多的屏障,兩倍。是以針對這個考慮權衡,會加一個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)
那麼掃描是不會掃描這個内部的。
記憶體池配置設定器接口:
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裡面實作記憶體配置設定器,适用處理兩種情況:
- 一種是用于配置設定對象裡面不包含其他引用
- 另一種,包含的引用對象也在這個配置設定器裡
其實,沒必要自己搞通用記憶體池。一旦繞過了golang的類型系統,就會出現坑。
堅持思考,方向比努力更重要。關注我:奇伢雲存儲