垃圾回收就是自动回收程序中不再使用的内存,避免内存泄漏。程序运行时分配了很多内存,比如给变量、结构体等。当某些变量或对象再也不会被使用时,垃圾回收会回收这些内存,让系统可以重新利用它。
Go 的 GC 是怎么工作的?
Go 使用一种叫 标记-清除算法 的方式进行垃圾回收,主要分为两步:
1. 标记阶段(Mark)
这个阶段的任务是 找到正在使用的内存(也叫“活跃对象”)。
- Go 会从“根对象”(比如全局变量、当前函数的局部变量)开始,沿着它们的引用关系,找到所有程序还能访问到的内存。
- 这些内存会被标记为“活跃”,表示它们不能被回收。
2. 清除阶段(Sweep)
这个阶段的任务是 回收未使用的内存。
- 在标记阶段中,未被标记的对象被认为是“垃圾”,它们占用的内存就会被回收。
- 回收的内存会被返回给 Go 的内存分配器,以便程序接下来可以继续使用。
举个例子
假设程序中有一个 map
,它占用了一些内存。你删除了 map
或者不再使用它。那么这些内存就成了“垃圾”。
-
标记阶段:
- Go 的垃圾回收器会找到哪些内存仍然被程序使用。
- 如果有一些内存不再被使用(比如你已经删掉的
map
),它会被标记为“垃圾”。
-
清除阶段:
- Go 会释放这些垃圾内存,让系统可以重新使用它们。
Go 的垃圾回收有什么特点?
1. 它是并发的
- 垃圾回收器和你的程序是同时运行的。
- 这意味着当垃圾回收器在工作时,你的程序可以继续运行,不会完全停下来。
2. 停顿时间很短
- 垃圾回收有时需要暂停程序(叫 Stop-the-World),但 Go 尽量把这个暂停时间压缩到几毫秒甚至更短,几乎不会影响程序性能。
3. 写屏障
- 当你在程序中修改内存时,GC 会记住你修改了哪些部分。这种技术叫 写屏障。
- 写屏障的作用是确保垃圾回收器知道哪些内存是新分配或被修改的,防止漏掉。
内存什么时候真正释放?
- 当垃圾回收完成后,不再使用的内存会被标记为可用,并返回给 Go 的内存管理器。
- 这些内存可能不会立即返回给操作系统,而是被留在程序内部,用于以后的内存分配。
- 如果程序内存压力变大,Go 会将一部分内存还给操作系统。
如何控制 GC 行为?
GOGC 参数
GOGC
是一个环境变量,用来控制垃圾回收的频率。
GOGC=100
(默认值):表示当堆内存的使用量增长到原来的 100%(两倍)时,触发一次 GC。GOGC=50
:表示当堆内存的使用量增长到原来的 50% 时触发 GC(回收更频繁)。GOGC=0
:关闭自动 GC(不推荐)。
你可以这样设置:
GOGC=50 ./your_program
一个简单的代码例子
package main
import (
"fmt"
"runtime"
)
func main() {
// 打印当前内存使用情况
printMemStats("Start")
// 分配大量内存
data := make([]byte, 100*1024*1024) // 分配 100 MB
printMemStats("After allocation")
// 手动触发垃圾回收
data = nil // 让 data 不再引用内存
runtime.GC() // 手动触发 GC
printMemStats("After GC")
}
func printMemStats(stage string) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("%s: Alloc = %v KB, Sys = %v KB\n", stage, m.Alloc/1024, m.Sys/1024)
}
运行结果可能是:
Start: Alloc = 512 KB, Sys = 2048 KB
After allocation: Alloc = 102400 KB, Sys = 105472 KB
After GC: Alloc = 512 KB, Sys = 2048 KB
解释:
- 在分配内存后,
Alloc
显示程序当前使用的内存增大。 - 在手动触发 GC 后,
Alloc
减小,表示垃圾内存被回收。
总结
- Go 的垃圾回收是通过 标记-清除算法 实现的,分为两个阶段:
- 标记阶段:找到所有活跃对象。
- 清除阶段:回收不再使用的内存。
- 它是并发运行的,不会明显阻塞程序。
- 通过
GOGC
参数可以调整垃圾回收的频率。 - 如果内存没有被频繁使用,Go 会将部分内存还给操作系统。