02.3 垃圾回收

垃圾回收是释放掉那些不再被使用的的内存空间的过程。换句话说,垃圾回收器会去检查哪些对象超出范围并且不会再被引用到,然后它会去释放掉那些对象占用的内存空间。这个过程是在go程序运行中以并发的方式去进行的,不是go程序执行之前,也不是go程序执行之后。go垃圾回收器实现的说明文档给出了如下声明(runtime下的mgc.go中):

The GC runs concurrently with mutator threads, is type accurate (aka precise), allows multiple GC thread to run in parallel. It is a concurrent mark and sweep that uses a write barrier. It is non-generational and non-compacting. Allocation is done using size segregated per P allocation areas to minimize fragmentation while eliminating locks in the common case.

垃圾回收是和go线程同时运行的,它是类型精确的,而且多个垃圾回收线程可以并行运行。它是一种使用了写屏障的并发标记清除的垃圾回收方式。它是非分代和非压缩的。使用按P分配区域隔离的大小来完成分配,以最小化碎片,同时消除常见情况下的锁。

这里面有很多术语,我一会儿会解释。但是首先,我会为你展示一种查看垃圾回收过程的参数的方式。幸运的是,Go标准库提供了方法,允许你去学习垃圾回收器的操作以及了解更多关于垃圾回收器在背后所做的事情。相关的代码保存在了gColl.go中,它有三个部分。

gColl.go的第一部分代码段如下所示:

package main

import (
    "fmt"
    "os"
    "runtime"
    "runtime/trace"
    "time"
)

func printStats(mem runtime.MemStats) {
    runtime.ReadMemStats(&mem)
    fmt.Println("mem.Alloc:", mem.Alloc)
    fmt.Println("mem.TotalAlloc:", mem.TotalAlloc)
    fmt.Println("mem.HeapAlloc:", mem.HeapAlloc)
    fmt.Println("mem.NumGC:", mem.NumGC)
    fmt.Println("-----")
}

注意到每次你都需要获取更多最近的垃圾回收统计信息,你会需要调用runtime.ReadMemStats()方法。printStats()方法的目的是去避免每次要写相同代码。

第二部分代码如下:

func main() {
    f, err := os.Create("/tmp/traceFile.out")
    if err != nil {
        panic(err)
    }
    defer f.Close()
    err = trace.Start(f)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer trace.Stop()

    var mem runtime.MemStats
    printStats(mem)
    for i := 0; i < 10; i++ {
        s := make([]byte, 50000000)
        if s == nil {
            fmt.Println("Operation failed!")
        }
    }
    printStats(mem)

for循环里创建一堆大的go slices,目的是为了进行大量内存分配来触发垃圾回收。

最后一部分是接下来gColl.go的代码,用go slices进行了更多的内存分配。

    for i := 0; i < 10; i++ {
        s := make([]byte, 100000000)
        if s == nil {
            fmt.Println("Operation failed!")
        }
        time.Sleep(time.Millisecond)
    }
    printStats(mem)
}

在macOS High Sierra上面gColl.go的输出如下:

mem.Alloc: 66024
mem.TotalAlloc: 66024
mem.HeapAlloc: 66024
mem.NumGC: 0
-----
mem.Alloc: 50078496
mem.TotalAlloc: 500117056
mem.HeapAlloc: 50078496
mem.NumGC: 10
-----
mem.Alloc: 76712
mem.TotalAlloc: 1500199904
mem.HeapAlloc: 76712
mem.NumGC: 20
-----

尽管你不会每次都去检查go垃圾收集器的操作,但是在一个慢的应用程序上可以看到go垃圾回收器的工作方式,长时间的运行里会节省你很多时间。我很确定,花点时间去整体学习了解垃圾回收,尤其是了解go垃圾回收器的工作方式,你不会后悔的。

这里有一个技巧可以让你得到更多关于go 垃圾收集器操作的细节,使用下面这个命令。

GODEBUG=gctrace=1 go run gColl.go

所以,如果你在任何go run命令前面加上GODEBUG=gctrace=1,go就会去打印关于垃圾回收操作的一些分析数据。生成的数据是如下的形式:

gc 4 @0.025s 0%: 0.002+0.65+0.018 ms clock, 0.021+0.040/0.057/0.003+0.14 ms cpu, 47->47->0 MB, 48 MB goal, 8 P
gc 17 @30.103s 0%: 0.004+0.080+0.019ms clock, 0.033+0/0.076/0.071+0.15 ms cpu, 95->95->0 MB, 96 MB goal, 8 P

这些数据给你提供了更多垃圾回收过程中的堆内存大小的信息。让我们以47->47->0 MB这三个值为例。第一个数值是垃圾回收器要去运行时候的堆内存大小。第二个值是垃圾回收器操作结束时候的堆内存大小。最后一个值就是生存堆的大小。

Last updated