亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Golang怎么實現GC掃描對象

發布時間:2023-03-31 17:16:00 來源:億速云 閱讀:127 作者:iii 欄目:開發技術

這篇文章主要介紹了Golang怎么實現GC掃描對象的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Golang怎么實現GC掃描對象文章都會有所收獲,下面我們一起來看看吧。

    掃描的目的

    掃描到底是為了什么?

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

    • 垃圾回收的根本目的是:“回收那些業務永遠都不會再使用的內存塊”;

    • 掃描的目的則是:“把這些不再使用的內存塊找出來”;

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

    掃描的實現

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

    • 編譯階段:編譯期是非常重要的一環,針對靜態類型做好標記準備(旁白:原則上編譯期能做的絕對不留到運行期);

    • 運行階段:賦值器分配內存的時候,根據編譯階段的 type 標示,會為分配的對象內存設置好一個對應的指針標示的 bitmap;

    • 掃描階段:根據指針的 bitmap 標示,地毯式掃描;

    編譯階段

    結構體對齊

    要理解編譯階段做的事情,那么首先要理解結構體對齊的基礎知識。這個和 C 語言類似,golang 的結構體是有對齊規則的,也就是說,必要的時候可能會填充一些內存空間來滿足對齊的要求。總結來說兩條規則:

    • 長度要對齊

    • 地址要對齊

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

    結構體的長度要至少是內部最長的基礎字段的整數倍。

    舉例:

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

    這個結構體內存占用 size 多大?

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

    dlv 調試如下:

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

    字節示意圖:

    |--8 Byte--|--4 Byte--|--4 Byte--|

    “地址要對齊”怎么理解?

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

    舉例:

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

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

    dlv 調試如下:

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

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

    • ptr 是第一個字段,當然和結構體本身地址一樣,相對偏移是 0,所以地址是 0xc00008a010 == 0xc00008a010 + 0 ;

    • f1 是第二個字段,由于前一個字段 ptr 是 uintptr 類型(8字節),并且由于 f1 本身是 uint8 類型(1字節),所以 f1 從 8 偏移開始沒毛病,所以 f1 的偏移地址從 0xc00008a018 == 0xc00008a010 + 8

    • f2 是第三個字段,由于前一個字段 f1 是 uint8(1字節),所以表面上看好像 f2 要接著 0xc00008a019 (= 0xc00008a018 +1) 這個地址才對,但是 f2 本身是 uint32 (4字節的類型),所以 f2 地址偏移至少要是 4 的倍數,所以 f2 的地址要從 0xc00008a01c (0xc00008a018 + 4)這個地址開始才對。也就是說,f1 到 f2 之間填充了一些不用的空間,為了地址對齊。

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

    指針位標記

    golang 的所有類型都對應一個 _type 結構,可以在 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
    	f1  uint8
    	f2  *uint8
    	f3  uint32
    	f4  *uint64
    	f5  uint64
    }

    該結構 dlv 調試如下:

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

    在編譯期間,編譯器就會在內部生成一個 _type 結構體與之對應。_type 里面重點解釋幾個和本次掃描主題相關的字段:

    1.size:類型長度,我們上面這個類型長度應該是 32 字節;

    這里理解要應用上上面講的結構體字節對齊的知識,這里就不再復述;

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

    要理解字節對齊哈;

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

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

    4.gcdata:這個就重要了,這個就是指針的 bitmap,因為編譯器他在編譯分析的時候,肯定就知道了所有的類型結構,那么自然知道所有的指針位置。gcdata 是 *byte 類型(byte 數組),當前值是 20,20 轉換成二進制數據就是 00010100 ,這個眼熟不?這個你要從右往左看就是 00101000(從低 bit 往高 bit 看),這個不就是剛好是 TestStruct 的指針 bitmap 嘛,每個 bit 表示一個指針大小(8 字節)的內存,00101000 第 3 個 bit 和第 5 個 bit 是 1,表示 第 3 個字段(第 3 個 8 字節的位置)和第 5 個字段(第 5 個 8 字節的位置)是存儲的是指針類型,這里剛好就和 TestStruct.f2 和 TestStruct.f4 對應起來。

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

    小結

    編譯階段給每個類型生成 _type 類型,內部對類型字段生成指針的 bitmap,這個是后面掃描行為的基礎依據。

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

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

    • 精度太低,你編譯期間不做準備,那運行期間就要來償還這部分損耗,你無法判斷是不是指針,所以只要指向了一個有效內存地址,就得無腦保護,這樣就保護了很多不需要保護的內存塊;

    • 掃描太低效,必須全地址掃描,因為你沒有 bitmap,無法識別是否有指針。也無法做優化,比如我們程序里面可能 一半以上的類型內是不包含指針的,這種根本就不需要掃描;

    運行期內存分配

    下一步就是賦值器的做的事情,也就是業務運行的過程中分配內存。分配內存的時候肯定要指定類型,調用 runtime.newobject 函數進行分配,本質上調用 mallocgc 函數來操作。mallocgc 函數做幾件事情:

    • 分配內存

    • 內存采樣

    • gc 標記準備

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

    heapBitsSetType 主要邏輯解析:

    func heapBitsSetType(x, size, dataSize uintptr, typ *_type) {
        // ...
    
        // 最重要的兩個步驟:
        // 通過分配地址反查獲取到 heap 的 heapBits 結構(回憶下 golang 的內存地址管理)
        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 第一個字節保存起來
            b = uintptr(*p)
            // p 指向下一個字節
            p = add1(p)
            // 
            nb = 8
        }
        
        // 我們的是簡單的 Struct 結構(48==48)
        if typ.size == dataSize {
            // nw == 5 == 40/8,說明掃描到第 5 個字段為止即可。
            // ptrdata 指明有指針的范圍在[0, 40]以內,再往外確定就沒有指針字段了;
            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)
            // 指針往后一個字節(遞進一個字節)
            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 {
                // 處理完了,有指針的字段都包含在已經處理的 ptrmask 范圍內了
                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.
        // ...
    }

    所以,上面函數調用完,h.bitp 就給設置上了:

    低字節 -> 高字節 [ 1101 0100 ], [ 0001 0001 ] |&ndash;前4*8字節&ndash;|&ndash;后4*8字節&ndash;|

    這個就是 mallocgc 內存的時候做的事情。

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

    運行掃描階段

    1.掃描以 markroot 開始,從棧,全局變量,寄存器等根對象開始掃描,創建一個有向引用圖,把根對象投入到隊列中,重點的一個函數就是 scanstack 。

    2.另外異步的 goroutine 運行 gcDrain 函數,從隊列里消費對象,并且掃描這個對象;

    掃描調用的就是 scanobject 函數

    下面重點介紹:scanstackscanobject 這個函數怎么掃描對象。

    scanstack

    這個函數是起點函數( 起始最原始的還是 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
            // ...
            // 獲取到這個類型內存塊的 ptr 的 bitmap(編譯期間編譯器設置好)
            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 (還記得這個吧,這個指明了指針所在內的邊界)
            // 指針 bitmap:t.gcdata
            scanblock(state.stack.lo+uintptr(obj.off), t.ptrdata, gcdata, gcw, &state)
    
            if s != nil {
                dematerializeGCProg(s)
            }
        }
        // ...
    }

    小結:

    • 找到這個 goroutine 棧上的內存對象(一個個找,一個個處理);

    • 找到對象之后,獲取到這個對象的 type 結構,然后取出 type.ptrdata, type.gcdata ,從而我們就知道掃描的內存范圍,和內存塊上指針的所在位置;

    • 調用 scanblock 掃描這個內存塊;

    scanblock

    scanblock 這個函數不說你應該知道,這是一個非常底層且通用的函數,他的一切參數都是傳入的,這個函數作為一個基礎函數被很多地方調用:

    /*
    b0: 掃描開始的位置
    n0: 掃描結束的長度
    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 字節,8個 bit (1個字節)標識 64 個字節;
            // 這里計算到合適的 bits
            bits := uint32(*addb(ptrmask, i/(sys.PtrSize*8)))
            // 如果整個 bits == 0,那么說明這 8 個 8 字節都沒有指針引用,可以直接跳到下一輪
            if bits == 0 {
                i += sys.PtrSize * 8
                continue
            }
            // bits 非0,說明內部有指針引用,就必須一個個掃描查看;
            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 字節指向的是可達的內存對象,那么就投入掃描隊列(置灰)保護起來;
                            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 結構舉例的話,假設在棧上分配了對象 TestStruct{},地址是 0xc00007cf20,那么會從這個地址掃描 scanblock ( 0xc00007cf20, 40, 20, xxx)

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

    示意圖如下:

    Golang怎么實現GC掃描對象

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

    小結:

    • scanblock 這個函數非常簡單,只掃描給定的一段內存塊;

    • 大循環每次遞進 64 個字節,小循環每次遞進 8 字節;

    • 是否作為指針掃描是由 ptrmask 指定的;

    • 只要長度和地址是對齊的,指針類型按 8 字節對齊,那么我們按照 8 字節遞進掃描一定是全方位覆蓋,不會漏掉一個對象的;

    • 再次提醒下,uintptr 是數值類型,編譯器不會標識成指針類型,所以不受掃描保護;

    scanobject

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

    Golang怎么實現GC掃描對象

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

    還是 TestStruct 舉例:

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

    如果一個創建在堆上的 TestStruct 對象被投入到掃描隊列,對應的 type.gcdata 是 0001 0100 ,TestStruct 對應編譯器創建的 type 類型如下:

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

    scanobject 邏輯如下:

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

    小結:

    • scanobject 的目的其實很簡單:就是進一步發現引用關系,盡可能的把可達對象全覆蓋;

    • 這個地方就沒有直接使用到 type ,而是使用到 mallocgc 時候的準備成果( heapBitsSetType 設置),每個內存塊都對應了一個指針的 bitmap;

    關于“Golang怎么實現GC掃描對象”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Golang怎么實現GC掃描對象”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    阿坝| 华池县| 丹棱县| 巴南区| 青河县| 湟源县| 渭源县| 永安市| 曲沃县| 元朗区| 剑阁县| 满洲里市| 宝清县| 巴中市| 桐乡市| 昂仁县| 鄂托克前旗| 凉城县| 大新县| 通渭县| 本溪市| 安义县| 江达县| 临夏县| 米林县| 高陵县| 从化市| 兴文县| 贵溪市| 忻州市| 房山区| 宜君县| 梁山县| 武乡县| 石阡县| 吴桥县| 海原县| 汝阳县| 山东| 沅陵县| 河曲县|