Go 的垃圾回收

垃圾回收是释放未使用的内存空间的过程。换句话说,垃圾收集器查看哪些对象不在范围内并且无法再引用,并释放它们消耗的内存空间。此过程在 Go 程序运行时(而不是在程序执行之前或之后)以并行方式发生。 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.

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

这里面有很多术语,我一会儿会解释。但是首先,我会为你展示一种查看垃圾回收过程的参数的方式。

幸运的是,Go 标准库提供了方法,允许你去学习垃圾回收器的操作以及了解更多关于垃圾回收器在背后所做的事情。相关的代码保存在了gColl.go中,它有三个部分。

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

package main

import (
    "fmt"
    "runtime"
    "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() {
  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(5 * time.Second)
         }
    }
    printStats(mem)
}

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

  $ go run 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 垃圾收集器操作的细节,使用下面这个命令:

  $ GODEBUG=gctrace=1 go run gColl.go

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

  gc 4 @0.025s 0%: 0.002+0.065+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.019 ms 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