<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深度剖析 Golang 的 GC 掃描對象的實現(xiàn)

          共 9554字,需瀏覽 20分鐘

           ·

          2020-08-05 00:44


          ??大綱??



          • 掃描的目的

          • 掃描的實現(xiàn)

            • 編譯階段

            • 運行期內(nèi)存分配

            • 運行掃描階段

          • 總結(jié)

          之前闡述了 golang 垃圾回收通過保證三色不變式來保證回收的正確性,通過寫屏障來實現(xiàn)業(yè)務(wù)賦值器和 gc 回收器正確的并發(fā)的邏輯。其中高概率的提到了“掃描隊列”和“掃描對象”。隊列這個邏輯非常容易理解,那么”掃描對象“ 這個你理解了嗎?有直觀的感受嗎?這篇文章就是要把這個掃描的過程深入剖析下。

          1. 掃描的東西是啥?形象化描述下
          2. 怎么去做的掃描?形象化描述下

          我們就是要把這兩個抽象的概念搞懂,不能停留在語言級別淺層面,要知其然知其所以然。

          ??掃描的目的??



          掃描到底是為了什么?

          之前的文章我們深入剖析了垃圾回收的理論和實現(xiàn),可以總結(jié)這么節(jié)點:

          1. 垃圾回收的根本目的是:“回收那些業(yè)務(wù)永遠都不會再使用的內(nèi)存塊”;
          2. 掃描的目的則是:“把這些不再使用的內(nèi)存塊找出來”;

          我們通過地毯式的掃描,從一些 root 起點開始,不斷推進搜索,最終形成了一張有向可達的網(wǎng),那些不在網(wǎng)里的就是沒有被引用到的,也就是可回收的內(nèi)存。


          ??掃描的實現(xiàn)??



          掃描對象代碼邏輯其實不簡單,但主體線索很清晰,可以分為三部分:

          1. 編譯階段:編譯期是非常重要的一環(huán),針對靜態(tài)類型做好標記準備(旁白:原則上編譯期能做的絕對不留到運行期);
          2. 運行階段:賦值器分配內(nèi)存的時候,根據(jù)編譯階段的 type 標示,會為分配的對象內(nèi)存設(shè)置好一個對應(yīng)的指針標示的 bitmap;
          3. 掃描階段:根據(jù)指針的 bitmap 標示,地毯式掃描

          編譯階段


          結(jié)構(gòu)體對齊

          要理解編譯階段做的事情,那么首先要理解結(jié)構(gòu)體對齊的基礎(chǔ)知識。這個和 C 語言類似,golang 的結(jié)構(gòu)體是有對齊規(guī)則的,也就是說,必要的時候可能會填充一些內(nèi)存空間來滿足對齊的要求??偨Y(jié)來說兩條規(guī)則:

          1. 長度要對齊
          2. 地址要對齊
          “長度要對齊”怎么理解?

          結(jié)構(gòu)體的長度要至少是內(nèi)部最長的基礎(chǔ)字段的整數(shù)倍。

          舉例:

          type TestStruct struct {  ptr uintptr     // 8   f1  uint32      // 4  f2  uint8       // 1}

          這個結(jié)構(gòu)體內(nèi)存占用 size 多大?

          答案是:16個字節(jié),因為字段 ptr 是 uintptr 類型,占 8 字節(jié),是內(nèi)部字段最大的,TestStruct 整體長度要和 8 字節(jié)對齊。那么就是 16 字節(jié)了,而不是有些人想的 13 字節(jié)(8+4+1)。

          dlv 調(diào)試如下:

          (dlv) p typ*runtime._type {	size: 16,    ...

          字節(jié)示意圖:

          |--8 Byte--|--4 Byte--|--4 Byte--|
          “地址要對齊”怎么理解?

          字段的地址偏移要是自身長度的整數(shù)倍。

          舉例:

          type TestStruct struct {  ptr uintptr   // 8  f1  uint8     // 1   f2  uint32    // 4}

          假設(shè) new 一個 TestStruct 結(jié)構(gòu)體 a 的地址是 0xc00008a010 ,那么 &a.ptr 是 0xc00008a010 (= a + 0),&a.f1 是 0xc00008a018 (= a + 8) ,&a.f2 是 0xc00008a01c (= a + 8 + 4) 。

          dlv 調(diào)試如下:

          (dlv) p &a.ptr(*uintptr)(0xc00008a010)(dlv) p &a.f1(*uint8)(0xc00008a018)(dlv) p &a.f2(*uint32)(0xc00008a01c)

          假設(shè) TestStruct 分配對象 a 的地址是 0xc00008a010 ,解釋如下:

          • ptr 是第一個字段,當然和結(jié)構(gòu)體本身地址一樣,相對偏移是 0,所以地址是 0xc00008a010 == 0xc00008a010 + 0 ;
          • f1 是第二個字段,由于前一個字段 ptr 是 uintptr 類型(8字節(jié)),并且由于 f1 本身是 uint8 類型(1字節(jié)),所以 f1 從 8 偏移開始沒毛病,所以 f1 的偏移地址從 0xc00008a018 == 0xc00008a010 + 8;
          • f2 是第三個字段,由于前一個字段 f1 是 uint8(1字節(jié)),所以表面上看好像 f2 要接著 0xc00008a019 (= 0xc00008a018 +1) 這個地址才對,但是 f2 本身是 uint32 (4字節(jié)的類型),所以 f2 地址偏移至少要是 4 的倍數(shù),所以 f2 的地址要從 0xc00008a01c (0xc00008a018 + 4)這個地址開始才對。也就是說,f1 到 f2 之間填充了一些不用的空間,為了地址對齊。

          所以這樣算下來,整個 TestStruct 的占用空間長度是 16字節(jié) (8+1+3+4)。

          指針位標記

          golang 的所有類型都對應(yīng)一個 _type 結(jié)構(gòu),可以在 runtime/type.go 里面找到,定義如下:

          type _type struct {  size       uintptr  ptrdata    uintptr // size of memory prefix holding all pointers  hash       uint32  tflag      tflag  align      uint8  fieldalign uint8  kind       uint8  alg        *typeAlg  // gcdata stores the GC type data for the garbage collector.  // If the KindGCProg bit is set in kind, gcdata is a GC program.  // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.  gcdata    *byte  str       nameOff  ptrToThis typeOff}

          比如我們定義了一個 Struct 如下:

          type TestStruct struct {  ptr uintptr   // 8  f1  uint8     // 1  f2  *uint8    // 8  f3  uint32    // 4  f4  *uint64   // 8??f5??uint64????//?8}

          該結(jié)構(gòu) dlv 調(diào)試如下:

          (dlv) p typ*runtime._type {	size: 48,	ptrdata: 40,	hash: 4075663022,	tflag: tflagUncommon|tflagExtraStar|tflagNamed (7),	align: 8,	fieldalign: 8,	kind: 25,	alg: *runtime.typeAlg {hash: type..hash.main.TestStruct, equal: type..eq.main.TestStruct},	gcdata: *20,	str: 28887,	ptrToThis: 49504,}

          在編譯期間,編譯器就會在內(nèi)部生成一個 _type 結(jié)構(gòu)體與之對應(yīng)。_type 里面重點解釋幾個和本次掃描主題相關(guān)的字段:

          1. size:類型長度,我們上面這個類型長度應(yīng)該是 32 字節(jié);
            1. 這里理解要應(yīng)用上上面講的結(jié)構(gòu)體字節(jié)對齊的知識,這里就不再復(fù)述;
          2. ptrdata:指針截止的長度位置,我們 f4 是指針,所以包含指針的字段最多也就到 40 字節(jié)的位置,ptrdata==40;
            1. 要理解字節(jié)對齊哈;
          3. kind:表明類型,我們是自定義struct類型,所以 kind == 25
            1. kind 枚舉定義在 runtime/typekind.go 文件里;
          4. gcdata:這個就重要了,這個就是指針的 bitmap,因為編譯器他在編譯分析的時候,肯定就知道了所有的類型結(jié)構(gòu),那么自然知道所有的指針位置。gcdata 是 *byte 類型(byte 數(shù)組),當前值是 20,20 轉(zhuǎn)換成二進制數(shù)據(jù)就是 00010100 ,這個眼熟不?這個你要從右往左看就是 00101000(從低 bit 往高 bit 看),這個不就是剛好是 TestStruct 的指針 bitmap 嘛,每個 bit 表示一個指針大?。? 字節(jié))的內(nèi)存,00101000 ?第 3 個 bit 和第 5 個 bit 是 1,表示 第 3 個字段(第 3 個 8 字節(jié)的位置)和第 5 個字段(第 5 個 8 字節(jié)的位置)是存儲的是指針類型,這里剛好就和 TestStruct.f2 ?和 TestStruct.f4 ?對應(yīng)起來。

          劃重點:這里重點回顧一下 uintptr 類型的問題,這里注意到,第一個字段 ptr(uintptr 類型)在指針的 bitmap 上是沒有標記成指針類型的,這里一定要注意了,uintptr 是數(shù)值類型,非指針類型,用這個存儲指針是無法保護對象的(掃描的時候 uintptr 指向的對象不會被掃描),這里就是實錘了。

          小結(jié)

          編譯階段給每個類型生成 _type 類型,內(nèi)部對類型字段生成指針的 bitmap,這個是后面掃描行為的基礎(chǔ)依據(jù)。

          思考題:是否可以不用 bitmap,其實有個最簡單最笨拙的掃描方式,我們可以不搞這個指針的 bitmap,我上來就直接掃描,每 8 字節(jié)的讀取內(nèi)存,然后去看這個內(nèi)存塊存儲的值是否指向了一個對象?如果是我就保護起來。

          這個實現(xiàn)理論上可以滿足,但是有兩個不能接受的缺陷:

          1. 精度太低,你編譯期間不做準備,那運行期間就要來償還這部分損耗,你無法判斷是不是指針,所以只要指向了一個有效內(nèi)存地址,就得無腦保護,這樣就保護了很多不需要保護的內(nèi)存塊;
          2. 掃描太低效,必須全地址掃描,因為你沒有 bitmap,無法識別是否有指針。也無法做優(yōu)化,比如我們程序里面可能 一半以上的類型內(nèi)是不包含指針的,這種根本就不需要掃描;

          運行期內(nèi)存分配



          下一步就是賦值器的做的事情,也就是業(yè)務(wù)運行的過程中分配內(nèi)存。分配內(nèi)存的時候肯定要指定類型,調(diào)用 runtime.newobject 函數(shù)進行分配,本質(zhì)上調(diào)用 mallocgc 函數(shù)來操作。mallocgc 函數(shù)做幾件事情:

          1. 分配內(nèi)存
          2. 內(nèi)存采樣
          3. gc 標記準備

          我們這里重點分析給 gc 做掃描做的準備。在分配完堆內(nèi)存之后,會調(diào)用一個函數(shù) heapBitsSetType ,這個函數(shù)邏輯非常復(fù)雜,但是做的事情其實一句話能概括:“給 gc 掃描做準備,對分配的內(nèi)存塊做好標記,這小塊內(nèi)存中,哪些位置是指針,我們用一個 bitmap 對應(yīng)記錄下來”。這就是 heapBitsSetType 500 多行代碼做的所有事情,之所以這么復(fù)雜是因為要判斷各種情況。

          heapBitsSetType 主要邏輯解析:

          func heapBitsSetType(x, size, dataSize uintptr, typ *_type) {    // ...
          // 最重要的兩個步驟: // 通過分配地址反查獲取到 heap 的 heapBits 結(jié)構(gòu)(回憶下 golang 的內(nèi)存地址管理) h := heapBitsForAddr(x) // 獲取到類型的指針 bitmap; ptrmask := typ.gcdata // start of 1-bit pointer mask (or GC program, handled below)
          var ( // ... )
          // 把 h.bitp 這個堆上的 bitmap 取出來; hbitp = h.bitp
          // 該類型的指針 bitmap p = ptrmask
          // ... if p != nil { // 把 bitmap 第一個字節(jié)保存起來 b = uintptr(*p) // p 指向下一個字節(jié) p = add1(p) // nb = 8 }
          // 我們的是簡單的 Struct 結(jié)構(gòu)(48==48) if typ.size == dataSize { // nw == 5 == 40/8,說明掃描到第 5 個字段為止即可。 // ptrdata 指明有指針的范圍在[0, 40]以內(nèi),再往外確定就沒有指針字段了; nw = typ.ptrdata / sys.PtrSize } else { nw = ((dataSize/typ.size-1)*typ.size + typ.ptrdata) / sys.PtrSize }
          switch { default: throw("heapBitsSetType: unexpected shift")
          case h.shift == 0: // b 是類型的 ptr bitmap => 00010100 // bitPointerAll => 00001111 // hb => 0000 0100 hb = b & bitPointerAll // bitScan => 0001 0000 // 0001 0000 | 0100 0000 | 1000 0000 // hb => 1101 0100 hb |= bitScan | bitScan<<(2*heapBitsShift) | bitScan<<(3*heapBitsShift) // 賦值 hbitp => 1101 0100 *hbitp = uint8(hb) // 指針往后一個字節(jié)(遞進一個字節(jié)) hbitp = add1(hbitp) // b => 0000 0001 b >>= 4 // nb => 4 nb -= 4
          case sys.PtrSize == 8 && h.shift == 2: // ... }
          // ... // 處理完了前 4 bit,接下來處理后 4 bit nb -= 4 for { // b => 0000 0001 // hb => 0000 0001 hb = b & bitPointerAll // hb => 1111 0001 hb |= bitScanAll if w += 4; w >= nw { // 處理完了,有指針的字段都包含在已經(jīng)處理的 ptrmask 范圍內(nèi)了 break } // ... }
          Phase3: // Phase 3: Write last byte or partial byte and zero the rest of the bitmap entries. // 8 > 5 if w > nw { // mask => 1 mask := uintptr(1)<<(4-(w-nw)) - 1 // hb => 0001 0001 hb &= mask | mask<<4 // apply mask to both pointer bits and scan bits }
          // nw => 6 nw = size / sys.PtrSize
          // ...
          if w == nw+2 { // 賦值 hbitp => 0001 0001 *hbitp = *hbitp&^(bitPointer|bitScan|(bitPointer|bitScan)<uint8(hb) }
          Phase4: // Phase 4: Copy unrolled bitmap to per-arena bitmaps, if necessary. // ...}

          所以,上面函數(shù)調(diào)用完,h.bitp 就給設(shè)置上了:

          低字節(jié) -> 高字節(jié) [ 1101 0100 ], [ 0001 0001 ] |--前4*8字節(jié)--|--后4*8字節(jié)--|

          這個就是 mallocgc 內(nèi)存的時候做的事情。

          總結(jié)就一句話:根據(jù)編譯期間針對每個 struct 生成的 type 結(jié)構(gòu),來設(shè)置 gc 需要掃描的位圖,也就是指針 bitmap。(旁白:每分配一塊內(nèi)存出去,我都會有一個 bitmap 對應(yīng)到這個內(nèi)存塊,指明哪些地方有指針)。

          運行掃描階段



          1. 掃描以 markroot 開始,從棧,全局變量,寄存器等根對象開始掃描,創(chuàng)建一個有向引用圖,把根對象投入到隊列中,重點的一個函數(shù)就是 scanstack
          2. 另外異步的 goroutine ?運行 gcDrain 函數(shù),從隊列里消費對象,并且掃描這個對象;
            1. 掃描調(diào)用的就是 scanobject 函數(shù)

          下面重點介紹:scanstack,scanobject ?這個函數(shù)怎么掃描對象。

          scanstack

          這個函數(shù)是起點函數(shù)( 起始最原始的還是 markroot,但是我們這里梳理主線 ),該掃描棧上所有可達對象,因為棧是一個根,因為你做事情總要有個開始的地方,那么“棧”就是 golang 的起點。

          func scanstack(gp *g, gcw *gcWork) {    // ...    // 掃描棧上所有的可達的對象    state.buildIndex()    for {        p := state.getPtr()        if p == 0 {            break        }        // 獲取一個到棧上對象        obj := state.findObject(p)        if obj == nil {            continue        }        // 獲取到這個對象的類型        t := obj.typ        // ...        // 獲取到這個類型內(nèi)存塊的 ptr 的 bitmap(編譯期間編譯器設(shè)置好)        gcdata := t.gcdata        var s *mspan        if t.kind&kindGCProg != 0 {            s = materializeGCProg(t.ptrdata, gcdata)            gcdata = (*byte)(unsafe.Pointer(s.startAddr))        }
          // 掃描這個對象 // 起點:對象起始地址 => state.stack.lo + obj.off // 終點:t.ptrdata (還記得這個吧,這個指明了指針所在內(nèi)的邊界) // 指針 bitmap:t.gcdata scanblock(state.stack.lo+uintptr(obj.off), t.ptrdata, gcdata, gcw, &state)
          if s != nil { dematerializeGCProg(s) } } // ...}

          小結(jié):

          1. 找到這個 goroutine 棧上的內(nèi)存對象(一個個找,一個個處理);
          2. 找到對象之后,獲取到這個對象的 type 結(jié)構(gòu),然后取出 type.ptrdata, type.gcdata ,從而我們就知道掃描的內(nèi)存范圍,和內(nèi)存塊上指針的所在位置;
          3. 調(diào)用 scanblock 掃描這個內(nèi)存塊;
          scanblock

          scanblock 這個函數(shù)不說你應(yīng)該知道,這是一個非常底層且通用的函數(shù),他的一切參數(shù)都是傳入的,這個函數(shù)作為一個基礎(chǔ)函數(shù)被很多地方調(diào)用:

          /*b0: 掃描開始的位置n0: 掃描結(jié)束的長度ptrmask: 指針的 bitmap*/func scanblock(b0, n0 uintptr, ptrmask *uint8, gcw *gcWork, stk *stackScanState) {    b := b0    n := n0    // 掃描到長度 n 為止;    for i := uintptr(0); i < n; {        // 每個 bit 標識一個 8 字節(jié),8個 bit (1個字節(jié))標識 64 個字節(jié);        // 這里計算到合適的 bits        bits := uint32(*addb(ptrmask, i/(sys.PtrSize*8)))        // 如果整個 bits == 0,那么說明這 8 個 8 字節(jié)都沒有指針引用,可以直接跳到下一輪        if bits == 0 {            i += sys.PtrSize * 8            continue        }        // bits 非0,說明內(nèi)部有指針引用,就必須一個個掃描查看;        for j := 0; j < 8 && i < n; j++ {            // 指針類型?只有標識了指針類型的,才有可能走到下面的邏輯去;            if bits&1 != 0 {                p := *(*uintptr)(unsafe.Pointer(b + i))                if p != 0 {                    if obj, span, objIndex := findObject(p, b, i); obj != 0 {                        // 如果這 8 字節(jié)指向的是可達的內(nèi)存對象,那么就投入掃描隊列(置灰)保護起來;                        greyobject(obj, b, i, span, gcw, objIndex)                    } else if stk != nil && p >= stk.stack.lo && p < stk.stack.hi {                        stk.putPtr(p)                    }                }            }            bits >>= 1            i += sys.PtrSize        }    }}

          如果以上面的 TestStruct 結(jié)構(gòu)舉例的話,假設(shè)在棧上分配了對象 TestStruct{},地址是 0xc00007cf20,那么會從這個地址掃描 scanblock ( 0xc00007cf20, 40, 20, xxx)

          type TestStruct struct {??ptr?uintptr???//?8??f1??uint8?????// 1??f2??*uint8????//?8  f3  uint32    // 4??f4??*uint64???// 8???f5??uint64????// 8?}

          示意圖如下:


          最外層 for 循環(huán)一次就夠了,里面 for 循環(huán) 5 次,掃描到 f4 字段就完了(還記得 type.ptrdata == 40 吧 )。只有 f2 ,f4 字段才會作為指針去掃描。如果 f2, f4 字段存儲的是有效的指針,那么指向的對象會被保護起來(greyobject)。

          小結(jié):

          1. scanblock 這個函數(shù)非常簡單,只掃描給定的一段內(nèi)存塊;
          2. 大循環(huán)每次遞進 64 個字節(jié),小循環(huán)每次遞進 8 字節(jié);
          3. 是否作為指針掃描是由 ptrmask 指定的;
          4. 只要長度和地址是對齊的,指針類型按 8 字節(jié)對齊,那么我們按照 8 字節(jié)遞進掃描一定是全方位覆蓋,不會漏掉一個對象的;
          5. 再次提醒下,uintptr 是數(shù)值類型,編譯器不會標識成指針類型,所以不受掃描保護;

          scanobject

          gcDrain 這個函數(shù)就是從隊列里不斷獲取,處理這些對象,最重要的一個就是調(diào)用 scanobject 繼續(xù)掃描對象。



          markroot 從根(棧)掃描,把掃描到的對象投入掃描隊列。gcDrain 等函數(shù)從里面不斷獲取,不斷處理,并且掃描這些對象,進一步挖掘引用關(guān)系,當掃描結(jié)束之后,那些沒有掃描到的就是垃圾了。

          還是 TestStruct 舉例:

          type TestStruct struct {  ptr uintptr  // 8  f1  uint8    // 1??f2??*uint8?  //?8  f3  uint32   // 4   f4  *uint64  // 8  f5  uint64   // 8}

          如果一個創(chuàng)建在堆上的 TestStruct 對象被投入到掃描隊列,對應(yīng)的 type.gcdata0001 0100 ,TestStruct 對應(yīng)編譯器創(chuàng)建的 type 類型如下:

          (dlv) p typ*runtime._type {	size: 48,	ptrdata: 40,    ...	gcdata: *20,	... }

          scanobject ?邏輯如下:

          /*b   : 是對象的內(nèi)存地址gcw : 是掃描隊列的封裝*/func scanobject(b uintptr, gcw *gcWork) {    // 通過對象地址 b 獲取到這塊內(nèi)存地址對應(yīng)的 hbits     hbits := heapBitsForAddr(b)    // 通過對象地址 b 獲取到這塊內(nèi)存地址所在的 span    s := spanOfUnchecked(b)    // span 的元素大小    n := s.elemsize    if n == 0 {        throw("scanobject n == 0")    }    // ...    var i uintptr    // 每 8 個字節(jié)處理遞進處理(因為堆上對象分配都是 span,每個 span 的內(nèi)存塊都是定長的,所以掃描邊界就是 span.elemsize )    for i = 0; i < n; i += sys.PtrSize {        if i != 0 {            hbits = hbits.next()        }        // 獲取到內(nèi)存塊的 bitmap        bits := hbits.bits()
          // 確認該整個內(nèi)存塊沒有指針,直接跳出,節(jié)約時間; if i != 1*sys.PtrSize && bits&bitScan == 0 { break // no more pointers in this object } // 確認 bits 對應(yīng)的小塊內(nèi)存沒有指針,所以可以直接到下一輪 // 如果是指針,那么就往下看看這 8 字節(jié)啥情況 if bits&bitPointer == 0 { continue // not a pointer }
          // 把這 8 字節(jié)里面存的值取出來; obj := *(*uintptr)(unsafe.Pointer(b + i)) // 如果 obj 有值,并且合法(不在一個 span 的內(nèi)存塊里) if obj != 0 && obj-b >= n { // 如果 obj 指向一個有效的對象,那么把這個對象置灰色,投入掃描隊列,等待處理 if obj, span, objIndex := findObject(obj, b, i); obj != 0 { greyobject(obj, b, i, span, gcw, objIndex) } } } // ...}

          小結(jié):

          1. scanobject 的目的其實很簡單:就是進一步發(fā)現(xiàn)引用關(guān)系,盡可能的把可達對象全覆蓋;
          2. 這個地方就沒有直接使用到 type ,而是使用到 mallocgc 時候的準備成果( heapBitsSetType 設(shè)置),每個內(nèi)存塊都對應(yīng)了一個指針的 bitmap;

          ??總結(jié)??



          1. 要達到“正確并且高效的掃描”需要 編譯期間,運行分配期間,掃描期間 三者配合處理;
          2. 內(nèi)存對齊是非常重要的一個前提條件;
          3. 編譯期間生成 type 類型,對用戶定義的類型全方位分析,標記出所有的指針類型字段;
          4. 運行期間,賦值器分配內(nèi)存的時候,根據(jù) type 結(jié)構(gòu),設(shè)置和對象內(nèi)存一一對應(yīng)的 bitmap,標明指針所在位置,以便后續(xù) gc 掃描;
          5. 回收器掃描期間,從根部開始掃描,遇到對象,則置灰,投入隊列,并且不斷的掃描這些對象指向的對象,直到結(jié)束。掃描的依據(jù),就根據(jù)編譯期間生成的 bitmap,分配期間設(shè)置的 bitmap 來識別哪些地方有指針,然后進一步處理;
          6. 掃描只需要給個開始地址,然后每 8 字節(jié)推進就可以掃描了,為了加快效率我們才有了指針的 bitmap (所以這個是個優(yōu)化項);
          7. 再次強調(diào)下,定義的非指針類型不受保護,比如 uintptr 里面就算存儲的是一個地址的值,也是不會被掃描到的;


          推薦閱讀



          學習交流 Go 語言,掃碼回復(fù)「進群」即可


          站長 polarisxu

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

          不限于 Go 技術(shù)

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


          Go語言中文網(wǎng)

          每天為你

          分享 Go 知識

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



          瀏覽 105
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天堂网www啊啊啊啊啊啊 | 日日操天天 | 啪啪AV网站 | 无码性生活视频 | 亚洲AV成人中文无码专区观看 |