欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深度剖析Golang如何實(shí)現(xiàn)GC掃描對象

 更新時(shí)間:2023年03月30日 08:34:57   作者:奇伢云存儲(chǔ)  
這篇文章主要為大家詳細(xì)介紹了Golang是如何實(shí)現(xiàn)GC掃描對象的,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,需要的小伙伴可以參考一下

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

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

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

掃描的目的

掃描到底是為了什么?

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

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

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

掃描的實(shí)現(xiàn)

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

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

編譯階段

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

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

  • 長度要對齊
  • 地址要對齊

“長度要對齊”怎么理解?

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

舉例:

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

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

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

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

指針位標(biāo)記

golang 的所有類型都對應(yīng)一個(gè) _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
}

比如我們定義了一個(gè) Struct 如下:

type TestStruct struct {
	ptr uintptr
	f1  uint8
	f2  *uint8
	f3  uint32
	f4  *uint64
	f5  uint64
}

該結(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,}

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

1.size:類型長度,我們上面這個(gè)類型長度應(yīng)該是 32 字節(jié);

這里理解要應(yīng)用上上面講的結(jié)構(gòu)體字節(jié)對齊的知識(shí),這里就不再復(fù)述;

2.ptrdata:指針截止的長度位置,我們 f4 是指針,所以包含指針的字段最多也就到 40 字節(jié)的位置,ptrdata==40;

要理解字節(jié)對齊哈;

3.kind:表明類型,我們是自定義struct類型,所以 kind == 25

kind 枚舉定義在 runtime/typekind.go 文件里;

4.gcdata:這個(gè)就重要了,這個(gè)就是指針的 bitmap,因?yàn)榫幾g器他在編譯分析的時(shí)候,肯定就知道了所有的類型結(jié)構(gòu),那么自然知道所有的指針位置。gcdata 是 *byte 類型(byte 數(shù)組),當(dāng)前值是 20,20 轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)就是 00010100 ,這個(gè)眼熟不?這個(gè)你要從右往左看就是 00101000(從低 bit 往高 bit 看),這個(gè)不就是剛好是 TestStruct 的指針 bitmap 嘛,每個(gè) bit 表示一個(gè)指針大?。? 字節(jié))的內(nèi)存,00101000 第 3 個(gè) bit 和第 5 個(gè) bit 是 1,表示 第 3 個(gè)字段(第 3 個(gè) 8 字節(jié)的位置)和第 5 個(gè)字段(第 5 個(gè) 8 字節(jié)的位置)是存儲(chǔ)的是指針類型,這里剛好就和 TestStruct.f2 和 TestStruct.f4 對應(yīng)起來。

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

小結(jié)

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

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

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

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

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

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

  • 分配內(nèi)存
  • 內(nèi)存采樣
  • gc 標(biāo)記準(zhǔn)備

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

heapBitsSetType 主要邏輯解析:

func heapBitsSetType(x, size, dataSize uintptr, typ *_type) {
    // ...

    // 最重要的兩個(gè)步驟:
    // 通過分配地址反查獲取到 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 這個(gè)堆上的 bitmap 取出來;
    hbitp = h.bitp

    // 該類型的指針 bitmap
    p = ptrmask
    
    // ...
    if p != nil {
        // 把 bitmap 第一個(gè)字節(jié)保存起來
        b = uintptr(*p)
        // p 指向下一個(gè)字節(jié)
        p = add1(p)
        // 
        nb = 8
    }
    
    // 我們的是簡單的 Struct 結(jié)構(gòu)(48==48)
    if typ.size == dataSize {
        // nw == 5 == 40/8,說明掃描到第 5 個(gè)字段為止即可。
        // 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)
        // 指針往后一個(gè)字節(jié)(遞進(jìn)一個(gè)字節(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)<<heapBitsShift) | 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é)–|

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

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

運(yùn)行掃描階段

1.掃描以 markroot 開始,從棧,全局變量,寄存器等根對象開始掃描,創(chuàng)建一個(gè)有向引用圖,把根對象投入到隊(duì)列中,重點(diǎn)的一個(gè)函數(shù)就是 scanstack 。

2.另外異步的 goroutine 運(yùn)行 gcDrain 函數(shù),從隊(duì)列里消費(fèi)對象,并且掃描這個(gè)對象;

掃描調(diào)用的就是 scanobject 函數(shù)

下面重點(diǎn)介紹:scanstackscanobject 這個(gè)函數(shù)怎么掃描對象。

scanstack

這個(gè)函數(shù)是起點(diǎn)函數(shù)( 起始最原始的還是 markroot,但是我們這里梳理主線 ),該掃描棧上所有可達(dá)對象,因?yàn)闂J且粋€(gè)根,因?yàn)槟阕鍪虑榭傄袀€(gè)開始的地方,那么“棧”就是 golang 的起點(diǎn)。

func scanstack(gp *g, gcw *gcWork) {
    // ...
    // 掃描棧上所有的可達(dá)的對象
    state.buildIndex()
    for {
        p := state.getPtr()
        if p == 0 {
            break
        }
        // 獲取一個(gè)到棧上對象
        obj := state.findObject(p)
        if obj == nil {
            continue
        }
        // 獲取到這個(gè)對象的類型
        t := obj.typ
        // ...
        // 獲取到這個(gè)類型內(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))
        }

        // 掃描這個(gè)對象
        // 起點(diǎn):對象起始地址 => state.stack.lo + obj.off
        // 終點(diǎn):t.ptrdata (還記得這個(gè)吧,這個(gè)指明了指針?biāo)趦?nèi)的邊界)
        // 指針 bitmap:t.gcdata
        scanblock(state.stack.lo+uintptr(obj.off), t.ptrdata, gcdata, gcw, &state)

        if s != nil {
            dematerializeGCProg(s)
        }
    }
    // ...
}

小結(jié):

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

scanblock

scanblock 這個(gè)函數(shù)不說你應(yīng)該知道,這是一個(gè)非常底層且通用的函數(shù),他的一切參數(shù)都是傳入的,這個(gè)函數(shù)作為一個(gè)基礎(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; {
        // 每個(gè) bit 標(biāo)識(shí)一個(gè) 8 字節(jié),8個(gè) bit (1個(gè)字節(jié))標(biāo)識(shí) 64 個(gè)字節(jié);
        // 這里計(jì)算到合適的 bits
        bits := uint32(*addb(ptrmask, i/(sys.PtrSize*8)))
        // 如果整個(gè) bits == 0,那么說明這 8 個(gè) 8 字節(jié)都沒有指針引用,可以直接跳到下一輪
        if bits == 0 {
            i += sys.PtrSize * 8
            continue
        }
        // bits 非0,說明內(nèi)部有指針引用,就必須一個(gè)個(gè)掃描查看;
        for j := 0; j < 8 && i < n; j++ {
            // 指針類型?只有標(biāo)識(shí)了指針類型的,才有可能走到下面的邏輯去;
            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é)指向的是可達(dá)的內(nèi)存對象,那么就投入掃描隊(duì)列(置灰)保護(hù)起來;
                        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,那么會(huì)從這個(gè)地址掃描 scanblock ( 0xc00007cf20, 40, 20, xxx)

type TestStruct struct {
	ptr uintptr
	f1  uint8
	f2  *uint8
	f3  uint32
	f4  *uint64
	f5  uint64
}

示意圖如下:

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

小結(jié):

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

scanobject

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

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

還是 TestStruct 舉例:

type TestStruct struct {
	ptr uintptr
	f1  uint8
	f2  *uint8
	f3  uint32
	f4  *uint64
	f5  uint64
}

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

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

scanobject 邏輯如下:

/*
b   : 是對象的內(nèi)存地址
gcw : 是掃描隊(duì)列的封裝
*/
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 個(gè)字節(jié)處理遞進(jìn)處理(因?yàn)槎焉蠈ο蠓峙涠际?span,每個(gè) 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()
        
        // 確認(rèn)該整個(gè)內(nèi)存塊沒有指針,直接跳出,節(jié)約時(shí)間;
        if i != 1*sys.PtrSize && bits&bitScan == 0 {
            break // no more pointers in this object
        }
        // 確認(rèn) bits 對應(yīng)的小塊內(nèi)存沒有指針,所以可以直接到下一輪
        // 如果是指針,那么就往下看看這 8 字節(jié)啥情況
        if bits&bitPointer == 0 {
            continue // not a pointer
        }

        // 把這 8 字節(jié)里面存的值取出來;
        obj := *(*uintptr)(unsafe.Pointer(b + i))
        // 如果 obj 有值,并且合法(不在一個(gè) span 的內(nèi)存塊里)
        if obj != 0 && obj-b >= n {
            // 如果 obj 指向一個(gè)有效的對象,那么把這個(gè)對象置灰色,投入掃描隊(duì)列,等待處理
            if obj, span, objIndex := findObject(obj, b, i); obj != 0 {
                greyobject(obj, b, i, span, gcw, objIndex)
            }
        }
    }
    // ...
}

小結(jié):

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

總結(jié)

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

到此這篇關(guān)于深度剖析Golang如何實(shí)現(xiàn)GC掃描對象的文章就介紹到這了,更多相關(guān)Golang GC掃描對象內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • VsCode搭建Go語言開發(fā)環(huán)境的配置教程

    VsCode搭建Go語言開發(fā)環(huán)境的配置教程

    這篇文章主要介紹了在VsCode中搭建Go開發(fā)環(huán)境的配置教程,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-05-05
  • GoLang函數(shù)棧的使用詳細(xì)講解

    GoLang函數(shù)棧的使用詳細(xì)講解

    這篇文章主要介紹了GoLang函數(shù)棧的使用,我們的代碼會(huì)被編譯成機(jī)器指令并寫入到可執(zhí)行文件,當(dāng)程序執(zhí)行時(shí),可執(zhí)行文件被加載到內(nèi)存,這些機(jī)器指令會(huì)被存儲(chǔ)到虛擬地址空間中的代碼段,在代碼段內(nèi)部,指令是低地址向高地址堆積的
    2023-02-02
  • go語言版的ip2long函數(shù)實(shí)例

    go語言版的ip2long函數(shù)實(shí)例

    這篇文章主要介紹了go語言版的ip2long函數(shù),實(shí)例分析了Go語言實(shí)現(xiàn)的ip2long函數(shù)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下
    2015-02-02
  • 關(guān)于Golang變量初始化/類型推斷/短聲明的問題

    關(guān)于Golang變量初始化/類型推斷/短聲明的問題

    這篇文章主要介紹了關(guān)于Golang變量初始化/類型推斷/短聲明的問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-02-02
  • Golang?urfave/cli庫簡單應(yīng)用示例詳解

    Golang?urfave/cli庫簡單應(yīng)用示例詳解

    這篇文章主要為大家介紹了Golang?urfave/cli庫簡單應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • Golang內(nèi)存管理之內(nèi)存分配器詳解

    Golang內(nèi)存管理之內(nèi)存分配器詳解

    Go內(nèi)存分配器的設(shè)計(jì)思想來源于TCMalloc,全稱是Thread-Caching?Malloc,核心思想是把內(nèi)存分為多級管理,下面就來和大家深入聊聊Go語言內(nèi)存分配器的使用吧
    2023-06-06
  • golang簡單獲取上傳文件大小的實(shí)現(xiàn)代碼

    golang簡單獲取上傳文件大小的實(shí)現(xiàn)代碼

    這篇文章主要介紹了golang簡單獲取上傳文件大小的方法,涉及Go語言文件傳輸及文件屬性操作的相關(guān)技巧,需要的朋友可以參考下
    2016-07-07
  • GScript?編寫標(biāo)準(zhǔn)庫示例詳解

    GScript?編寫標(biāo)準(zhǔn)庫示例詳解

    這篇文章主要為大家介紹了GScript?編寫標(biāo)準(zhǔn)庫示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-10-10
  • Go安裝和環(huán)境配置圖文教程

    Go安裝和環(huán)境配置圖文教程

    本文主要介紹了Go安裝和環(huán)境配置圖文教程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • golang結(jié)構(gòu)化日志log/slog包之slog.Record的用法簡介

    golang結(jié)構(gòu)化日志log/slog包之slog.Record的用法簡介

    這篇文章主要為大家詳細(xì)介紹了golang結(jié)構(gòu)化日志log/slog包中slog.Record結(jié)構(gòu)體的使用方法和需要注意的點(diǎn),文中的示例代碼講解詳細(xì),需要的可以學(xué)習(xí)一下
    2023-10-10

最新評論