日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

ntfs分配单元大小_万字长文图解 Go 内存管理分析:工具、分配和回收原理

發(fā)布時(shí)間:2025/3/15 编程问答 50 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ntfs分配单元大小_万字长文图解 Go 内存管理分析:工具、分配和回收原理 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

?golang的內(nèi)存分析工具怎么用?內(nèi)存和回收原理,這一篇就夠了

大綱

  • 1. 目錄

  • 2. 由一個(gè)問(wèn)題展開(kāi)

  • 3. 名字說(shuō)明

  • 4. 內(nèi)存怎么采樣?

    • 4.1 編譯期間逃逸分析

    • 4.2 采樣的簡(jiǎn)單實(shí)現(xiàn)

    • 4.3 內(nèi)存采樣的時(shí)機(jī)

    • 4.4 內(nèi)存采樣的入口

    • 4.5 內(nèi)存采樣的信息

    • 4.6 golang的類型反射

  • 5. 內(nèi)存分配

    • 5.1 C語(yǔ)言你分配和釋放內(nèi)存怎么做?

    • 5.2 內(nèi)存分配設(shè)計(jì)考慮的幾個(gè)問(wèn)題

    • 5.3 golang的內(nèi)存分配

  • 6. 內(nèi)存回收

    • 6.1 golang協(xié)程搶占執(zhí)行

    • 6.2 STW是怎么回事?

    • 6.3 垃圾回收要求

    • 6.4 golang版本迭代歷史

    • 6.5 GC觸發(fā)條件

    • 6.6 三色定義

    • 6.7 GC流程

    • 6.8 寫屏障

    • 6.9 內(nèi)存可見(jiàn)性

    • 6.10 注意問(wèn)題

1. 目錄

2. 由一個(gè)問(wèn)題展開(kāi)

golang從語(yǔ)言級(jí)別,就提供了完整的采樣和分析的機(jī)制。大家經(jīng)常使用 pprof 分析內(nèi)存占用。

但是不清楚怎么實(shí)現(xiàn)?不清楚怎么看指標(biāo)?不清楚 flat,cum的區(qū)別?我們就從這個(gè)問(wèn)題展開(kāi)。

3. 名字說(shuō)明

內(nèi)存分析的時(shí)候,有四個(gè)輸入選項(xiàng):

  • alloc_objects : 歷史總分配的累計(jì)
  • alloc_space :歷史總分配累計(jì)
  • inuse_objects:當(dāng)前正在使用的對(duì)象數(shù)
  • 堆上分配出來(lái),業(yè)務(wù)正在使用的,也包括業(yè)務(wù)沒(méi)有使用但是還沒(méi)有垃圾回收掉的對(duì)象。
  • inuse_space:當(dāng)前正在使用的內(nèi)存
  • 兩個(gè)輸出選項(xiàng):

  • flat:平坦分配,非累加
  • cum:累加
  • 思考幾個(gè)問(wèn)題:

  • 上面說(shuō)的對(duì)象是什么概念?
  • 經(jīng)常使用內(nèi)存分析,這個(gè)內(nèi)存分析是否是精確的?性能消耗大不大
  • 為啥顯示的是堆棧?不是說(shuō)分配的對(duì)象嗎?為啥不直接顯示分配的對(duì)象結(jié)構(gòu)名?
  • 4. 內(nèi)存怎么采樣?

    4.1 編譯期間逃逸分析

    說(shuō)明下,golang pprof是分析從堆上分配的內(nèi)存。golang的內(nèi)存在堆上,還是在棧上?這個(gè)不是我們決定的,就算你調(diào)用new這個(gè)關(guān)鍵字,也不一定是在堆上分配。

    逃逸分析是golang的一個(gè)非常重要的一個(gè)點(diǎn)。對(duì)于內(nèi)存分配,垃圾回收的設(shè)計(jì)都有非常重要的影響。

    4.2 采樣的簡(jiǎn)單實(shí)現(xiàn)

    采樣的實(shí)現(xiàn)非常簡(jiǎn)單。簡(jiǎn)單描述流程:

  • 用一個(gè)公共變量用來(lái)記錄
  • 分配內(nèi)存的時(shí)候,加alloc size,加alloc對(duì)象數(shù)
  • 釋放內(nèi)存的時(shí)候,加free size,加free對(duì)象數(shù)
  • 累計(jì)分配:就是alloc 當(dāng)前在用 inuse:就是 alloc-free

    4.3 內(nèi)存采樣的時(shí)機(jī)

    采樣的時(shí)機(jī)說(shuō)3個(gè)點(diǎn):

  • 分配堆上內(nèi)存的時(shí)候,累計(jì)分配
  • 回收器釋放堆上內(nèi)存的時(shí)候,累計(jì)釋放
  • 每512KB打點(diǎn)采樣
  • 但是注意一點(diǎn):并不是每一次分配內(nèi)存都會(huì)被采樣。也就是說(shuō)這里其實(shí)是有個(gè)權(quán)衡的。現(xiàn)在是每滿512KB才會(huì)采樣一次。這里的考慮是性能和采樣效果的權(quán)衡。因?yàn)椴蓸邮且馁M(fèi)性能的,是要取堆棧的。

    怎么理解?舉個(gè)例子

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

    那么有人會(huì)想,這樣豈不是會(huì)漏掉了很多內(nèi)存?統(tǒng)計(jì)還能用來(lái)排查問(wèn)題嗎?

    這個(gè)是性能和效果的一個(gè)考慮,一般來(lái)講,我們是用pprof分析內(nèi)存占用的時(shí)候,在整個(gè)golang程序跑起來(lái)后,時(shí)時(shí)刻刻都在分配釋放內(nèi)存,每累計(jì)分配512KB,打點(diǎn)一次。雖然會(huì)漏掉一些內(nèi)存分配釋放,但是對(duì)每個(gè)結(jié)構(gòu)都是公平的。如果有一個(gè)內(nèi)存泄露分配行為,那么累計(jì)下來(lái)一定會(huì)被抓住的,并且是非常容易被抓住。

    4.4 內(nèi)存采樣的入口

    內(nèi)存采樣的入口,這個(gè)非常簡(jiǎn)單理解。肯定是一個(gè)在分配內(nèi)存的函數(shù)位置,一個(gè)是釋放內(nèi)存的位置。這里要特意提下上下文環(huán)境。因?yàn)間olang是垃圾回收類型的語(yǔ)言,內(nèi)存分配是完全交由golang自己管理,自己不能管理內(nèi)存。

    兩個(gè)入口函數(shù):

  • mProf_Malloc
  • mProf_Free
  • 這兩個(gè)是配套使用的采樣打點(diǎn)函數(shù)。而且一定是配套的。簡(jiǎn)單說(shuō):

    • mProf_Malloc 是由業(yè)務(wù)程序行為(賦值器)觸發(fā)的,分配內(nèi)存嘛。比如你new了一個(gè)對(duì)象,這個(gè)對(duì)象在堆上,那么會(huì)調(diào)用 mallocgc 分配內(nèi)存,如果到了采樣點(diǎn),那么會(huì)調(diào)用 mProf_Malloc 采樣。
    • mProf_Free 是回收器在確定并且將要回收內(nèi)存的時(shí)候調(diào)用的。是垃圾回收過(guò)程的一環(huán)。并且還要注意一點(diǎn),只有打過(guò)點(diǎn)的(mProf_Malloc計(jì)數(shù)過(guò)的對(duì)象,會(huì)有一個(gè)特殊處理),才會(huì)配套使用mProf_Free。
      • 不是說(shuō),任意給一個(gè)內(nèi)存地址給你。你都知道這個(gè)是業(yè)務(wù)類型。

    4.5 內(nèi)存采樣的信息

    這里問(wèn)你的是,golang采樣是采樣啥?類型信息?這里也說(shuō)過(guò)一點(diǎn),內(nèi)存這里和類型系統(tǒng)是沒(méi)啥關(guān)系的。這里采樣的是分配棧,也就是分配路徑。

    4.5.1 flat,cum 分別是怎么來(lái)的?

    看個(gè)例子:

    大家可以先猜下,我們看alloc_space。這個(gè)內(nèi)存會(huì)是怎么累計(jì)到的。實(shí)際統(tǒng)計(jì)如下:

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

    首先說(shuō)幾個(gè)結(jié)論:

  • flat統(tǒng)計(jì)到的,就是這個(gè)函數(shù)實(shí)際分配的。
  • cum是累計(jì)的,包含自己分配的,也包含路過(guò)的。
    • cum和flat不相同的時(shí)候,代表這個(gè)函數(shù)除了自己分配內(nèi)存,自己內(nèi)部調(diào)用的別的函數(shù)也在分配內(nèi)存。

    重點(diǎn)提示:這個(gè)要理解這個(gè),首先要知道,內(nèi)存采樣的是什么,內(nèi)存采樣的是分配棧。

    解釋說(shuō)明

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

  • main函數(shù)里,A函數(shù)調(diào)用了5次,B函數(shù) 5次,C函數(shù)5次。其中B會(huì)調(diào)用A,C會(huì)調(diào)用B。
  • 調(diào)用一次A會(huì)分配10M內(nèi)存,調(diào)用一次B會(huì)分配20M,調(diào)用一次C會(huì)分配30M。總累計(jì)分配內(nèi)存是300M
  • A函數(shù)實(shí)際調(diào)用次數(shù)是 15次;這個(gè)和flat的值是一致的:150M
  • (A) * 5
  • (B -> A) * 5
  • (C -> B -> A) * 5
  • B函數(shù)函數(shù)實(shí)際調(diào)用10次;這個(gè)和flat的值也是一致的:100M
  • B * 5
  • (C -> B) * 5
  • C函數(shù)5次:這個(gè)和flat的值是一致的:50M
  • C * 5
  • main函數(shù)300M,也是一致的。
  • 圖示

    記住一句話:采樣是記錄分配堆棧,而不是類型信息。

    4.6 golang的類型反射

    思考幾個(gè)問(wèn)題:

  • 任意給一個(gè)內(nèi)存地址給你,能知道這個(gè)對(duì)象類型嗎?
  • golang的反射到底是怎么回事?
  • 先說(shuō)結(jié)論:golang里面,內(nèi)存塊是沒(méi)有攜帶對(duì)象類型信息的,這個(gè)跟C是一樣的。但是golang又有反射,golang的反射一定要基于interface使用。這個(gè)要仔細(xì)理解下。

    因?yàn)?#xff0c;golang里面interface的結(jié)構(gòu)變量,是會(huì)記錄type類型的。

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

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

    還是那句話,golang的反射一定是依賴接口類型的,一定是經(jīng)過(guò)接口倒騰過(guò)的。

    因?yàn)楫?dāng)前接口這個(gè)類型對(duì)應(yīng)了兩個(gè)內(nèi)部結(jié)構(gòu):struct iface,struct eface,這兩個(gè)結(jié)構(gòu)都是會(huì)存儲(chǔ)type類型。以后的一切都是基于這個(gè)類型的。

    5. 內(nèi)存分配

    5.1 C語(yǔ)言你分配和釋放內(nèi)存怎么做?

    思考一個(gè)問(wèn)題,在C語(yǔ)言里,我們分配內(nèi)存:

    分配內(nèi)存的時(shí)候,傳入大小,拿到一個(gè)指針。

    ptr = malloc(1024);

    釋放內(nèi)存的時(shí)候,直接傳入ptr,沒(méi)有任何其他參數(shù):

    free (ptr);

    釋放的時(shí)候,怎么確定釋放哪些位置?如果要你自己實(shí)現(xiàn),有很多簡(jiǎn)單的思路,說(shuō)一個(gè)最簡(jiǎn)單的:分配的時(shí)候,不止分配1024字節(jié),還分配了其他的信息,帶head了。

    這種分配方式有什么問(wèn)題:

  • 開(kāi)銷大,在通用的內(nèi)存分配器中,很多場(chǎng)景下,有可能meta信息比自身還要大。
  • 5.2 內(nèi)存分配設(shè)計(jì)考慮的幾個(gè)問(wèn)題

  • 性能
  • 局部性
  • 碎片率
  • 內(nèi)部碎片率
  • 外部碎片率
  • 5.3 golang的內(nèi)存分配

    golang大方向的考慮就是基于局部性和碎片率來(lái)考慮的。使用的是和tcmalloc一致的設(shè)計(jì)。

    5.3.1 整體設(shè)計(jì)

    首先,內(nèi)存塊是不帶類型信息的。像我們?cè)贑語(yǔ)言里面,有時(shí)候?qū)崿F(xiàn)的簡(jiǎn)單的內(nèi)存池,在不考慮一些開(kāi)銷的時(shí)候,會(huì)把業(yè)務(wù)類型放到meta信息里,為的是排查問(wèn)題方便。golang內(nèi)存管理作為一個(gè)通用模塊,不會(huì)這么搞。

    5.3.1.1 地址空間設(shè)計(jì)

    很多時(shí)候,你查golang的資料,會(huì)看到這張圖:

    這張圖有幾個(gè)信息比較重要

  • 為什么spans區(qū)域是512M,bitmap區(qū)是16G,arena是512G?先不要糾結(jié)值,我們先說(shuō)這個(gè)比例關(guān)系:
  • spans區(qū)域,一個(gè)指針大小(8Byte)對(duì)應(yīng)arena的一個(gè)page(8KB),倍數(shù)是1024
  • bitmap區(qū)域,一個(gè)字節(jié)(8bit)對(duì)應(yīng)arena的32Bytes,倍數(shù)是32倍
  • 我們給用戶分配的內(nèi)存就是arena區(qū)域的內(nèi)存,spans區(qū),bitmap區(qū)均為其他用途的元數(shù)據(jù)信息。
  • bitmap這個(gè)實(shí)現(xiàn)我們這次不談,不同通過(guò)這個(gè)你得知道一點(diǎn):并不是所有的內(nèi)存空間都會(huì)掃描一把,是有挑選判斷的。
  • spans區(qū)域是一般用來(lái)根據(jù)一個(gè)內(nèi)存地址查詢mspan結(jié)構(gòu)的。調(diào)用函數(shù):spanOf。
  • bitmap是用來(lái)輔助垃圾回收用的區(qū)域。有這個(gè)bitmap信息可以提高回收效率和精度。注意一點(diǎn),這個(gè)不是標(biāo)識(shí)object是否分配的位圖,標(biāo)識(shí)是否分配object的問(wèn)題是mspan.allocBits結(jié)構(gòu)。這個(gè)可以理解為提高垃圾回收效率的實(shí)現(xiàn)。
  • 注意幾個(gè)點(diǎn):

  • 很多文章都提到golang內(nèi)存512GB這個(gè)事情。512GB說(shuō)的是內(nèi)存虛擬地址空間的限制,是最大能力,是最大的規(guī)劃利用。golang之前最大可以使用的內(nèi)存地址空間。
  • golang1.11 之后已經(jīng)沒(méi)有512GB的限制了。基本上和系統(tǒng)的虛擬地址空間一致
  • 這個(gè)比例還是一樣的,1:1024,1:32
  • 就算golang1.11之前,也不是說(shuō)golang的程序上來(lái)就向系統(tǒng)申請(qǐng)這么大塊虛擬地址。也是每64M的申請(qǐng),管理對(duì)象單元是heapArea結(jié)構(gòu)。
  • 三個(gè)區(qū)域看著連續(xù)結(jié)在一起,但是其實(shí)不是連續(xù)的地址。
  • 實(shí)際的實(shí)現(xiàn)中都是以64M(heapArena)的小單位進(jìn)行的。
  • 5.3.2 抽象對(duì)象概念

    物理偏向概念

  • heapArena:堆上物理空間管理的一個(gè)小單元,64M一個(gè)。
  • page:物理內(nèi)存最小單位,8KB一個(gè)。
  • 邏輯偏向概念

  • span:span為內(nèi)存分配的一個(gè)管理單元。span內(nèi)按照固定大小size劃分,相同的size劃分為同一類。一個(gè)span管理一個(gè)連續(xù)的page。
  • object:內(nèi)存分配的最小單元。
  • 管理結(jié)構(gòu)層次概念

    mcache:每個(gè)M上的,管理內(nèi)存用的。我們都知道GMP架構(gòu),每個(gè)M都有自己的內(nèi)存cache管理,這樣是為了局部性。只是一個(gè)cache管理。mcentral:mheap結(jié)構(gòu)所有,也只是一個(gè)cache管理,但是是為所有人服務(wù)的。mheap:是真正負(fù)責(zé)分配和釋放物理內(nèi)存的。

    5.3.3 局部性的設(shè)計(jì)

    這個(gè)思路很簡(jiǎn)單,就是設(shè)計(jì)成局部性的一個(gè)層次設(shè)計(jì)。

    5.3.3.1 mcache

    mcache由于只歸屬自己的M,span一旦在這個(gè)結(jié)構(gòu)管理下,其他人是不可見(jiàn),不會(huì)去操作的。只有這個(gè)m會(huì)操作。所以自然就不需要加鎖。

    5.3.3.2 mcentral

    mcentral是所有人可見(jiàn)的。所以操作自然要互斥,這個(gè)的作用也是一個(gè)cache的統(tǒng)一管理。

    5.3.3.3 mheap

    這個(gè)是負(fù)責(zé)真實(shí)內(nèi)存分配和釋放的的一個(gè)結(jié)構(gòu)。

    5.3.4 針對(duì)碎片率的設(shè)計(jì)

    golang的內(nèi)存設(shè)計(jì)目標(biāo):碎片率平均12.5%左右。

    說(shuō)明:

  • tail wast實(shí)際是浪費(fèi)的外部碎片
  • 比如說(shuō),第一種size,8字節(jié)。一個(gè)page 8KB,8字節(jié)剛好對(duì)齊。外部碎片為0.
  • max waste說(shuō)的是最大的內(nèi)部碎片率
  • 怎么算的?每一個(gè)放進(jìn)該span的對(duì)象大小都是最小值的情況
  • 比如說(shuō),第一種size,8字節(jié)。最小的對(duì)象是1字節(jié),浪費(fèi)7字節(jié),最大碎片率為 1-1/8 = 87.5%
  • 怎么的出來(lái)的這些值?經(jīng)驗(yàn)值吧,可能。

    6. 內(nèi)存回收

    6.1 golang協(xié)程搶占執(zhí)行

    首先,golang沒(méi)有真正的搶占。golang調(diào)度單位為協(xié)程,所謂搶占,也就是強(qiáng)行剝奪執(zhí)行權(quán)。但是有一點(diǎn),golang本質(zhì)上是非搶占的,不像操作系統(tǒng)那樣,有時(shí)鐘中斷和時(shí)間片的概念。golang雖然里面是有一個(gè)搶占的概念,但是注意了,這個(gè)搶占是建議性質(zhì)的搶占,也就是說(shuō),如果有協(xié)程不聽(tīng)話,那是沒(méi)有辦法的,實(shí)現(xiàn)搶占的效果是要對(duì)方協(xié)程自己配合的。

    一句話:系統(tǒng)想讓某個(gè)goroutine自己放棄執(zhí)行權(quán),會(huì)給這個(gè)協(xié)程設(shè)置一個(gè)魔數(shù),協(xié)程在切調(diào)度,或者其他時(shí)機(jī)檢查到了的時(shí)候,會(huì)感知到這一個(gè)行為。

    當(dāng)前的搶占實(shí)現(xiàn)是:

  • 給這個(gè)協(xié)程設(shè)置一個(gè)的魔數(shù)(stackguard)。每個(gè)函數(shù)的入口會(huì)比較當(dāng)前棧寄存器值和stackguard值來(lái)決定是否觸發(fā)morestack函數(shù)。(這是一個(gè)搶占調(diào)度點(diǎn))
  • 協(xié)程調(diào)用函數(shù)的時(shí)候,會(huì)檢查是否需要棧擴(kuò)容。如果被設(shè)置了搶占標(biāo)示,那么就會(huì)首先調(diào)用到
  • 調(diào)用newstack,在newstack里面判斷是否是特殊值,這種特殊值,目的不在于擴(kuò)容,而在于讓出調(diào)度。
  • 所以,在golang里面,只要有函數(shù)調(diào)用,就會(huì)有感知搶占的時(shí)機(jī)。stw就是基于這個(gè)實(shí)現(xiàn)的。

    思考一個(gè)問(wèn)題:

    如果有一個(gè)猥瑣的函數(shù):非常耗時(shí),一直在做cpu操作,并且完全沒(méi)有函數(shù)調(diào)用。這種情況下,golang是沒(méi)有一點(diǎn)辦法的。那么這種情況會(huì)影響到整個(gè)程序的能力。

    所以,我們平時(shí)寫函數(shù),一定要短小精悍,功能拆分合理。

    6.2 STW是怎么回事?

    STW:stop the world,也就是說(shuō)暫停說(shuō)由協(xié)程的調(diào)度和執(zhí)行。stw是怎么實(shí)現(xiàn)?stw的基礎(chǔ)就是上面提到的搶占實(shí)現(xiàn)。stw調(diào)用的目的是為了讓整個(gè)程序(賦值器停止),那么就需要?jiǎng)儕Z每一個(gè)協(xié)程的執(zhí)行。

    stw在垃圾回收的幾個(gè)關(guān)鍵操作里是需要的,比如開(kāi)啟垃圾回收,需要stw,做好準(zhǔn)備工作。如果stw的時(shí)候,出現(xiàn)了猥瑣的函數(shù),那么會(huì)導(dǎo)致整個(gè)系統(tǒng)的能力降低。因?yàn)榇蠹叶荚诘饶阋粋€(gè)人。

    6.3 垃圾回收要求

  • 正確性:絕對(duì)不能回收正在使用的的內(nèi)存對(duì)象。
  • 存活性:一輪回收過(guò)程一定是有邊界,可結(jié)束的。
  • 6.4 golang版本迭代歷史

  • go 1.3 以前,使用是標(biāo)記-清掃的方式,整個(gè)過(guò)程需要stw
  • go 1.3 版本分離了標(biāo)記和清掃操作,標(biāo)記過(guò)程stw,清掃過(guò)程并發(fā)執(zhí)行
  • go 1.5 版本在標(biāo)記過(guò)程中,使用三色標(biāo)記法。回收過(guò)程分為四個(gè)階段,其中,標(biāo)記和清掃都并發(fā)執(zhí)行的,但標(biāo)記階段的前后需要stw一定時(shí)間來(lái)做gc的準(zhǔn)備工作和棧的re-scan。
  • go 1.8 版本引入了混合寫屏障機(jī)制,避免了對(duì)棧的re-scan,極大的減少了stw的時(shí)間。
  • 6.5 GC觸發(fā)條件

    • gcTriggerHeap 當(dāng)分配的內(nèi)存達(dá)到一定值就觸發(fā)GC
    • gcTriggerTime 當(dāng)一定時(shí)間沒(méi)有執(zhí)行過(guò)GC就觸發(fā)
    • gcTriggerCycle 要求啟動(dòng)新一輪的GC,一啟動(dòng)則跳過(guò),手動(dòng)觸發(fā)GC的runtime.GC( )會(huì)使用這個(gè)條件

    6.6 三色定義

    6.6.1 強(qiáng)三色

    黑色對(duì)象不允許指向白色對(duì)象。

    6.6.2 弱三色

    黑色對(duì)象可以指向白色對(duì)象,但是前提是,該白色對(duì)象一定是處于灰色保護(hù)鏈中。

    6.7 GC流程

    這里不詳細(xì)闡述了。貼一張go1.8之前的圖:

    當(dāng)下GC大概分為四個(gè)階段:

  • GC準(zhǔn)備階段
  • 標(biāo)記階段
  • 標(biāo)記結(jié)束階段
  • 清理階段
  • 6.8 寫屏障

    如果標(biāo)記和回收不用和應(yīng)用程序并發(fā),在標(biāo)記和回收整個(gè)過(guò)程直接stw,那么就簡(jiǎn)單了。golang為了提供低時(shí)延,就必須讓賦值器和回收器并發(fā)起來(lái)。但是在并發(fā)的過(guò)程中,賦值器和回收器對(duì)于引用樹(shù)的理解就會(huì)出現(xiàn)不一致,這里就一定要配合寫屏障技術(shù)。

    寫屏障技術(shù),是動(dòng)態(tài)捕捉寫操作,維持回收正確性的技術(shù)。寫屏障就是一段 hook 代碼,編譯期間生成,運(yùn)行期間跟進(jìn)情況會(huì)調(diào)用到 hook 的代碼段,也就是寫屏障的代碼;

    下面系統(tǒng)整體的討論下寫屏障的技術(shù)。

    6.8.1 插入寫屏障

    (Dijkstra '78)

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

    這個(gè)是另外一個(gè)通用的屏障技術(shù)。這個(gè)維護(hù)的是強(qiáng)三色不變式來(lái)保證正確性,保證黑色對(duì)象一定不能指向白色對(duì)象。golang使用的是這個(gè)屏障,插入屏障。按照道理,是幾乎完全不需要stw的。但是golang有一個(gè)處理,由于棧上面使用屏障會(huì)導(dǎo)致處理非常復(fù)雜,并且開(kāi)銷會(huì)非常大。所以當(dāng)前golang只針對(duì)堆上的寫操作做了屏障。

    那么就會(huì)帶來(lái)一個(gè)問(wèn)題:所以當(dāng)一輪掃描完了之后,在標(biāo)記結(jié)束的階段,還需要重新掃描一遍goroutine棧,并且棧引用到的所有對(duì)象也要掃描。因?yàn)間oroutine有可能直接指向了白色對(duì)象。在掃描goroutine棧過(guò)程中,需要stw。這個(gè)也是go1.8以前的一個(gè)非常大的延遲來(lái)源。

    (開(kāi)始的時(shí)候,stw掃描棧,得到灰色對(duì)象)

    圖表演示

    堆上路徑賦值:

    step1:堆上對(duì)象賦值的時(shí)候,插入寫屏障,保護(hù)強(qiáng)三色不變式

    step2:刪除的時(shí)候,沒(méi)啥問(wèn)題

    棧上對(duì)象賦值:

    step3:棧上對(duì)象賦值的時(shí)候,沒(méi)有寫屏障。白色對(duì)象直接被黑色對(duì)象引用。

    step4:刪除灰色保護(hù)路徑。

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

    6.8.2 刪除寫屏障

    (Yuasa '90)

    writePointer ( slot, ptr ): // 刪除之前,保護(hù)原先白色或者灰色指向的數(shù)據(jù)塊 if ( isGery ( slot ) || isWhite ( slot ) ) shade ( *slot ) *slot = ptr

    這個(gè)是通用的一種寫屏障技術(shù)。golang并沒(méi)有實(shí)現(xiàn),而是實(shí)現(xiàn)了插入寫屏障。原因就在于:這個(gè)在垃圾回收之前,必須做一個(gè)快照掃描,這個(gè)就會(huì)對(duì)用戶時(shí)延有比較嚴(yán)重的影響。下面詳述。

    主要流程:

  • 在標(biāo)記之前,需要打一個(gè)引用關(guān)系的快照。所以,這個(gè)對(duì)于棧內(nèi)存很大的時(shí)候,影響越大。
  • 不需要完整的快照,只需要在掃描堆對(duì)象之前,確保所有的棧對(duì)象是黑色的。引用都是灰色的,這樣就保證了一個(gè)前提:所有可達(dá)的對(duì)象都處于灰色保護(hù)狀態(tài)中。
  • 對(duì)棧快照掃描需要stw,去掃描棧對(duì)象。這個(gè)時(shí)候,是需要暫停所有的用戶程序。
  • 掃描堆對(duì)象的時(shí)候,可以和應(yīng)用程序并發(fā)的。此后根一直保持黑色(黑色賦值器),不用再掃描棧。
  • 對(duì)象被刪除的時(shí)候,刪除寫屏障會(huì)捕捉到。置灰。
  • 上面的偽代碼顯示有條件,其實(shí)第一版的時(shí)候是沒(méi)有條件的。
  • 這里加上條件是為了回收精度:當(dāng)上游之前是白色或者灰色才需要把這個(gè)置灰色。如果是黑?那么一定是處于灰色保護(hù)狀態(tài),因?yàn)檫@個(gè)是前提(理解這個(gè)非常重要)。
  • (開(kāi)始的時(shí)候,stw掃描棧,得到灰色對(duì)象)

    圖表演示

    初始掃描快照后:

    step1: 賦值。這里賦值是允許的,雖然是破壞了強(qiáng)三色不變式。但是還是符合弱三色不變式。

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

    回收精度:

    刪除寫屏障的精度比插入寫屏障的精度更低。刪除的即使是最后一個(gè)指針,也會(huì)保留到下一輪,屬于一個(gè)浮動(dòng)垃圾。這個(gè)比插入屏障精度還低。因?yàn)?#xff0c;對(duì)于插入屏障所保留的對(duì)象,回收器至少可以確定曾在其中執(zhí)行了某些回收相關(guān)的操作(獲取或?qū)懭雽?duì)象的引用),但刪除屏障所保留的對(duì)象卻不一定被賦值器操作過(guò)。

    為什么需要打快照?

    刪除寫屏障,又叫快照屏障增量技術(shù)(或者說(shuō),一定要配合這個(gè)來(lái)做)。

  • 首先,是需要stw,針對(duì)掃描整個(gè)棧根打做一遍掃描。相當(dāng)于一個(gè)快照。這個(gè)過(guò)程掃描之后,就能保證當(dāng)前(時(shí)刻)所有可達(dá)的對(duì)象都處于灰色保護(hù)狀態(tài),滿足弱三色不變式。
  • 然后,賦值器和回收器就可以并發(fā)。但是并發(fā)有可能會(huì)破壞導(dǎo)致弱三色不變式。這個(gè)時(shí)候,就需要?jiǎng)h除寫屏障來(lái)時(shí)刻保護(hù)白色對(duì)象。
  • golang為啥沒(méi)有用這個(gè)?

  • 一個(gè)是精度問(wèn)題,這個(gè)精度要比插入寫屏障低;
  • 考慮goroutine可能非常多,不適合上來(lái)就stw,掃描所有的內(nèi)存棧。這個(gè)適合小內(nèi)存的場(chǎng)景。
  • 思考一個(gè)問(wèn)題:這個(gè)和混合寫屏障有沒(méi)有區(qū)別?還是有區(qū)別的,這里是要鎖整個(gè)棧,混合寫屏障是并發(fā)的,每次只需要鎖單個(gè)棧。
  • 6.8.3 混合寫屏障

    混合屏障是結(jié)合插入屏障和刪除屏障。

    偽代碼:

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

    (開(kāi)始的時(shí)候,stw掃描棧,得到黑色對(duì)象)

    golang實(shí)際情況:

    偽代碼如上。但是這里提出來(lái)一點(diǎn),golang根本不是和偽代碼說(shuō)的這樣。沒(méi)有做條件判斷,所以現(xiàn)在的回收精度很低。這個(gè)算是一個(gè)TodoList。

    注意:使用了混合屏障,還是針對(duì)堆上的,棧上對(duì)象寫入還是沒(méi)有barrier。golang之前只使用插入屏障,關(guān)鍵在于棧對(duì)象沒(méi)有,導(dǎo)致棧上黑對(duì)象可能指向白對(duì)象。所以要rescan。因?yàn)槿绻籸escan,而且又破壞了弱三色不變式(沒(méi)有處于灰色保護(hù)鏈中),那么就丟數(shù)據(jù)了。

    混合屏障,就是結(jié)合刪除屏障,保護(hù)這一個(gè)前提,代價(jià)就是進(jìn)一步降低回收精度。

    圖表示例:

    混合屏障就是要解決:棧指向白色對(duì)象,stw重新掃描棧的問(wèn)題。

    step1:賦值白對(duì)象到黑對(duì)象引用,這個(gè)不會(huì)阻止這個(gè),也不會(huì)有寫屏障。就是一個(gè)正常的賦值。

  • 這個(gè)時(shí)候黑色指向了白色對(duì)象。破壞了強(qiáng)三色不變式。
  • 但是這個(gè)白色對(duì)象還處于灰色狀態(tài)保護(hù)下。符合弱三色不變式。
  • step2:刪除指針的時(shí)候,意圖破壞弱三色不變式的時(shí)候,寫屏障就會(huì)把這個(gè)對(duì)象置灰色。

    問(wèn)題一:如果有個(gè)還會(huì)想?由于棧上沒(méi)有寫屏障,這個(gè)刪除的對(duì)象式根指向的呢?如果存在以下場(chǎng)景?

    step1:堆上的白色對(duì)象引用賦值給黑色棧對(duì)象。

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

    這個(gè)怎么辦呢?

    答案是:其實(shí)根本就不可能出現(xiàn)這個(gè)場(chǎng)景的引用圖。第一個(gè)圖就不會(huì)出現(xiàn)。因?yàn)殡m然沒(méi)有stw,但是掃描某個(gè)g的時(shí)候,這個(gè)g是暫停的。相當(dāng)于這個(gè)g棧是一個(gè)快照狀態(tài)。

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

    那么這個(gè)暫停g這個(gè)是怎么做到的?

  • 掃描的時(shí)候,會(huì)設(shè)置一個(gè) _Gscan 狀態(tài)。
  • casgstatus的時(shí)候,保證循環(huán)等待這個(gè)狀態(tài)完成。之前是直接吃cpu的,后面做了一個(gè)優(yōu)化,加了一個(gè)yield,5us的間隔。
  • 關(guān)于這段代碼的改動(dòng)
  • 問(wèn)題二:如果是多個(gè)棧呢,那么就不是原子的快照了。比如下圖?那么就可能導(dǎo)致這種情況。

    如果說(shuō)A和前面的黑色對(duì)象不屬于同一個(gè)g棧。那么是否可能會(huì)導(dǎo)致這種場(chǎng)景出現(xiàn)?分析下:

  • 這個(gè)場(chǎng)景是有這么一個(gè)白色對(duì)象,先只被G2棧根引用到。
  • 當(dāng)前G1已經(jīng)被掃描完,G2還沒(méi)有掃描。
  • 把這個(gè)白色對(duì)象賦值給G1棧的黑色對(duì)象。
  • 這個(gè)時(shí)候把G2對(duì)白色對(duì)象的引用刪掉,這樣豈不是會(huì)出現(xiàn)黑色白色對(duì)象,且為唯一指針?
  • 答案是:這里的關(guān)鍵在于第三步。G1的棧對(duì)象接受賦值,這個(gè)并不是憑空來(lái)的。那么一定是G1自己找來(lái)的,可達(dá)的對(duì)象。這個(gè)是一個(gè)前提。所以,如果能接受這樣的賦值,那么這個(gè)白色對(duì)象一定是處于G1棧的灰色保護(hù)下,因?yàn)镚1一定是可訪問(wèn)這個(gè)對(duì)象的。否則,根本就不能完成這個(gè)賦值。

    混合寫屏障的場(chǎng)景,白色對(duì)象處于灰色保護(hù)下,但是只由堆上的灰色對(duì)象保護(hù)。注意理解這點(diǎn);

    屏障生成示例:

  • 寫堆上內(nèi)容,才會(huì)在編譯期間生成寫屏障
  • 棧上的寫,不會(huì)有寫屏障。
  • runtime.gcWriteBarrier :

  • 計(jì)算出wbBuf的next位置
  • record ptr
  • ptr指針?lè)诺絯bBuf隊(duì)列中。
  • 把 *(slot) 存到wbBuf隊(duì)列中 ( 置灰色,flush了就是灰色 )
  • shade( *slot )
  • 如果隊(duì)列沒(méi)有滿
  • 那么就賦值寫(*(slot) = ptr); 則返回
  • 如果隊(duì)列滿了,那么跳到flush
  • wbBufFlush就是把wbBufFlush里的元屬flush到灰色隊(duì)列中。
  • 調(diào)用完了 runtime.wbBufFlush 處理之后,返回賦值ret(*(slot) = ptr)
  • 這么看起來(lái),就不存在 判斷stack是否為灰色的條件?

    6.8.4 其他屏障

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

    優(yōu)點(diǎn):

  • 這種無(wú)條件的屏障更加容易理解,直接把目標(biāo)和源都置灰色保護(hù)
  • heap上沒(méi)有黑色到白色的指針
  • 唯一有可能出現(xiàn)黑色到白色的引用 只可能出現(xiàn)在 被掃描了的stack
  • 一旦 stack 被掃描過(guò)了,只有一種辦法能得到白色對(duì)象指針(white pointer):通過(guò)transfer一個(gè)可達(dá)(reachable)對(duì)象
  • 刪除屏障和混合寫屏障,保護(hù)了shade(*slot)這個(gè)指針,就保護(hù)了一條路徑:這個(gè)來(lái)路一定是灰色的,下游的白色都會(huì)收到保護(hù)。并且,我們知道,棧上得到的白色指針一定是可達(dá)的,那么一定是有堆上灰色對(duì)象保護(hù)的。
  • 任何一個(gè)白色對(duì)象(被黑色棧對(duì)象指向的)一定是被堆上灰色對(duì)象保護(hù)可達(dá)的。
  • 缺點(diǎn):

    這種屏障會(huì)導(dǎo)致比較多的屏障,兩倍。所以針對(duì)這個(gè)考慮權(quán)衡,會(huì)加一個(gè)stack條件判斷,就是我們看到的混合屏障的樣子。

    6.9 內(nèi)存可見(jiàn)性

    提一下golang的內(nèi)存可見(jiàn)性。在c里面,如果是在多線程環(huán)境,并發(fā)操作一些變量,需要考慮一些可見(jiàn)性的問(wèn)題。比如賦值一個(gè)變量,這個(gè)線程還有可能在寄存器里沒(méi)有刷下去,或者編譯器幫你優(yōu)化到寄存器中,不去內(nèi)存讀。所以有一個(gè)volatile關(guān)鍵字,強(qiáng)制去內(nèi)存讀。

    golang是否有這個(gè)內(nèi)存可見(jiàn)性的問(wèn)題?

    一句話,golang里面,只要你保證順序性,那么內(nèi)存一致性就沒(méi)有問(wèn)題。具體可以搜索happen-before的機(jī)制。

    6.10 注意問(wèn)題

    6.10.1 千萬(wàn)不要嘗試?yán)@過(guò)golang的類型系統(tǒng)

    千萬(wàn)不要嘗試?yán)@過(guò)golang的類型系統(tǒng)。golang官方在提到uintptr類型的時(shí)候,都說(shuō)不要產(chǎn)生uintptr的臨時(shí)變量,因?yàn)楹苡锌赡軙?huì)導(dǎo)致gc的錯(cuò)誤回收(這個(gè)做過(guò)一個(gè)簡(jiǎn)單的驗(yàn)證,發(fā)現(xiàn)新版本的uintptr類型是會(huì)作為指針標(biāo)記的)。

    舉一個(gè)極端的例子,如果你new了一個(gè)對(duì)象,然后把這個(gè)對(duì)象的地址保存在8個(gè)不連續(xù)的byte類型里,那就等著coredump吧。

    6.10.2 在golang里按照c的思路實(shí)現(xiàn)一個(gè)內(nèi)存池很容易踩到巨坑。

    比如現(xiàn)在你分配一個(gè)大內(nèi)存出來(lái)(1G的[ ]byte類型空間)。這是一個(gè)大內(nèi)存塊。并且golang沒(méi)有任何標(biāo)識(shí)這個(gè)地方標(biāo)識(shí)指針。

    // 分配一個(gè)大內(nèi)存數(shù)組(1GB),數(shù)組元素是byte。那么自然每個(gè)元素都是不含指針的。begin := make([]byte, 1024*1024*1024)

    那么掃描是不會(huì)掃描這個(gè)內(nèi)部的。

    內(nèi)存池分配器接口:func (ac *Allocator) Alloc (size int) unsafe.Pointer

    用來(lái)分配對(duì)象,使用可能會(huì)導(dǎo)致莫名其妙的內(nèi)存錯(cuò)誤。假設(shè)用來(lái)分配對(duì)象T:

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

    T對(duì)象是從一個(gè)大數(shù)組里劃出來(lái)的,垃圾回收其實(shí)并不知道T這個(gè)對(duì)象。不過(guò)只要1G內(nèi)存池本身不被回收,T對(duì)象還是安全的。但是T里面的S,是golang走類型系統(tǒng)分配出來(lái)的,就會(huì)有問(wèn)題。

    假設(shè)發(fā)生垃圾回收了,GC會(huì)認(rèn)為這個(gè)內(nèi)存空間是一個(gè)Byte數(shù)組,而不會(huì)掃描,那么t.s指向的對(duì)象認(rèn)為未被任何對(duì)象引用到,它會(huì)被清理掉。最后t.s就成了一個(gè)懸掛指針。

    golang里面實(shí)現(xiàn)內(nèi)存分配器,適用處理兩種情況:

  • 一種是用于分配對(duì)象里面不包含其他引用
  • 另一種,包含的引用對(duì)象也在這個(gè)分配器里
  • 其實(shí),沒(méi)必要自己搞通用內(nèi)存池。一旦繞過(guò)了golang的類型系統(tǒng),就會(huì)出現(xiàn)坑。


    推薦閱讀

    • Go GC 怎么標(biāo)記內(nèi)存?顏色是什么含義?圖解三色標(biāo)記法

    學(xué)習(xí)交流 Go 語(yǔ)言,掃碼回復(fù)「進(jìn)群」即可

    站長(zhǎng) polarisxu

    自己的原創(chuàng)文章

    不限于 Go 技術(shù)

    職場(chǎng)和創(chuàng)業(yè)經(jīng)驗(yàn)

    Go語(yǔ)言中文網(wǎng)

    每天為你

    分享 Go 知識(shí)

    Go愛(ài)好者值得關(guān)注

    總結(jié)

    以上是生活随笔為你收集整理的ntfs分配单元大小_万字长文图解 Go 内存管理分析:工具、分配和回收原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。