<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>GC on 止语Lab</title>
        <link>https://www.wujiachen.com.cn/tags/gc/</link>
        <description>Recent content in GC on 止语Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Wed, 08 Apr 2026 21:51:59 +0800</lastBuildDate><atom:link href="https://www.wujiachen.com.cn/tags/gc/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>Go GC 十年：一部延迟战争史</title>
            <link>https://www.wujiachen.com.cn/posts/go-gc-deep-dive/</link>
            <pubDate>Wed, 08 Apr 2026 21:50:00 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/go-gc-deep-dive/</guid>
            <description>&lt;img src=&#34;https://www.wujiachen.com.cn/&#34; alt=&#34;Featured image of post Go GC 十年：一部延迟战争史&#34; /&gt;&lt;p&gt;&lt;img alt=&#34;封面&#34; class=&#34;gallery-image&#34; data-flex-basis=&#34;565px&#34; data-flex-grow=&#34;235&#34; height=&#34;672&#34; loading=&#34;lazy&#34; sizes=&#34;(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px&#34; src=&#34;https://img.wujiachen.com.cn/go-gc-deep-dive/cover.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast&#34; srcset=&#34;https://www.wujiachen.com.cn/cover_4336368987607363087_hu_ce79b1228ca53255.png 800w, https://img.wujiachen.com.cn/go-gc-deep-dive/cover.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast 1584w&#34; width=&#34;1584&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;2014 年，Go 的垃圾回收器还在用最原始的 STW 标记清除。每次回收，整个程序停 300 毫秒。对于一个 Web 服务来说，300 毫秒的停顿意味着什么？意味着用户的请求超时，监控告警亮红，SRE 值班电话响起。&lt;/p&gt;&#xA;&lt;p&gt;Go 团队决定动手。他们花十年时间，把 STW 停顿从 300 毫秒压到亚毫秒——这场仗打了五步：并发标记、混合写屏障、GOGC 调优、GOMEMLIMIT 兜底、Green Tea 探路。但数字背后，是一连串的取舍——每一步&amp;quot;选了什么&amp;quot;都不如&amp;quot;没选什么&amp;quot;更值得理解。&lt;/p&gt;&#xA;&lt;h2 id=&#34;一go-15并发三色标记从-300ms-到亚毫秒&#34;&gt;&lt;a href=&#34;#%e4%b8%80go-15%e5%b9%b6%e5%8f%91%e4%b8%89%e8%89%b2%e6%a0%87%e8%ae%b0%e4%bb%8e-300ms-%e5%88%b0%e4%ba%9a%e6%af%ab%e7%a7%92&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;一、Go 1.5：并发三色标记——从 300ms 到亚毫秒&#xA;&lt;/h2&gt;&lt;p&gt;Go 1.5 之前的标记清除算法，工作方式很粗暴：暂停程序，扫描所有存活对象，清除死对象，恢复程序。暂停期间，你的代码一行都不执行。&lt;/p&gt;&#xA;&lt;p&gt;三色标记打破了这个僵局。它把标记过程拆成三个颜色：白色=未访问，灰色=已发现但子对象未处理，黑色=已处理完毕。算法从根对象出发，把可达对象标灰；然后逐个处理灰色对象——把它的子对象标灰，自己标黑。处理完所有灰色对象后，剩下的白色对象就是垃圾。&lt;/p&gt;&#xA;&lt;p&gt;关键变化是：标记过程可以和用户代码并发执行。GC 不再需要独占 CPU，STW 只剩初始和收尾两个极短的阶段。&lt;/p&gt;&#xA;&lt;p&gt;Go 1.5 之后，STW 降到 100-300 微秒。从 300 毫秒到亚毫秒，三个数量级的跨越。&lt;/p&gt;&#xA;&lt;p&gt;但并发标记引入了新问题：用户代码在 GC 运行期间可能修改指针，导致漏标。解决方案是写屏障——每次指针写入都被 GC 记录。Go 1.5 采用的是 Dijkstra 式插入写屏障：只要新指针被写入，GC 就把它记录下来。写屏障是并发标记的代价，也是后续所有演进的起点。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img alt=&#34;从 STW 标记清除到并发三色标记&#34; class=&#34;gallery-image&#34; data-flex-basis=&#34;430px&#34; data-flex-grow=&#34;179&#34; height=&#34;768&#34; loading=&#34;lazy&#34; sizes=&#34;(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px&#34; src=&#34;https://img.wujiachen.com.cn/go-gc-deep-dive/ch1-stw-to-concurrent.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast&#34; srcset=&#34;https://www.wujiachen.com.cn/ch1-stw-to-concurrent_13399945947881856409_hu_bc10436ecba96f53.png 800w, https://img.wujiachen.com.cn/go-gc-deep-dive/ch1-stw-to-concurrent.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast 1376w&#34; width=&#34;1376&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;写屏障解决了并发标记的漏标问题，但它自己也有副作用——而且还不小。下一场仗，就是冲着这个副作用来的。&lt;/p&gt;&#xA;&lt;h2 id=&#34;二go-18混合写屏障消灭栈重扫描&#34;&gt;&lt;a href=&#34;#%e4%ba%8cgo-18%e6%b7%b7%e5%90%88%e5%86%99%e5%b1%8f%e9%9a%9c%e6%b6%88%e7%81%ad%e6%a0%88%e9%87%8d%e6%89%ab%e6%8f%8f&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;二、Go 1.8：混合写屏障——消灭栈重扫描&#xA;&lt;/h2&gt;&lt;p&gt;三色标记的写屏障最初用的是 Dijkstra 插入屏障。它有一个讨厌的副作用：GC 结束时必须重新扫描所有 goroutine 的栈，因为栈上的指针修改没有被写屏障捕获。栈重扫描需要 STW，goroutine 越多，STW 越长。&lt;/p&gt;&#xA;&lt;p&gt;Go 1.8 引入了混合写屏障：Dijkstra 插入屏障 + Yuasa 删除屏障。组合使用后，栈上的指针修改不再需要重新扫描——因为删除屏障确保被覆盖的旧指针指向的对象不会被错误回收。&lt;/p&gt;&#xA;&lt;p&gt;效果：STW 从数百微秒进一步压缩，且不再与 goroutine 数量正相关。&lt;/p&gt;&#xA;&lt;p&gt;这里的取舍很清晰：混合写屏障比纯 Dijkstra 屏障开销略高（每次写操作多做一点工作），但换来了 STW 的确定性。Go 的逻辑是：写屏障的开销分摊到每次写操作，感知不到；但 STW 的停顿集中在一次，感知强烈。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img alt=&#34;从 Dijkstra 到混合写屏障&#34; class=&#34;gallery-image&#34; data-flex-basis=&#34;321px&#34; data-flex-grow=&#34;133&#34; height=&#34;896&#34; loading=&#34;lazy&#34; sizes=&#34;(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px&#34; src=&#34;https://img.wujiachen.com.cn/go-gc-deep-dive/ch2-hybrid-write-barrier.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast&#34; srcset=&#34;https://www.wujiachen.com.cn/ch2-hybrid-write-barrier_9341919284420401974_hu_fb575e264b9d7fe.png 800w, https://img.wujiachen.com.cn/go-gc-deep-dive/ch2-hybrid-write-barrier.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast 1200w&#34; width=&#34;1200&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;STW 的问题基本解决了。但延迟战争不只是 STW——还有一个更隐蔽的敌人：GC 触发时机。&lt;/p&gt;&#xA;&lt;h2 id=&#34;三gogc内存换-cpu-的旋钮&#34;&gt;&lt;a href=&#34;#%e4%b8%89gogc%e5%86%85%e5%ad%98%e6%8d%a2-cpu-%e7%9a%84%e6%97%8b%e9%92%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;三、GOGC：内存换 CPU 的旋钮&#xA;&lt;/h2&gt;&lt;p&gt;理解 Go GC 的调优，先理解 GOGC。&lt;/p&gt;&#xA;&lt;p&gt;GOGC 控制的是 GC 触发的时机。公式很简单：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;NextGC = LiveHeap × (1 + GOGC/100)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;GOGC=100（默认值）意味着：当堆增长到存活对象的 2 倍时触发 GC。GOGC=200 意味着 3 倍，GOGC=50 意味着 1.5 倍。&lt;/p&gt;&#xA;&lt;p&gt;GOGC 本质是一个&amp;quot;内存换 CPU&amp;quot;的旋钮。GOGC 越大，GC 频率越低，CPU 省了，但堆更大。GOGC 越小，GC 更积极，堆小了，但 CPU 开销上升。&lt;/p&gt;&#xA;&lt;p&gt;我跑了一组实测：10MB 存活堆，1000 万次 64B 短生命周期分配，&lt;code&gt;GODEBUG=gctrace=1&lt;/code&gt; 采集数据。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;GOGC&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;GC 次数&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;GC CPU 占比&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;总耗时&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;50&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;4 次&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;5.3%&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;4.8ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;100&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;2 次&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;4.8%&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;3.4ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;200&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;1 次&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;4.4%&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;2.6ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;off&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;0 次&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;0%&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;2.4ms，但内存不受控&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;GOGC=50 比 GOGC=200 多跑了 3 次 GC，耗时多了 85%。数字摆在这里：GOGC 小→GC 频繁→CPU 贵但堆小；GOGC 大→GC 懒惰→CPU 省但堆大。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img alt=&#34;GOGC 旋钮：内存换 CPU&#34; class=&#34;gallery-image&#34; data-flex-basis=&#34;240px&#34; data-flex-grow=&#34;100&#34; height=&#34;1024&#34; loading=&#34;lazy&#34; sizes=&#34;(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px&#34; src=&#34;https://img.wujiachen.com.cn/go-gc-deep-dive/ch3-gogc-dial.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast&#34; srcset=&#34;https://www.wujiachen.com.cn/ch3-gogc-dial_9204369796956244949_hu_b7b4043bc36aa6e5.png 800w, https://img.wujiachen.com.cn/go-gc-deep-dive/ch3-gogc-dial.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast 1024w&#34; width=&#34;1024&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;但 GOGC 有一个关键盲区：没有上限意识。假设存活堆 10GB、GOGC=200，GC 要等堆到 30GB 才触发。如果你的容器只有 16GB 内存，进程会被 OOM Kill。&lt;/p&gt;&#xA;&lt;h3 id=&#34;gomemlimit给旋钮加安全网&#34;&gt;&lt;a href=&#34;#gomemlimit%e7%bb%99%e6%97%8b%e9%92%ae%e5%8a%a0%e5%ae%89%e5%85%a8%e7%bd%91&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;GOMEMLIMIT：给旋钮加安全网&#xA;&lt;/h3&gt;&lt;p&gt;Go 1.19 引入 GOMEMLIMIT，给 GOGC 加了硬上限：当堆接近 GOMEMLIMIT 时，GC 会自动更积极地运行，等效于动态降低 GOGC。&lt;/p&gt;&#xA;&lt;p&gt;我实测了 GOGC=off + GOMEMLIMIT=32MB 的组合——堆分配到 18MB 时 GC 仍然按预期触发，而 GOGC=off 单独使用时 GC 完全不工作。GOMEMLIMIT 确实补上了 GOGC 的盲区。&lt;/p&gt;&#xA;&lt;p&gt;对 P99 延迟的意义也很大。Ilya Brin 在生产环境发现：P50 延迟 5ms，但 P99 飙到 520ms。根因是 GC 在大堆时不够积极，偶发性全堆标记造成尾部延迟暴涨。GOMEMLIMIT 让 GC 提前介入，减少这种毛刺。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img alt=&#34;GOMEMLIMIT 安全网&#34; class=&#34;gallery-image&#34; data-flex-basis=&#34;321px&#34; data-flex-grow=&#34;133&#34; height=&#34;896&#34; loading=&#34;lazy&#34; sizes=&#34;(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px&#34; src=&#34;https://img.wujiachen.com.cn/go-gc-deep-dive/ch3-memlimit-safety-net.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast&#34; srcset=&#34;https://www.wujiachen.com.cn/ch3-memlimit-safety-net_11075895050501988598_hu_5078955269094559.png 800w, https://img.wujiachen.com.cn/go-gc-deep-dive/ch3-memlimit-safety-net.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast 1200w&#34; width=&#34;1200&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;GOGC 和 GOMEMLIMIT 解决了&amp;quot;GC 什么时候该出手&amp;quot;的问题。但延迟战争还没打完——STW 压到亚毫秒后，新的瓶颈浮出水面了。&lt;/p&gt;&#xA;&lt;h2 id=&#34;四green-tea-gc新战场cpu-cache-miss&#34;&gt;&lt;a href=&#34;#%e5%9b%9bgreen-tea-gc%e6%96%b0%e6%88%98%e5%9c%bacpu-cache-miss&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;四、Green Tea GC：新战场——CPU Cache Miss&#xA;&lt;/h2&gt;&lt;p&gt;前面的所有改进都在打同一场仗：缩短 STW。当 STW 压到亚毫秒以后，Go 团队发现延迟战争的下一个敌人不在 STW，而在 GC 的 CPU 开销本身。&lt;/p&gt;&#xA;&lt;p&gt;Go 1.5 之后的并发标记，每次 GC 都要遍历整个对象图。堆小的时候没问题，堆大了就是灾难：CPU Cache Miss 暴增，标记阶段扫描一个大堆，GC CPU 开销可能超过 20%。&lt;/p&gt;&#xA;&lt;p&gt;打个比方：逐对象扫描就像在一栋大楼里挨个敲门——每扇门都是一次内存访问，可能触发 Cache Miss。Green Tea GC 的思路是换一种敲门方式。&lt;/p&gt;&#xA;&lt;p&gt;Green Tea GC（Go 1.25 实验性引入，Go 1.26 将默认启用）改变了扫描的基本单位：从逐对象扫描改为按页扫描。它用位图标记每页中的对象存活状态，甚至利用 SIMD 指令一次处理多个对象——就像从&amp;quot;挨个敲门&amp;quot;变成&amp;quot;看楼层平面图&amp;quot;，整层楼的对象状态一目了然。&lt;/p&gt;&#xA;&lt;p&gt;从&amp;quot;对象图洪水&amp;quot;到&amp;quot;页级扫描&amp;quot;，这不是换个算法，是换了个战场。之前的延迟战争打的是 STW 时长，Green Tea 打的是 Cache 友好性。战场变了，战争性质没变：还在追求更低的延迟代价。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img alt=&#34;Green Tea GC：逐对象→按页扫描&#34; class=&#34;gallery-image&#34; data-flex-basis=&#34;430px&#34; data-flex-grow=&#34;179&#34; height=&#34;768&#34; loading=&#34;lazy&#34; sizes=&#34;(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px&#34; src=&#34;https://img.wujiachen.com.cn/go-gc-deep-dive/ch4-green-tea-scan.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast&#34; srcset=&#34;https://www.wujiachen.com.cn/ch4-green-tea-scan_11422939084957501310_hu_6a94a3e73550ed80.png 800w, https://img.wujiachen.com.cn/go-gc-deep-dive/ch4-green-tea-scan.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast 1376w&#34; width=&#34;1376&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;Green Tea 是延迟战争的新战线，但 Go GC 还有两个老问题没有解——它们不是 bug，是刻意的设计选择。&lt;/p&gt;&#xA;&lt;h2 id=&#34;五不完美的真相碎片化与无分代的代价&#34;&gt;&lt;a href=&#34;#%e4%ba%94%e4%b8%8d%e5%ae%8c%e7%be%8e%e7%9a%84%e7%9c%9f%e7%9b%b8%e7%a2%8e%e7%89%87%e5%8c%96%e4%b8%8e%e6%97%a0%e5%88%86%e4%bb%a3%e7%9a%84%e4%bb%a3%e4%bb%b7&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;五、不完美的真相：碎片化与无分代的代价&#xA;&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;碎片化&lt;/strong&gt;。Go 不做内存整理（compaction）。内存分配器把对象按 67 个大小类别管理，释放后的空间只能被同大小类别的新对象复用。大量小对象释放后，内存里满是碎片。&lt;/p&gt;&#xA;&lt;p&gt;我跑了一个实验：分配 100 万个 64B 小对象，隔一个释放一个，然后用 &lt;code&gt;runtime.ReadMemStats&lt;/code&gt; 看 Go runtime 持有的内存。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;阶段&lt;/th&gt;&#xA;          &lt;th&gt;HeapAlloc&lt;/th&gt;&#xA;          &lt;th&gt;HeapSys&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;碎片率&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;分配 100 万个 64B 对象后&lt;/td&gt;&#xA;          &lt;td&gt;91.9MB&lt;/td&gt;&#xA;          &lt;td&gt;99.4MB&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;7.6%&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;隔一释放后&lt;/td&gt;&#xA;          &lt;td&gt;293KB&lt;/td&gt;&#xA;          &lt;td&gt;99.4MB&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;&lt;strong&gt;99.7%&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;再分配 4KB 大对象后&lt;/td&gt;&#xA;          &lt;td&gt;293KB&lt;/td&gt;&#xA;          &lt;td&gt;99.4MB&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;&lt;strong&gt;99.7%&lt;/strong&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;释放了约 50 万个 64B 对象后，HeapAlloc 只有 293KB，但 HeapSys 仍然是 99.4MB——Go runtime 从系统申请了 99.4MB，你的程序只用了 293KB。再分配 4KB 大对象？不行，4KB 属于不同的 size class，放不进 64B span 的空洞。&lt;/p&gt;&#xA;&lt;p&gt;大堆缓存服务的碎片率更夸张：碎片化可能让你多花 30-50% 的内存。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img alt=&#34;碎片化与无分代的代价&#34; class=&#34;gallery-image&#34; data-flex-basis=&#34;321px&#34; data-flex-grow=&#34;133&#34; height=&#34;896&#34; loading=&#34;lazy&#34; sizes=&#34;(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px&#34; src=&#34;https://img.wujiachen.com.cn/go-gc-deep-dive/ch5-tradeoffs.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast&#34; srcset=&#34;https://www.wujiachen.com.cn/ch5-tradeoffs_11530881496690875222_hu_eda030ccde2dc973.png 800w, https://img.wujiachen.com.cn/go-gc-deep-dive/ch5-tradeoffs.png!/watermark/text/5q2i6K+tTGFi/size/20/color/666666/opacity/70/align/southeast 1200w&#34; width=&#34;1200&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;不分代&lt;/strong&gt;。分代 GC 的核心假说是&amp;quot;大多数对象朝生夕死&amp;quot;，只频繁回收新生代能大幅减少工作量。但分代需要写屏障跟踪跨代引用，这意味着每次指针写入都有额外开销——即使 GC 没在运行。Go 的设计目标是最大化应用代码的 CPU 时间，写屏障的&amp;quot;全员税&amp;quot;不可接受。所以 Go 选择每次 GC 都扫描全堆。&lt;/p&gt;&#xA;&lt;p&gt;这两个&amp;quot;不&amp;quot;——不整理、不分代——是 Go 用效率换确定性的结果。碎片化是&amp;quot;低延迟 &amp;gt; 内存效率&amp;quot;的代价，不分代是&amp;quot;mutator 吞吐 &amp;gt; GC 效率&amp;quot;的代价。&lt;/p&gt;&#xA;&lt;p&gt;Go 团队知道这些问题。他们的判断是：对于大多数 Go 服务（堆 &amp;lt;4GB），这些代价可接受。如果你的场景是大堆缓存服务，碎片化可能让你多花约 30% 的内存，这时需要评估是否值得。&lt;/p&gt;&#xA;&lt;h2 id=&#34;尾声战争仍在继续&#34;&gt;&lt;a href=&#34;#%e5%b0%be%e5%a3%b0%e6%88%98%e4%ba%89%e4%bb%8d%e5%9c%a8%e7%bb%a7%e7%bb%ad&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;尾声：战争仍在继续&#xA;&lt;/h2&gt;&lt;p&gt;从 300ms 到 0.5ms，Go GC 用十年时间打赢了 STW 这场仗。但战争远未结束。&lt;/p&gt;&#xA;&lt;p&gt;Green Tea GC 正在跟 CPU Cache Miss 较劲。碎片化问题没有银弹，Go 的态度是&amp;quot;用空间换确定性&amp;quot;。分代？短期内不会来，因为写屏障的&amp;quot;全员税&amp;quot;与 Go 的设计目标冲突。&lt;/p&gt;&#xA;&lt;p&gt;如果你在用 Go，记住这个调优思路：先用默认 GOGC=100 跑起来；遇到 P99 毛刺，设 GOMEMLIMIT 让 GC 提前出手；大堆场景关注 GC CPU 开销，必要时调整 GOGC。具体参数不重要，理解&amp;quot;为什么这样调&amp;quot;才重要。&lt;/p&gt;&#xA;&lt;p&gt;Go GC 的故事不是一个完美演进的故事，是一个&amp;quot;每一步都在取舍&amp;quot;的故事。理解它没选什么，比理解它选了什么，更能帮你做出正确的工程决策。&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
