天天看點

從golang的垃圾回收說起(下篇)

文章來自網易雲社群

4 Golang垃圾回收的相關參數

4.1 觸發GC

gc觸發的時機:2分鐘或者記憶體占用達到一個門檻值(目前堆記憶體占用是上次gc後對記憶體占用的兩倍,當GOGC=100時)

 # 表示目前應用占用的記憶體是上次GC時占用記憶體的兩倍時,觸發GCexport GOGC=100      

4.2 檢視GC資訊

export GODEBUG=gctrace=1      

可以檢視gctrace資訊。

舉例:

gc 1 @0.008s 6%: 0.071+2.0+0.080 ms clock, 0.21+0.22/1.9/1.9+0.24 ms cpu, 4->4->3 MB, 5 MB goal, 4 P# command-line-argumentsgc 1 @0.001s 16%: 0.071+3.3+0.060 ms clock, 0.21+0.17/2.9/0.36+0.18 ms cpu, 4->4->4 MB, 5 MB goal, 4 Pgc 2 @0.016s 8%: 0.020+6.0+0.070 ms clock, 0.082+0.094/3.9/2.2+0.28 ms cpu, 8->9->8 MB, 9 MB goal, 4 Pgc 3 @0.046s 7%: 0.019+7.3+0.062 ms clock, 0.076+0.089/7.1/7.0+0.24 ms cpu, 14->16->14 MB, 17 MB goal, 4 Pgc 4 @0.092s 8%: 0.015+24+0.10 ms clock, 0.060+0.10/24/0.75+0.42 ms cpu, 25->27->24 MB, 28 MB goal, 4 P      

每個字段表示什麼資訊可以參考 golang doc

5 如何提高GC的性能

Golang的GC算法是固定的,使用者無法去配置采用什麼算法,也沒法像Java一樣配置年輕代、老年代的空間比例等。golang的GC相關的配置參數隻有一個,即GOGC,用來表示觸發GC的條件。

目前來看,提高GC效率我們唯一能做的就是減少垃圾的産生。是以說,這一章稱為提高GC的性能也不太合适。下面我們就主要讨論一下,在golang中如何減少垃圾的産生,有哪些需要注意的方面。

5.1 golang中的記憶體配置設定

參考官網Frequently Asked Questions (FAQ)

How do I know whether a variable is allocated on the heap or the stack?

From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

我們看一個例子有個直覺的認識:

1 package main2 3 import ()4 5 func foo() *int {6     var x int7     return &x8 }9 10 func bar() int {11     x := new(int)12     *x = 113     return *x14 }15 16 func big() {17     x := make([]int,0,20)18     y := make([]int,0,20000)19 20     len := 1021     z := make([]int,0,len)22 }23 24 func main() {25  26 }      
# go build -gcflags='-m -l' test.go./test.go:7:12: &x escapes to heap
./test.go:6:9: moved to heap: x
./test.go:11:13: bar new(int) does not escape
./test.go:18:14: make([]int, 0, 20000) escapes to heap
./test.go:21:14: make([]int, 0, len) escapes to heap
./test.go:17:14: big make([]int, 0, 20) does not escape
./test.go:17:23: x declared and not used
./test.go:18:23: y declared and not used
./test.go:21:23: z declared and not used      

5.2 sync.Pool對象池

sync.Pool主要是為了重用對象,一方面縮短了申請空間的時間,另一方面,還減輕了GC的壓力。不過它是一個臨時對象池,為什麼這麼說呢?因為對象池中的對象會被GC回收。是以說,有狀态的對象,比如資料庫連接配接是不能夠用sync.Pool來實作的。

use sync.Pool if you frequently allocate many objects of the same type and you want to save some allocation and garbage collection overhead. However, in the current implementation, any unreferenced sync.Pool objects are removed at each garbage collection cycle, so you can't use this as a long-lived free-list of objects. If you want a free-list that maintains objects between GC runs, you'll still have to build that yourself. This is only to reuse allocated objects between garbage collection cycles.  

sync.Pool主要有兩個方法:

func (p *Pool) Get() interface{} {
    ...
}func (p *Pool) Put(x interface{}) {
    ...
}      

Get方法是指從臨時對象池中申請對象,put是指把不再使用的對象傳回對象池,以便後續重用。如果我們在使用Get申請新對象時pool中沒有可用的對象,那麼就會傳回nil,除非設定了sync.Pool的New func:

type Pool struct {

    ...    // New optionally specifies a function to generate
    // a value when Get would otherwise return nil.
    // It may not be changed concurrently with calls to Get.
    New func() interface{}
}      

另外,我們不能對從對象池申請到的對象值做任何假設,可能是New新生成的,可能是被某個協程修改過放回來的。

一個比較好的使用sync.Pool的例子:

var DEFAULT_SYNC_POOL *SyncPool

func NewPool() *SyncPool {
    DEFAULT_SYNC_POOL = NewSyncPool(
        5,     
        30000, 
        2,     
    )
    return DEFAULT_SYNC_POOL
}

func Alloc(size int) []int64 {
    return DEFAULT_SYNC_POOL.Alloc(size)
}

func Free(mem []int64) {
    DEFAULT_SYNC_POOL.Free(mem)
}

// SyncPool is a sync.Pool base slab allocation memory pool
type SyncPool struct {
    classes     []sync.Pool
    classesSize []int
    minSize     int
    maxSize     int
}

func NewSyncPool(minSize, maxSize, factor int) *SyncPool {
    n := 0
    for chunkSize := minSize; chunkSize <= maxSize; chunkSize *= factor {
        n++
    }
    pool := &SyncPool{
        make([]sync.Pool, n),
        make([]int, n),
        minSize, maxSize,
    }
    n = 0
    for chunkSize := minSize; chunkSize <= maxSize; chunkSize *= factor {
        pool.classesSize[n] = chunkSize
        pool.classes[n].New = func(size int) func() interface{} {
            return func() interface{} {
                buf := make([]int64, size)
                return &buf
            }
        }(chunkSize)
        n++
    }
    return pool
}

func (pool *SyncPool) Alloc(size int) []int64 {
    if size <= pool.maxSize {
        for i := 0; i < len(pool.classesSize); i++ {
            if pool.classesSize[i] >= size {
                mem := pool.classes[i].Get().(*[]int64)
                // return (*mem)[:size]
                return (*mem)[:0]
            }
        }
    }
    return make([]int64, 0, size)
}

func (pool *SyncPool) Free(mem []int64) {
    if size := cap(mem); size <= pool.maxSize {
        for i := 0; i < len(pool.classesSize); i++ {
            if pool.classesSize[i] >= size {
                pool.classes[i].Put(&mem)
                return
            }
        }
    }
}      

有一個開源的通用golang對象池實作,有興趣的可以研究一下:Go Commons Pool,在此不再贅述。

5.3 append

我們先看一下append的基本用法。

nums:=make([]int,0,10)      

建立切片,len=0,cap=10,底層實際上配置設定了10個元素大小的空間。在沒有append資料的情況下,不能直接使用nums[index]。

nums:=make([]int,5,10)      

建立切片,len=5,cap=10,底層實際上配置設定了10個元素大小的空間。在沒有append資料的情況下,可以直接使用nums[index],index的範圍是[0,4]。執行append操作的時候是從index=5的位置開始存儲的。

nums := make([]int,5)      

如果沒有指定capacity,那麼cap與len相等。

nums = append(nums,10)      

執行append操作的時候,nums的位址可能會改變,是以需要利用其傳回值重新設定nums。至于nums的位址會不會改變,取決于還沒有空間來存儲新的資料,如果沒有空閑空間了,那就需要申請cap*2的空間,将資料複制過去。

是以,我們在使用append操作的時候,最好是設定一個比較合理的cap值,即根據自己的應用場景預申請大小合适的空間,避免無謂的不斷重新申請新空間,這樣可以減少GC的壓力。

由append導緻的記憶體飙升和GC壓力過大這個問題,需要特别注意一下。

參考文獻

1 https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)

2 http://legendtkl.com/2017/04/28/golang-gc/

3 https://github.com/golang/proposal/blob/master/design/17503-eliminate-rescan.md

4 https://lengzzz.com/note/gc-in-golang

5 https://making.pusher.com/golangs-real-time-gc-in-theory-and-practice/

6 https://blog.twitch.tv/gos-march-to-low-latency-gc-a6fa96f06eb7

7 https://golang.org/doc/faq

8 《垃圾回收的算法與實作》 中村成楊 相川光. 編著  

網易雲新使用者大禮包:https://www.163yun.com/gift

本文來自網易實踐者社群,經作者李岚清授權釋出。

相關文章:

【推薦】 四六級成績查詢,你的『驗證碼』刷出來了嗎?

【推薦】 接口測試之Kotlin篇(下)