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

 更新时间:2025年09月03日 09:58:50   作者:小羊在睡觉  
本文主要介绍了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?

  • 减轻开发者的负担:在没有 GC 的语言(比如 C++)中,开发者需要手动分配和释放内存。这很容易出错,比如忘记释放内存导致 内存泄漏,或者重复释放内存导致程序崩溃。
  • 提高开发效率:有了 GC,开发者可以更专注于业务逻辑的实现,而不用花费大量精力去管理内存,大大提升了开发效率。

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

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

三种颜色代表什么?

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

  • 白色 (White): 初始状态,所有对象都是白色的。它们是 GC 眼中的 “潜在垃圾”
  • 灰色 (Gray): 对象被 标记 了,但它里面包含的引用(比如一个结构体里的指针)还没有被检查。我们可以把灰色对象看作是“待处理”的对象。
  • 黑色 (Black): 对象被标记了,并且它所引用的所有子对象也都被检查过了。黑色对象就是 “确认存活” 的对象。

三色标记的流程

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

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

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

什么是 STW?

  • 在 STW 模式下,GC 运行时,程序会完全暂停,不能做任何事情。
  • 这就好比清洁工来打扫房间时,你必须停下所有工作,坐在椅子上不动,等他打扫完了你才能继续。
  • 缺点:如果你的程序内存很大,GC 暂停的时间就会很长,这会严重影响程序的性能,尤其是在高并发的服务器应用中,用户可能会感受到明显的卡顿。

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

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

  • 什么是并发 GC? 顾名思义,就是 GC 的大部分工作可以 和程序同时进行
  • 这大大减少了 STW 的暂停时间,让 GC 几乎不会影响到程序的正常运行。

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

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

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

如何查看 GC 信息?

  • 你可以在运行 Go 程序时,通过设置环境变量来查看详细的 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 的 ID、触发时间、STW 暂停时间、回收的内存量等关键信息。

如何减少 GC 压力?

  • 减少内存分配:每次 new 或 make 都会增加 GC 的工作量。尽量复用对象,减少不必要的内存分配。
  • 使用 sync.Pool:对于那些创建和销毁都非常频繁的小对象,可以使用 sync.Pool 来缓存和复用它们,大大减轻 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垃圾回收机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Go语言中 Channel 详解

    Go语言中 Channel 详解

    Go 语言中的 channel 是实现 goroutine 间无锁通信的关键机制,他使得写多线程并发程序变得简单、灵活、触手可得。下面就个人理解对 channel 使用过程中应该注意的地方进行一个简要的总结。
    2018-10-10
  • 解读golang中的const常量和iota

    解读golang中的const常量和iota

    这篇文章主要介绍了golang中的const常量和iota,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • golang国内proxy设置方式

    golang国内proxy设置方式

    建议使用goproxy.cn作为Go包镜像,因其速度快且缓存丰富,若遇下载问题可更换源,同时注意GitHub删除的包可能在goproxy.io中缓存
    2025-07-07
  • 简单了解Go语言中函数作为值以及函数闭包的使用

    简单了解Go语言中函数作为值以及函数闭包的使用

    这篇文章主要介绍了简单了解Go语言中函数作为值以及函数闭包的使用,是golang入门学习中的基础知识,需要的朋友可以参考下
    2015-10-10
  • golang实现跨域访问的方法

    golang实现跨域访问的方法

    这篇文章主要介绍了golang实现跨域访问的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01
  • golang gorm多条件筛选查询操作

    golang gorm多条件筛选查询操作

    这篇文章主要介绍了golang gorm多条件筛选查询操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • Go-客户信息关系系统的实现

    Go-客户信息关系系统的实现

    这篇文章主要介绍了Go-客户信息关系系统的实现,本文章内容详细,具有很好的参考价值,希望对大家有所帮助,需要的朋友可以参考下
    2023-01-01
  • go打包aar及flutter调用aar流程详解

    go打包aar及flutter调用aar流程详解

    这篇文章主要为大家介绍了go打包aar及flutter调用aar流程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • go mod 使用私有gitlab群组的解决方案

    go mod 使用私有gitlab群组的解决方案

    这篇文章主要介绍了go mod 使用私有gitlab群组的解决方案,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • 用go gin server来做文件上传服务

    用go gin server来做文件上传服务

    今天小编就为大家分享一篇关于用go gin server来做文件上传服务,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-04-04

最新评论