登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go Green Tea GC 开启后怎样比较实际堆扫描开销

来源:17golang原创

时间:2026-11-13 22:57:33 360浏览 收藏

如果你把 Go 1.25 的 GOEXPERIMENT=greenteagc 或 Go 1.26 的默认 Green Tea GC 打开,最容易犯的错误是只跑一次压测,然后拿总耗时下结论。Green Tea 优化的是标记和扫描阶段的局部性与 CPU 可扩展性;真正的收益取决于活跃堆、指针形状、分配速率和工作负载是否持续制造 GC 压力。

官方地址:https://go.dev/blog/greenteagc

版本说明:https://go.dev/doc/go1.26

比较实际堆扫描开销时,至少同时记录扫描相关指标、GC CPU、活跃堆和业务延迟;用同一份代码构建两个对照二进制,重复多轮后再解释差异。一次运行的总耗时只能说明结果,不能说明 GC 为什么变快或没有变快。

先把 Green Tea 的比较边界固定下来

Go 官方介绍 Green Tea 时给出的核心变化是:从逐对象追踪转向按页积累和扫描对象,让相邻对象更有机会一起被访问。Go 1.25 中它以实验特性提供,构建时使用 GOEXPERIMENT=greenteagc;Go 1.26 已默认启用,并提供构建时的 GOEXPERIMENT=nogreenteagc 作为对照或回滚窗口。

因此,当前 Go 1.26 的一组可比构建可以这样写:

#!/usr/bin/env bash
set -euo pipefail

# 两个二进制必须来自同一源码、同一工具链和同一构建参数。
go build -trimpath -o ./bin/bench-green-tea ./cmd/bench

# 只关闭 Green Tea,保留其它构建输入,作为实验对照。
GOEXPERIMENT=nogreenteagc go build -trimpath -o ./bin/bench-no-green-tea ./cmd/bench

# 让运行阶段的输入一致;GODEBUG 只打开 GC 事件输出,不改变工作负载。
GODEBUG=gctrace=1 ./bin/bench-green-tea -duration=60s 2>green-tea.gctrace
GODEBUG=gctrace=1 ./bin/bench-no-green-tea -duration=60s 2>no-green-tea.gctrace

如果你仍在 Go 1.25 上做实验,实验构建可以使用 GOEXPERIMENT=greenteagc,再用不设置该变量的构建作为对照。不要把构建时的 GOEXPERIMENT 和运行时的 GODEBUG 当成同一种开关:前者决定二进制使用哪种运行时实现,后者用于打开运行时诊断。

Green Tea GC、堆页、对象扫描批次、标记工作、缓存局部性与 GC CPU 开销的静态关系图
图1:Green Tea GC 的扫描单位、堆页和 GC CPU 成本之间的静态结构说明图,不是运行截图。

工作负载要主动制造堆扫描差异

基准不能只有一个“分配很多对象”的循环。至少准备两类负载:一类是大量小对象、对象之间存在较稳定指针关系,另一类是大块字节缓冲或低指针密度数据。前者更容易让扫描工作在页内形成批次,后者可能主要受分配、复制或业务处理影响,不能预设一定会得到同样的 GC 收益。

package main

import (
	"runtime"
	"time"
)

type node struct {
	next *node
	data [32]byte
}

func pointerRichLoad(stop time.Time) {
	for time.Now().Before(stop) {
		root := &node{}
		cur := root
		for i := 0; i 

这段代码不是为了制造“越快越好”的结果,而是为了让实验有两个明确的堆形状。真实项目应把自己的请求、缓存、序列化或队列路径放进基准,示例负载只能帮助你检查采集链路是否工作。不要在两个二进制之间改变并发度、输入规模、CPU 配额、内存限制或请求分布。

用 runtime/metrics 记录可解释的指标

GODEBUG=gctrace=1 会在每次 GC 输出一行摘要,适合观察周期、堆大小和 GC 阶段时间,但格式可能随版本变化。更适合写入对比表的是 runtime/metrics:/gc/scan/heap:bytes 表示可扫描堆空间,/gc/heap/live:bytes 表示上一轮 GC 标记出的活跃堆,/gc/cycles/total:gc-cycles 则可以帮助把总量换算成每轮或每秒的尺度。

package main

import (
	"fmt"
	"runtime/metrics"
)

func readGCShape() {
	samples := []metrics.Sample{
		{Name: "/gc/scan/heap:bytes"},
		{Name: "/gc/heap/live:bytes"},
		{Name: "/gc/cycles/total:gc-cycles"},
	}

	// 一次读取同一时刻的指标,避免逐个读取造成难以解释的时间偏移。
	metrics.Read(samples)
	for _, sample := range samples {
		if sample.Value.Kind() != metrics.KindUint64 {
			// 指标类型变化时不把错误值当成有效的扫描量。
			continue
		}
		fmt.Printf("%s=%d\n", sample.Name, sample.Value.Uint64())
	}
}

注意“可扫描堆空间”和“本轮实际扫描字节”不是同一个概念。前者是堆形状的重要输入,后者还受到 GC 周期、对象可达性和运行时内部工作分配影响。对比时可以把采样点和 GC 周期一起记录,再计算每轮扫描量的变化;不要把一个时刻的绝对值直接当成总 GC 成本。

把 GC CPU 与业务结果放在同一张表

Green Tea 的收益首先应体现在 GC 相关 CPU 成本下降或单位工作量的 GC 成本下降,而不是简单地追求更少的 GC 次数。建议每个构建至少重复 5 到 10 轮,并记录下面这些列:

维度建议记录避免误读
扫描工作/gc/scan/heap:bytes、每轮 GC 或每秒的扫描量不要把当前可扫描堆当成一次 GC 的全部工作
GC CPUgctrace 阶段 CPU、CPU profile 中的 GC 标记函数占比空闲 P 上的后台 GC 时间与业务忙时的影响要分开解释
堆状态活跃堆、堆目标、分配速率、GC 周期数只看堆峰值无法判断扫描是否更高效
服务结果吞吐、平均延迟、p95/p99、错误率GC 变快但业务锁或 I/O 成为瓶颈时,总耗时未必变化

如果需要看累计 CPU 归因,可以用同一套输入运行 CPU profile,并关注 runtime.gcBgMarkWorker、runtime.gcAssistAlloc 与 runtime.mallocgc 的累计占比。它们是解释“GC 是否吃掉了 CPU”的证据,不能替代端到端延迟和吞吐数据。

greenteagc 构建、nogreenteagc 构建、gctrace、heapScanWork、GC CPU、活跃堆和基准负载的静态比较关系图
图2:两个构建、运行指标与工作负载之间的静态比较关系图;图中的 heapScanWork 是解释扫描工作的概念标签,不是伪造的运行结果。

按堆形状解释为什么收益不同

官方 Green Tea 文章明确提醒,收益不是所有程序都相同。对象大小和对象图结构比较规律时,按页积累扫描工作更容易提高局部性;如果一个页上经常只能找到一个待扫描对象,积累本身可能抵消一部分收益。也就是说,看到两个服务的 GC CPU 差异,不要直接归因于“Green Tea 在服务 A 上有效、在服务 B 上无效”,先确认两者的活跃堆结构和分配路径是否相似。

我会把结果分成三层判断:

  • 扫描成本下降且业务指标改善:说明工作负载确实受到 GC 扫描影响,适合继续保留默认配置。
  • 扫描成本下降但业务指标不变:说明 GC 不是当前瓶颈,收益可能被锁、调度、网络或序列化遮住。
  • 结果接近或出现回归:先检查样本量、CPU 频率、负载稳定性和堆形状,再决定是否报告问题;不要用调高 GOGC 的方式制造“更少 GC”的假象。

常见对比错误

只跑一次基准就能判断吗?

不能。GC 与调度、缓存和系统噪声有耦合,至少需要固定环境并重复多轮,最后比较中位数和离散程度。一次运行适合发现采集链路是否能工作,不适合做发布决策。

Green Tea 开启后要改 GOGC 吗?

不要为了对比而同时改变 GOGC。先用相同 GOGC 观察扫描和 CPU 差异,再单独研究内存与 CPU 的取舍。否则你无法判断变化来自收集器实现还是来自更长的 GC 周期。

为什么 gctrace 看不到 heapScanWork 这个字段?

heapScanWork 是运行时内部用于累计堆扫描工作的概念,普通 gctrace=1 输出并不会承诺直接打印这个字段。面向应用的比较应使用官方公开的 runtime/metrics 扫描指标、GC 周期、CPU profile 和业务结果,不要解析未承诺稳定的内部日志字段。

Go 1.26 还需要一直设置 greenteagc 吗?

Go 1.26 已默认启用 Green Tea;对照实验可以用 GOEXPERIMENT=nogreenteagc 构建另一个二进制。这个退出开关是迁移和定位问题的窗口,是否继续保留要依据相同负载下的测量结果与兼容性反馈。

最后可以把结论压缩成一句话:Green Tea GC 是否“更省”,要看每单位业务工作对应的扫描量和 GC CPU,而不是只看 GC 次数或一次压测总耗时。固定构建输入,准备不同堆形状的负载,同时记录扫描指标、活跃堆、GC CPU 和延迟,才能把收集器的实际收益与业务噪声分开。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>