Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go垃圾回收机制

Go 语言垃圾回收机制从入门到理解

作者:小羊在睡觉

本文主要介绍了Go 语言垃圾回收机制从入门到理解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

前言:为什么我们要聊 GC?

我们写程序就像是在一个大房间里工作,每当我们创建一个变量、一个对象,就等于往房间里放了一件家具。用完的家具,如果我们不及时处理,房间就会越来越乱,最终挤得连走路的地方都没有。在编程世界里,这个“房间”就是内存,而“垃圾”就是那些程序不再使用的内存空间。

在 Go 语言中,我们不用自己去手动清理这些“垃圾”,因为有一个勤劳的“清洁工”—— 垃圾回收器(Garbage Collector, GC) 会自动完成这项工作。虽然它很勤快,但如果我们的程序写得不够好,让它忙不过来,也可能会影响程序的性能。所以,理解 GC 的工作原理,能帮助我们写出更高效、更“干净”的代码。

1. 什么是垃圾回收?

在深入 Go 的 GC 之前,我们先来聊聊 GC 的基本概念。

什么是“垃圾”?

简单来说,“垃圾”就是 程序不再使用的内存

举个例子:

package main
 
import "fmt"
 
func main() {
	var a int = 10
	{
		var b int = 20
		fmt.Println(b) // 在这里,变量 b 仍然有效
	} // b 的作用域结束,b 占用的内存成为“垃圾”
 
	// 另一个例子:当一个变量不再被引用时
	s := "hello world"
	fmt.Println(s)
	s = "go language" // 字符串 "hello world" 不再被任何变量引用,成为“垃圾”
	fmt.Println(s)
} // a 的作用域结束,a 占用的内存成为“垃圾”
 
 
 
 

当一个变量的作用域结束,或者没有其他任何变量再指向它时,它所占用的内存就是可以被回收的“垃圾”了。

为什么需要 GC?

2. Go GC 的核心原理:三色标记法

Go GC 的核心算法叫做 “三色标记法”。听起来像是在画画,没错,它的原理就是给程序中的所有对象“涂上”三种颜色。

三种颜色代表什么?

我们可以把程序中所有的内存对象想象成一个个小方块,GC 的任务就是给这些小方块“涂色”,然后把白色的方块清理掉。

三色标记的流程

  1. 初始状态:所有对象都是白色的。
  2. 标记阶段 (Mark):GC 会从“根对象”开始遍历,比如全局变量、当前函数栈上的变量等。GC 会把这些根对象以及它们直接引用的对象标记为灰色,并放入一个队列。
  3. 循环检查:GC 依次从灰色队列中取出一个对象,把它标记为黑色,然后检查它所引用的所有对象。如果引用的对象是白色的,就把它标记为灰色并加入队列。
  4. 最终清理 (Sweep):当灰色队列变空,GC 就知道所有存活的对象都被标记成黑色了。这时,GC 就会遍历整个内存,把所有还停留在 白色 的对象全部回收掉。

3. Go GC 的演进:从“暂停世界”到“并发执行”

早期的 GC 算法有一个很大的缺点,叫做 STW (Stop-The-World)

什么是 STW?

Go 语言的 GC 团队也意识到了这个问题,并进行了一系列优化。

Go 1.5 之后:Go 语言引入了 并发 GC

Go 1.8 之后:Go GC 进一步优化,现在的 STW 暂停时间已经非常短,通常在微秒级别,几乎可以忽略不计。

4. 如何观察和优化 Go GC?

既然 GC 是自动的,那我们还需要关心它吗?当然!理解 GC 的工作,可以帮助我们更好地诊断和解决性能问题。

如何查看 GC 信息?

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	// 打印 GC 状态
	go func() {
		for {
			var m runtime.MemStats
			runtime.ReadMemStats(&m)
			fmt.Printf("当前已分配内存: %v MB, 下次GC内存阈值: %v MB\n", m.Alloc/1024/1024, m.NextGC/1024/1024)
			time.Sleep(2 * time.Second)
		}
	}()

	// 持续分配内存
	var a []byte
	for i := 0; i < 10; i++ {
		// 每次分配 100MB 内存
		a = append(a, make([]byte, 100*1024*1024)...)
		fmt.Printf("第 %d 次分配内存完成\n", i+1)
		time.Sleep(1 * time.Second)
	}
}

运行这个程序,同时设置 GODEBUG=gctrace=1 环境变量,例如: GODEBUG=gctrace=1 go run your_program.go

运行后,除了我们自己打印的内存信息,你还会看到类似下面的 GC 日志:

gc 1 @0.038s 0%: 0.054+1.4+0.007 ms clock, 0.43+0.32/1.4/0.38+0.05 ms cpu, 4->4 MB, 10->10 MB, 4 (2) objects, 1 (0) goroutines, 0/0/0/0 ms inter-sweep, 0/0(G)/0(H) MSpans, ...

如何减少 GC 压力?

sync.Pool 示例:

package main

import (
	"fmt"
	"sync"
	"time"
)

// 定义一个需要被频繁创建和销毁的对象
type Data struct {
	ID   int
	Name string
}

func main() {
	// 创建一个 sync.Pool,并定义 New 函数,用于创建新的对象
	dataPool := &sync.Pool{
		New: func() interface{} {
			fmt.Println("创建了一个新的 Data 对象")
			return &Data{}
		},
	}

	// 不使用 sync.Pool,每次都创建新对象
	fmt.Println("--- 不使用 sync.Pool ---")
	for i := 0; i < 3; i++ {
		_ = &Data{ID: i}
		time.Sleep(100 * time.Millisecond)
	}

	// 使用 sync.Pool,从池中获取对象
	fmt.Println("\n--- 使用 sync.Pool ---")
	for i := 0; i < 3; i++ {
		// Get() 方法会尝试从池中获取一个对象,如果池为空,则会调用 New()
		obj := dataPool.Get().(*Data)
		obj.ID = i
		obj.Name = fmt.Sprintf("数据 %d", i)
		fmt.Printf("使用对象: %+v\n", obj)

		// Put() 方法会将对象放回池中,供下次复用
		dataPool.Put(obj)
		time.Sleep(100 * time.Millisecond)
	}

	// 再次获取,这次会直接从池中复用,而不会调用 New()
	fmt.Println("\n--- 再次使用 sync.Pool ---")
	obj := dataPool.Get().(*Data)
	fmt.Printf("复用对象: %+v\n", obj)
}




合理设置 GOGCGOGC 是一个环境变量,可以用来控制 GC 的触发时机。默认值为 100,表示当新分配的内存达到上次 GC 之后存活内存的 100% 时,就会触发新一轮 GC。你可以根据程序的特点,适当调整这个值。

总结

Go GC 的设计理念是 并发、低延迟、自动管理。作为 Go 开发者,虽然我们不用手动管理内存,但理解 GC 的工作原理依然非常重要。它能帮助我们写出更高效的程序,更轻松地应对各种性能挑战。

到此这篇关于Go 语言垃圾回收机制从入门到理解的文章就介绍到这了,更多相关Go垃圾回收机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文