<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>第 13 章 垃圾回收器 on Go 语言原本</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/</link><description>Recent content in 第 13 章 垃圾回收器 on Go 语言原本</description><generator>Hugo</generator><language>zh-cn</language><atom:link href="http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/index.xml" rel="self" type="application/rss+xml"/><item><title>13.1 垃圾回收的基本想法</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/basic/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/basic/</guid><description>&lt;h1 id="131-垃圾回收的基本想法"&gt;13.1 垃圾回收的基本想法&lt;/h1&gt;
&lt;p&gt;垃圾回收（GC）把程序员从手动 &lt;code&gt;free&lt;/code&gt; 中解放出来，代价是运行时要自己判断「哪些内存还有用、
哪些可以收回」。Go 的 GC 以低延迟为首要目标，它宁可牺牲一点吞吐与内存，也要把卡顿
（stop-the-world 停顿）压到亚毫秒级。这一节建立 GC 的理论坐标：可达性、标记清扫、三色抽象，
以及 Go 在 GC 设计空间里的定位。后续各节是这套基本想法的展开。&lt;/p&gt;
&lt;p&gt;在展开之前，先借用一组贯穿全章的术语。Dijkstra 等人 [Dijkstra et al., 1978] 把一个带 GC 的
程序拆成两个半独立的角色：赋值器（mutator）与回收器（collector）。赋值器即用户态代码，
它只做一件与 GC 相关的事，修改对象之间的引用关系，也就是在一张「对象图」上增删有向边；
回收器则是运行时里负责找出并收回垃圾的那部分代码。本章的全部难处，都源自这两者要同时运行。&lt;/p&gt;
&lt;h2 id="1311-可达性即存活"&gt;13.1.1 可达性即存活&lt;/h2&gt;
&lt;p&gt;GC 判断存活的依据是可达性：从一组根（root）出发，凡能沿指针到达的对象都算存活，
到达不了的就是垃圾。Go 的根集合有三类，全局变量、各 goroutine 栈上的变量、以及寄存器中
可能存放的指针。回收器从这些根出发遍历对象图，标记一路可达的对象。&lt;/p&gt;
&lt;p&gt;可达性是一个保守而安全的近似。它并不真正知道某个对象将来还会不会被读到，只知道「从根够不到的，
程序一定再也用不上」。于是「这块内存是否还有用」这个语义问题，被转化成了一个图遍历问题：
从根开始走对象图，走得到的留下，走不到的回收。&lt;/p&gt;
&lt;p&gt;这里还藏着一个 Go 与许多 C/C++ 上的回收器的分野。Go 的 GC 是精确（precise，亦称 type-accurate）
的：运行时借助编译期生成的类型信息与 span 元数据（&lt;a href="../../ch12alloc/component"&gt;12.2&lt;/a&gt;），
能准确分辨一个字（word）究竟是指针还是普通整数。保守式回收器做不到这一点，它只能把任何
「看起来像指针」的位模式都当作指针看待，因而可能误判、且无法移动对象。精确是 Go 后续许多
设计（写屏障、栈扫描）得以成立的前提。&lt;/p&gt;
&lt;h2 id="1312-标记清扫"&gt;13.1.2 标记—清扫&lt;/h2&gt;
&lt;p&gt;最经典的实现是标记—清扫（mark-sweep），由 McCarthy 在 1960 年实现 Lisp 时首次提出
[McCarthy, 1960]。它把回收分成两个阶段。标记阶段从根出发遍历对象图，给每个可达对象打上标记：&lt;/p&gt;</description></item><item><title>13.2 写屏障技术</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/barrier/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/barrier/</guid><description>&lt;h1 id="132-写屏障技术"&gt;13.2 写屏障技术&lt;/h1&gt;
&lt;p&gt;&lt;a href=".././basic"&gt;13.1&lt;/a&gt; 给出了三色抽象与并发回收的轮廓：回收器把对象从白染灰、由灰染黑，
让一道「灰色波面」在对象图上单调推进，波面扫过之处即为已确认存活。若赋值器（mutator，
即用户态代码）在波面推进时停下不动，这套抽象自洽且会正确收敛。问题恰恰在于赋值器不会停。
并发回收的根本困难是：回收器追踪对象图的同时，赋值器在改写它，两者对同一张图各执一词。&lt;/p&gt;
&lt;p&gt;这一节要回答的是：赋值器的一次指针写入，凭什么不会让回收器漏标一个存活对象。答案是&lt;strong&gt;写屏障&lt;/strong&gt;
（write barrier）：编译器在堆指针写入处插入的一小段代码，让赋值器把「我动了哪条边」告知回收器。
我们先看清并发改图为何会漏标（&lt;a href="#1321-%E5%B9%B6%E5%8F%91%E6%94%B9%E5%9B%BE%E4%B8%BA%E4%BD%95%E4%BC%9A%E6%BC%8F%E6%A0%87"&gt;13.2.1&lt;/a&gt;），由此引出两条三色不变性
（&lt;a href="#1322-%E4%B8%A4%E6%9D%A1%E4%B8%89%E8%89%B2%E4%B8%8D%E5%8F%98%E6%80%A7"&gt;13.2.2&lt;/a&gt;）；再看堵住漏标的两个屏障族（&lt;a href="#1323-%E4%B8%A4%E4%B8%AA%E5%B1%8F%E9%9A%9C%E6%97%8F-dijkstra-%E6%8F%92%E5%85%A5%E4%B8%8E-yuasa-%E5%88%A0%E9%99%A4"&gt;13.2.3&lt;/a&gt;）；
最后落到 Go 自 1.8 起沿用至今的混合写屏障（&lt;a href="#1324-go-%E7%9A%84%E6%B7%B7%E5%90%88%E5%86%99%E5%B1%8F%E9%9A%9C"&gt;13.2.4&lt;/a&gt;），
理解它为何能一举消除标记终止阶段那次代价高昂的栈重扫。&lt;/p&gt;
&lt;p&gt;需要先厘清一处常见的名词混淆。本节的「写屏障」是垃圾回收意义上的赋值器屏障，是一段&lt;strong&gt;软件&lt;/strong&gt;
逻辑，它与 CPU 层面阻止访存重排的内存屏障（memory barrier，见 &lt;a href="../../../part3concurrency/ch11sync/mem"&gt;11.9&lt;/a&gt;）
是两回事。两者唯一的交点在于：Go 的写屏障实现为保证自身在多核下的正确性，确实依赖了一处访存
顺序约束，这一点留到 &lt;a href="#1324-go-%E7%9A%84%E6%B7%B7%E5%90%88%E5%86%99%E5%B1%8F%E9%9A%9C"&gt;13.2.4&lt;/a&gt; 再谈。&lt;/p&gt;
&lt;h2 id="1321-并发改图为何会漏标"&gt;13.2.1 并发改图为何会漏标&lt;/h2&gt;
&lt;p&gt;考虑回收器正扫到一半的某个瞬间。设灰色对象 $A$ 指向白色对象 $B$，赋值器此时并发地做两件事：
把一个已被染黑的对象 $C$ 指向 $B$，再把 $A$ 到 $B$ 的那条引用抹去。于是 $B$ 现在只挂在黑色
对象 $C$ 之下。回收器认为黑色对象已扫描完毕、不会再访问，而通往 $B$ 的灰色入口又被抹掉了，
$B$ 就此从波面上消失，最终被当作垃圾错误回收。&lt;/p&gt;

&lt;script src="http://golang.design/under-the-hood/mermaid.min.js"&gt;&lt;/script&gt;
&lt;script&gt;
 window.addEventListener("DOMContentLoaded", function () {
 mermaid.initialize({
 startOnLoad: false,
 theme: "neutral",
 securityLevel: "strict",
 fontFamily: "inherit",
 });
 mermaid.run({ querySelector: ".mermaid" });
 });
&lt;/script&gt;


&lt;pre class="mermaid"&gt;flowchart LR
 subgraph before[&amp;#34;改图前&amp;#34;]
 A1[&amp;#34;A 灰&amp;#34;]:::grey --&amp;gt;|ref| B1[&amp;#34;B 白&amp;#34;]:::white
 C1[&amp;#34;C 黑&amp;#34;]:::black
 end
 subgraph after[&amp;#34;改图后：B 漏标&amp;#34;]
 A2[&amp;#34;A 灰&amp;#34;]:::grey -.-&amp;gt;|删除 ref| B2[&amp;#34;B 白&amp;#34;]:::white
 C2[&amp;#34;C 黑&amp;#34;]:::black --&amp;gt;|新增 ref| B2
 end
 before --&amp;gt; after
 classDef white fill:#fff,stroke:#333,color:#000
 classDef grey fill:#bbb,stroke:#333,color:#000
 classDef black fill:#333,stroke:#000,color:#fff&lt;/pre&gt;
&lt;p&gt;把这一瞬拆开看，漏标恰由两件事&lt;strong&gt;同时&lt;/strong&gt;发生才成立。[Wilson, 1992] 将其精确为两个条件：&lt;/p&gt;</description></item><item><title>13.3 触发频率及其调步算法</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/pacing/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/pacing/</guid><description>&lt;h1 id="133-触发频率及其调步算法"&gt;13.3 触发频率及其调步算法&lt;/h1&gt;
&lt;p&gt;&lt;a href=".././barrier"&gt;13.2&lt;/a&gt; 交代了写屏障如何让并发标记与赋值器（mutator）共处，保证「正在回收的同时
还能继续分配」。这就留下了一个时间问题：什么时候该启动下一轮 GC？太晚，堆已经涨到不可接受的
体积；太早，回收频繁地白白烧掉 CPU。回答这个问题的，是 Go 从 1.5 起引入、1.18 重新设计的
&lt;strong&gt;调步器&lt;/strong&gt;（pacer）。本节先把一轮 GC 的完整时序摆出来，再讲清调步器是一个什么样的反馈控制器：
它要在赋值器持续分配的过程中，赶在堆触到目标之前完成标记。&lt;/p&gt;
&lt;h2 id="1331-一轮-gc-的时序"&gt;13.3.1 一轮 GC 的时序&lt;/h2&gt;
&lt;p&gt;把一轮 GC 拆开看，它在 &lt;code&gt;_GCoff&lt;/code&gt;、&lt;code&gt;_GCmark&lt;/code&gt;、&lt;code&gt;_GCmarktermination&lt;/code&gt; 三个阶段间流转，其间夹着
两段极短的 STW（stop-the-world）：&lt;/p&gt;

&lt;pre class="mermaid"&gt;stateDiagram-v2
 [*] --&amp;gt; GCoff
 GCoff --&amp;gt; STW1: heap_live ≥ trigger
 note right of STW1
 极短 STW：开启写屏障
 扫描栈与全局根
 end note
 STW1 --&amp;gt; GCmark: 切入并发标记
 state GCmark {
 background: 后台标记 worker（约 25% CPU）
 assist: 赋值器 mark assist（分配快者被征用）
 background --&amp;gt; assist
 assist --&amp;gt; background
 }
 GCmark --&amp;gt; STW2: 标记队列排空
 note right of STW2
 极短 STW：标记终止
 关闭写屏障并结算
 end note
 STW2 --&amp;gt; Sweep: 转入并发清扫
 Sweep --&amp;gt; GCoff: 清扫随分配惰性推进
 GCoff --&amp;gt; [*]&lt;/pre&gt;
&lt;p&gt;整轮的脉络是这样：堆上的存活字节 &lt;code&gt;heap_live&lt;/code&gt; 涨到调步器算出的触发线 &lt;code&gt;trigger&lt;/code&gt;，第一段 STW
开启写屏障并扫描根（goroutine 栈与全局变量），随即放开世界进入并发标记。标记阶段，后台
worker 占用约 25% 的 &lt;code&gt;GOMAXPROCS&lt;/code&gt; 持续标记，分配过快的 goroutine 还会被征去做 mark assist
（&lt;a href="#1335-%E8%B5%8B%E5%80%BC%E5%99%A8%E5%8D%8F%E5%8A%A9-mark-assist"&gt;13.3.5&lt;/a&gt;）。标记队列排空后进入第二段 STW 做标记终止，关闭
写屏障并结算本轮统计量，之后转入并发清扫，清扫工作随后续分配惰性摊销（&lt;a href=".././sweep"&gt;13.5&lt;/a&gt;）。
两段 STW 都被压到亚毫秒量级，绝大部分回收工作与赋值器并发进行，这正是 Go 低延迟 GC 的形态。&lt;/p&gt;</description></item><item><title>13.4 扫描标记与标记辅助</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/mark/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/mark/</guid><description>&lt;h1 id="134-扫描标记与标记辅助"&gt;13.4 扫描标记与标记辅助&lt;/h1&gt;
&lt;p&gt;标记是 GC 的主体工作：从根（全局变量、各 goroutine 的栈、寄存器）出发，沿指针把所有可达对象
置黑（&lt;a href=".././basic"&gt;13.1&lt;/a&gt; 的三色抽象）。一个朴素的实现会停下整个世界、单线程地走完对象图，这正是
早年 Go 不堪的停顿来源。go1.5 之后，标记被改造成两件事同时成立的形态：它&lt;strong&gt;与用户程序并发&lt;/strong&gt;地推进，
又能在用户分配过快时&lt;strong&gt;把代价分摊&lt;/strong&gt;到分配者头上。这一节回答三个问题：标记如何并行展开而不互相踩踏；
扫描一个对象时怎么知道它的哪些字是指针；以及当用户一边分配、标记一边追赶时，凭什么保证标记不会
被永远甩在后面。&lt;/p&gt;
&lt;h2 id="1341-谁来标记后台-worker-与-25-的预算"&gt;13.4.1 谁来标记：后台 worker 与 25% 的预算&lt;/h2&gt;
&lt;p&gt;标记的主力是一组后台 goroutine，&lt;code&gt;gcBgMarkWorker&lt;/code&gt;。运行时为每个 P 准备一个，但并不让它们全速
空转抢占 CPU，而是把整个标记阶段的 CPU 预算固定在 &lt;strong&gt;GOMAXPROCS 的 25%&lt;/strong&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// runtime/mgcpacer.go（速写）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;const&lt;/span&gt; gcBackgroundUtilization = 0.25 &lt;span style="color:#008000"&gt;// 标记阶段后台 GC 的固定 CPU 占用目标&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;为什么是一个固定比例，而不是「有多少干多少」？这是调步器（&lt;a href=".././pacing"&gt;13.3&lt;/a&gt;）的核心约定：把 GC
对吞吐的侵蚀钉在一个可预测的水平上，剩下的 75% 留给用户程序。25% 这个数字本身是工程折中，高了伤
吞吐，低了标记追不上分配（追不上的缺口再由下文的标记辅助补齐）。&lt;/p&gt;
&lt;p&gt;固定预算落到具体 worker 上，会按 &lt;code&gt;GOMAXPROCS × 0.25&lt;/code&gt; 算出需要几个「专职」worker，余数用一个「零工」
worker 凑齐，于是有了三种工作模式：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// 三种后台标记 worker 模式（runtime）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcMarkWorkerDedicatedMode &lt;span style="color:#008000"&gt;// 专职：整个标记阶段独占一个 P，不被抢占&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcMarkWorkerFractionalMode &lt;span style="color:#008000"&gt;// 零工：补齐 25% 预算的小数部分，到点即让出&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gcMarkWorkerIdleMode &lt;span style="color:#008000"&gt;// 闲时：P 没有用户 goroutine 可跑时顺手标记&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;举例：&lt;code&gt;GOMAXPROCS = 8&lt;/code&gt; 时 $8 \times 0.25 = 2$，于是有 2 个专职 worker 各占一个 P；若是
$5 \times 0.25 = 1.25$，则 1 个专职 worker 加一个补齐 $0.25$ 的零工 worker。专职 worker 在标记期间不被调度
器抢占，零工 worker 跑到 &lt;code&gt;fractionalUtilizationGoal&lt;/code&gt; 这个比例就主动让出，闲时 worker 则只在某个 P
找不到用户活儿干时被 &lt;code&gt;findRunnable&lt;/code&gt;（&lt;a href="../../../part3concurrency/ch09sched/exec"&gt;9.5&lt;/a&gt;）顺手唤起。三者
合起来既稳住了 25% 的均值，又不浪费空闲算力。&lt;/p&gt;</description></item><item><title>13.5 清扫与位图</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/sweep/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/sweep/</guid><description>&lt;h1 id="135-清扫与位图"&gt;13.5 清扫与位图&lt;/h1&gt;
&lt;p&gt;标记（&lt;a href=".././mark"&gt;13.4&lt;/a&gt;）一旦结束，仍为白色的对象就是不可达的垃圾。把它们占据的内存收回、
重新交给分配器，是回收的最后一步：&lt;strong&gt;清扫&lt;/strong&gt;（sweep）。如果照搬教科书里「遍历堆、逐个释放死对象」
的写法，清扫的开销会与死对象的数量成正比，在一个动辄数百万对象的堆上，这是一笔不小的账。Go 的
清扫绕开了这笔账，它有三处值得讲清楚的设计：清扫靠&lt;strong&gt;位图翻转&lt;/strong&gt;而非逐对象处理完成；它与用户程序
&lt;strong&gt;并发&lt;/strong&gt;进行，且&lt;strong&gt;惰性&lt;/strong&gt;地把开销摊到分配路径上；以及它&lt;strong&gt;不移动对象&lt;/strong&gt;，因而接受碎片、放弃整理。
这三点都不是偶然，它们各自对应一处明确的工程取舍，本节逐一拆开。&lt;/p&gt;
&lt;h2 id="1351-清扫即位图翻转"&gt;13.5.1 清扫即位图翻转&lt;/h2&gt;
&lt;p&gt;回收的高效，根子在分配器的数据结构上。第 &lt;a href="../../ch12alloc/component"&gt;12.2&lt;/a&gt; 节讲过，每个 span
被切成同一尺寸类的等大槽位，并配两份位图：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;allocBits&lt;/code&gt;：哪些槽&lt;strong&gt;已分配&lt;/strong&gt;，分配路径据此找空槽。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gcmarkBits&lt;/code&gt;：本轮标记中哪些槽被置位，即&lt;strong&gt;存活&lt;/strong&gt;，由标记阶段写入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;清扫一个 span，核心动作出奇地短，把标记位图&lt;strong&gt;直接当作&lt;/strong&gt;新的分配位图：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;13
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;14
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;15
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// mspan.sweep 的核心（src/runtime/mgcsweep.go，速写）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; (s *mspan) sweep(preserve &lt;span style="color:#2b91af"&gt;bool&lt;/span&gt;) &lt;span style="color:#2b91af"&gt;bool&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// ……此前已处理 finalizer / weak / profile 等 special 记录……&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nalloc := uint16(s.countAlloc()) &lt;span style="color:#008000"&gt;// gcmarkBits 中置位的槽数 = 存活数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; s.allocCount = nalloc
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; s.freeindex = 0 &lt;span style="color:#008000"&gt;// 分配游标归零，下次从头扫空槽&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 关键三行：标记位图翻身成为分配位图&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; s.allocBits = s.gcmarkBits &lt;span style="color:#008000"&gt;// 存活的继续「在用」，未标记的就此「空闲」&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; s.gcmarkBits = newMarkBits(s.nelems) &lt;span style="color:#008000"&gt;// 备一份清零的标记位图，留给下一轮 GC&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; s.refillAllocCache(0) &lt;span style="color:#008000"&gt;// 重建 allocCache 位扫描缓存（见 12.2）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// ……根据存活数决定 span 的去向，见 13.5.3……&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;翻转之后，被标记的槽在新 &lt;code&gt;allocBits&lt;/code&gt; 里仍是 1，继续算「在用」；没被标记的槽在新位图里是 0，
就此变成「空闲」，下次分配直接覆写。死对象既不需要被找出来，也不需要被逐个清掉。源码注释把
这种状态说得很准：一个未标记又未被重新分配的槽，处境「就像挂在自由表上一样」（analogous to
being on a freelist）。这正是本节标题里「免清扫」（sweep-free）的含义,死对象无需任何显式
「清理」动作，它们的槽只是在位图翻转的一瞬间被重新解释为可分配。&lt;/p&gt;</description></item><item><title>13.6 标记终止阶段</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/termination/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/termination/</guid><description>&lt;h1 id="136-标记终止阶段"&gt;13.6 标记终止阶段&lt;/h1&gt;
&lt;p&gt;并发标记（&lt;a href=".././mark"&gt;13.4&lt;/a&gt;）有一个不平凡的收尾问题：如何&lt;strong&gt;判定标记已经完成&lt;/strong&gt;。在单线程的
停顿式回收里，这个问题是平凡的，标记线程把灰色队列扫空，标记就结束了。可在并发世界里，
「我的本地队列空了」从来不等于「全局没有灰色对象了」。当一个工作协程刚把自己的队列扫空，
另一个 P 上的 mutator 可能正巧执行了一次指针写入，写屏障（&lt;a href=".././barrier"&gt;13.2&lt;/a&gt;）随之染灰了
一个新对象，把它塞进了那个 P 的本地缓存。判定终止，本质上是要在一个没有全局锁、工作分散
在各 P 本地缓存里的系统中，确认一个&lt;strong&gt;全局性质&lt;/strong&gt;：灰色集合为空，且未来不会再产生灰色对象。&lt;/p&gt;
&lt;p&gt;本节回答三件事：终止检测算法如何在无全局协调的前提下断定标记完成（&lt;code&gt;gcMarkDone&lt;/code&gt;）；为此
仍需的那一次短暂 STW（&lt;code&gt;_GCmarktermination&lt;/code&gt;）做了什么、为何能做到很短；以及为什么一个周期
里始终保留着首尾两次短 STW，运行时为何选择把每次 STW 的&lt;strong&gt;工作量&lt;/strong&gt;压到与堆栈规模无关，
而不去追逐字面意义上的零停顿。&lt;/p&gt;
&lt;h2 id="1361-并发下标记完成为何难判定"&gt;13.6.1 并发下「标记完成」为何难判定&lt;/h2&gt;
&lt;p&gt;把标记工作想象成一池灰色对象，分散存放在每个 P 的本地 &lt;code&gt;gcWork&lt;/code&gt; 缓存与若干全局工作缓冲里。
工作协程不断从池中取出灰色对象、扫描、把新发现的指针染灰投回池中，直到池空。难点在于
「池空」这个判断本身是分布式的：没有哪个线程能在不停下整个世界的前提下，瞬间窥见所有 P
的本地缓存都为空。&lt;/p&gt;
&lt;p&gt;更棘手的是写屏障的存在。只要 mutator 还在运行、写屏障还开着，一次 &lt;code&gt;*p = q&lt;/code&gt; 的指针写入就
可能凭空产生一个新的灰色对象。于是终止条件不是静态的「此刻池空」，而是动态的「池空，
且没有任何在途的、尚未被观测到的工作能再次填满它」。运行时用两个计数刻画前半句：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// 标记是否已完成的快速判据（速写，runtime/mgc.go）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; gcIsMarkDone() &lt;span style="color:#2b91af"&gt;bool&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// nproc 个标记工作者全部处于等待态，且全局已无可取的标记工作&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;return&lt;/span&gt; work.nwait == work.nproc &amp;amp;&amp;amp; !gcMarkWorkAvailable()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;gcIsMarkDone&lt;/code&gt; 只是必要条件，不是充分条件。它看到的是「此刻」的快照，无法排除某个 P 的
本地缓存里还压着尚未刷新到全局、因而对其他人不可见的工作。要把必要条件升级为充分条件，
需要一道&lt;strong&gt;全局屏障&lt;/strong&gt;，强制每个 P 把本地缓存抖落出来、让所有人看见，再复查。&lt;/p&gt;</description></item><item><title>13.7 安全点分析</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/safe/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/safe/</guid><description>&lt;h1 id="137-安全点分析"&gt;13.7 安全点分析&lt;/h1&gt;
&lt;p&gt;标记阶段（&lt;a href=".././mark"&gt;13.4&lt;/a&gt;）要扫描一个 goroutine 的栈，找出栈上所有指向堆的指针，把它们当作
GC 根加入灰色队列。这件事看似直白，实则藏着一个不易察觉的前提：&lt;strong&gt;栈上的一个机器字，到底是不是
指针？&lt;/strong&gt; 一个 64 位的字，可能是一个堆地址，也可能是一个恰好落在堆地址区间里的整数、一段被半拆
出来的浮点数、或一个尚未写入的垃圾值。若把非指针误当指针，会平白吊住一块本该回收的内存；若把
指针漏判成整数，则会回收掉仍在使用的对象,后者是致命的内存破坏。&lt;/p&gt;
&lt;p&gt;要分辨这两者，运行时需要一份「这一刻，栈上哪些位置存着指针」的精确说明。这份说明并非时时存在：
寄存器与栈槽的指针 / 非指针归属，随着指令逐条执行不断变化。编译器只在某些&lt;strong&gt;程序点&lt;/strong&gt;上为它生成
快照，这些点就是&lt;strong&gt;安全点&lt;/strong&gt;（safe-point）。GC 要精确扫描栈，就必须先把目标 goroutine 停在一个安全
点上。安全点这个概念在调度的抢占一节（&lt;a href="../../../part3concurrency/ch09sched/preemption"&gt;9.7&lt;/a&gt;）已从
「何时可以打断一个 goroutine」的角度系统讲过；本节从 GC 的角度补足它的另一半含义：&lt;strong&gt;安全点是栈
上指针信息精确可读的那些瞬间&lt;/strong&gt;。这两半其实是同一件事，本节最后会让它们合流。&lt;/p&gt;
&lt;h2 id="1371-栈图编译期记下每个安全点的指针布局"&gt;13.7.1 栈图：编译期记下每个安全点的指针布局&lt;/h2&gt;
&lt;p&gt;编译器为每个函数生成若干&lt;strong&gt;栈图&lt;/strong&gt;（stack map，又称指针位图 pointer map）。一份栈图是一个位向量
（&lt;code&gt;bitvector&lt;/code&gt;），第 $i$ 位为 1 表示「栈帧中第 $i$ 个字长的槽此刻存着一个指针」。同一个函数在不同
程序点上的栈布局不同，于是编译器为每个安全点各记一份栈图，并用一张 &lt;code&gt;PCDATA&lt;/code&gt; 表把「程序计数器
PC」映射到「该 PC 对应哪一份栈图」。运行时扫描某一帧时，拿该帧将要返回到的 PC 去查表，便得到
这一帧此刻的指针布局。&lt;/p&gt;
&lt;p&gt;运行时读取栈图的入口是 &lt;code&gt;getStackMap&lt;/code&gt;。它对一帧返回三样东西：局部变量的指针位图、参数的指针位图、
以及该帧内的&lt;strong&gt;栈对象&lt;/strong&gt;（stack object）记录:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 6
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 7
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 8
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt; 9
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;10
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;11
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;12
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;13
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;14
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;15
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;16
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;17
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// getStackMap：用帧的返回 PC 查出本帧的指针布局（速写）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; (frame *stkframe) getStackMap(debug &lt;span style="color:#2b91af"&gt;bool&lt;/span&gt;) (locals, args bitvector, objs []stackObjectRecord) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; targetpc := frame.continpc &lt;span style="color:#008000"&gt;// 本帧将返回到的 PC&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; f := frame.fn
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 回退到 CALL 指令处再查表：安全点记录的是「调用点」上的布局&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; targetpc--
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pcdata := pcdatavalue(f, abi.PCDATA_StackMapIndex, targetpc)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 局部变量位图：funcdata 里取出本函数的栈图表，按 pcdata 索引取第几份&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stkmap := (*stackmap)(funcdata(f, abi.FUNCDATA_LocalsPointerMaps))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; locals = stackmapdata(stkmap, pcdata)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// 参数位图：另一张表 FUNCDATA_ArgsPointerMaps，同样按 pcdata 索引&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#008000"&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#00f"&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;两个细节值得点出。其一，&lt;code&gt;targetpc--&lt;/code&gt;,查表用的不是返回地址本身，而是退一格落到 &lt;code&gt;CALL&lt;/code&gt; 指令上。
原因是被打断的帧总是停在「调用了某个下层函数、正等它返回」的位置，编译器恰是在调用点处记录了
栈布局，所以要对齐到调用点而非调用后的下一条指令。其二，指针信息分两类来源：&lt;code&gt;FUNCDATA_LocalsPointerMaps&lt;/code&gt;
管局部变量，&lt;code&gt;FUNCDATA_ArgsPointerMaps&lt;/code&gt; 管参数区，两者用同一个 &lt;code&gt;pcdata&lt;/code&gt; 索引各取一份位图。&lt;/p&gt;</description></item><item><title>13.8 分代假设与代际回收</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/generational/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/generational/</guid><description>&lt;h1 id="138-分代假设与代际回收"&gt;13.8 分代假设与代际回收&lt;/h1&gt;
&lt;p&gt;学过 JVM 或 .NET 的人常会问一个尖锐的问题：Go 为什么&lt;strong&gt;不做分代 GC&lt;/strong&gt;？分代是 Java、.NET 等
主流托管运行时 GC 的标配，被视为高效回收的关键，几乎成了「现代 GC 应当如此」的常识。Go 偏偏
没采用。这一节讲清分代的思想、它的威力，以及 Go 不走这条路的理由。这里的答案值得讲究：它不是
「Go 团队没想到」，而是 Go 团队&lt;strong&gt;真的实现过一版非移动的分代 GC，量过性能，最后放弃了&lt;/strong&gt;。这是
一处极能体现「没有放之四海皆准的设计」的取舍。&lt;/p&gt;
&lt;h2 id="1381-分代假设与代际回收的机制"&gt;13.8.1 分代假设与代际回收的机制&lt;/h2&gt;
&lt;p&gt;分代 GC 建立在一条反复被经验证实的观察上，称作&lt;strong&gt;分代假设&lt;/strong&gt;（generational hypothesis）：
&lt;strong&gt;绝大多数对象都「英年早逝」&lt;/strong&gt;，刚分配不久就不再被引用。它有强弱两个版本，工程上倚重的是弱版本，
即「年轻对象的死亡率远高于年老对象」。Lieberman 与 Hewitt（1983）、Ungar（1984）正是抓住这条
规律设计了最早的代际回收器。&lt;/p&gt;
&lt;p&gt;既然新对象大多速朽，就按「年龄」给对象分代：&lt;strong&gt;新生代&lt;/strong&gt;（young generation）装新分配的对象，
&lt;strong&gt;老年代&lt;/strong&gt;（old generation）装活过若干轮回收、被认为「站稳脚跟」的对象。回收分两档：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;minor GC&lt;/strong&gt;：只扫新生代。因为垃圾几乎都在这里，且新生代通常很小，扫一遍极快。多数回收都是
minor GC。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;major GC / full GC&lt;/strong&gt;：偶尔才全堆扫一次，处理那些熬进老年代后才死去的对象。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把回收工作集中在「小而垃圾密集」的新生代上，是分代 GC 威力的来源。用一点记号来量化：设新生代
存活率为 $s_y$、老年代存活率为 $s_o$，分代假设说 $s_y \ll s_o$。一次 minor GC 的扫描成本正比于
新生代的&lt;strong&gt;存活&lt;/strong&gt;对象数（标记-复制类回收器只触碰活对象），约为 $s_y \cdot N_y$；而它回收掉的垃圾
约为 $(1 - s_y) \cdot N_y$。$s_y$ 越小，单位扫描成本换来的回收收益越高，这正是「频繁、廉价的
minor GC」之所以划算的全部道理。&lt;/p&gt;</description></item><item><title>13.9 请求假设与事务制导回收</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/roc/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/roc/</guid><description>&lt;h1 id="139-请求假设与事务制导回收"&gt;13.9 请求假设与事务制导回收&lt;/h1&gt;
&lt;p&gt;&lt;a href=".././generational"&gt;13.8&lt;/a&gt; 讲过 Go 没有采用分代假设。读者或许会接着问：Go 有没有试过别的
「对象寿命假设」？有。这一节讲的是 Go 团队认真做过、最终放弃的一个实验，&lt;strong&gt;事务制导回收&lt;/strong&gt;
（Request-Oriented Collector, ROC）。讲它，不是因为它进了 Go，而是因为一个被放弃的设计，
往往比成功的设计更能照出约束所在。ROC 像一道做错了的证明题：错处恰好标出了 Go GC 设计空间
里那条不能逾越的边界。&lt;/p&gt;
&lt;h2 id="1391-请求假设"&gt;13.9.1 请求假设&lt;/h2&gt;
&lt;p&gt;服务端程序有一条很强的结构特征：工作以&lt;strong&gt;请求&lt;/strong&gt;为单位。一个请求进来，分配一批对象处理它，
请求一结束，这批对象绝大多数就该死了。Rick Hudson 与 Austin Clements 在 2016 年把这条观察
写成了&lt;strong&gt;请求假设&lt;/strong&gt;（request hypothesis）：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;由一个请求创建的对象，倾向于在该请求被处理完成时一同死亡。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;它与分代假设（&lt;a href=".././generational"&gt;13.8&lt;/a&gt;）说的不是一回事。分代假设讲的是「年轻对象易死」，
是一条关于&lt;strong&gt;年龄&lt;/strong&gt;的统计规律；请求假设讲的是「同一请求的对象同生共死」，是一条关于&lt;strong&gt;归属&lt;/strong&gt;
的结构规律。对服务端来说，后者更贴合现实，也更有力：它不只说某批对象会死，还指明了它们
&lt;strong&gt;何时&lt;/strong&gt;死、&lt;strong&gt;成批&lt;/strong&gt;死。云上的典型 goroutine 正是这种形状，收一条消息、反序列化、算一遍、
序列化、把结果丢回 channel 或 socket，然后退出：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#008000"&gt;// 每请求一个 goroutine：分配的对象多数随 goroutine 退出而死&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;func&lt;/span&gt; handle(conn net.Conn) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	req := decode(conn) &lt;span style="color:#008000"&gt;// 这些中间对象&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	result := compute(req) &lt;span style="color:#008000"&gt;// 几乎都只在本 goroutine 内活动&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	conn.Write(encode(result)) &lt;span style="color:#008000"&gt;// 函数返回后即成垃圾&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;若能利用这条规律，回收就能从「全局扫描找垃圾」退化成「请求一结束，把它的那批对象整批还
回去」,既快，又不必停下整个程序做全局 GC。问题只剩一个：怎么知道哪些对象「只属于这个
请求」？&lt;/p&gt;</description></item><item><title>13.10 终结器</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/finalizer/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/finalizer/</guid><description>&lt;h1 id="1310-终结器"&gt;13.10 终结器&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本节内容对标 Go 1.26。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;垃圾回收的常规剧情到&lt;a href=".././sweep"&gt;清扫（13.5）&lt;/a&gt;就结束了：标记找出存活对象，清扫把死对象的
槽位归还给分配器。但有一类对象在死之前还想说一句话。它包着一份非内存资源，一个文件描述符、
一段 mmap、一个 C 侧分配的句柄，当 Go 这边的包装对象不可达时，那份底层资源也该随之释放，
而垃圾回收只懂内存，并不知道描述符要 &lt;code&gt;close&lt;/code&gt;。终结器（finalizer）就是为这条缝隙而设的钩子：
为对象登记一个函数，等垃圾回收判定它不可达时，运行时不立即释放它，而是先调用这个函数。&lt;/p&gt;
&lt;p&gt;这是一个诱人却危险的机制。诱人在于它看起来像 C++ 的析构函数，能把「资源释放」自动挂到「对象
消亡」上；危险在于 Go 的对象消亡由垃圾回收决定，时机不可预测，于是一切「必须发生」的释放都
不能托付给它。本节先把 &lt;code&gt;runtime.SetFinalizer&lt;/code&gt; 的机制讲清楚，再逐条点出它的陷阱，最后介绍
Go 1.24 引入的 &lt;code&gt;runtime.AddCleanup&lt;/code&gt; 这个更安全的替代品，并给出一条简单的抉择原则。&lt;/p&gt;
&lt;h2 id="13101-setfinalizer-的机制"&gt;13.10.1 SetFinalizer 的机制&lt;/h2&gt;
&lt;p&gt;登记一个终结器只需一行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;
&lt;table style="border-spacing:0;padding:0;margin:0;border:0;"&gt;&lt;tr&gt;&lt;td style="vertical-align:top;padding:0;margin:0;border:0;"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;1
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;2
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;3
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;4
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;5
&lt;/span&gt;&lt;span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"&gt;6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%"&gt;
&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#00f"&gt;type&lt;/span&gt; File &lt;span style="color:#00f"&gt;struct&lt;/span&gt;{ fd &lt;span style="color:#2b91af"&gt;int&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;p := &amp;amp;File{fd: openSomeFd()}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;runtime.SetFinalizer(p, &lt;span style="color:#00f"&gt;func&lt;/span&gt;(p *File) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; syscall.Close(p.fd)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;&lt;code&gt;SetFinalizer(obj, fn)&lt;/code&gt; 要求 &lt;code&gt;obj&lt;/code&gt; 是一个指针，指向由 &lt;code&gt;new&lt;/code&gt;、复合字面量取址或局部变量取址
得到的、堆上分配的对象；&lt;code&gt;fn&lt;/code&gt; 是一个只接收一个参数（类型可由 &lt;code&gt;obj&lt;/code&gt; 赋值）的函数。运行时把
这对关系记在该对象所在 mspan 的一条 &lt;strong&gt;special&lt;/strong&gt; 记录里（&lt;a href="../../ch12alloc/component"&gt;12.2&lt;/a&gt;
讲过 mspan 的元数据），而不是塞进对象本身。&lt;code&gt;SetFinalizer(obj, nil)&lt;/code&gt; 撤销登记。&lt;/p&gt;</description></item><item><title>13.11 过去、现在与未来</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/history/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/history/</guid><description>&lt;h1 id="1311-过去现在与未来"&gt;13.11 过去、现在与未来&lt;/h1&gt;
&lt;p&gt;读完前面十节，读者手里已经握有 Go 垃圾回收器各个零件的现状：三色标记（&lt;a href=".././basic"&gt;13.1&lt;/a&gt;）、
混合写屏障（&lt;a href=".././barrier"&gt;13.2&lt;/a&gt;）、调步器（&lt;a href=".././pacing"&gt;13.3&lt;/a&gt;）、清扫与回收（&lt;a href=".././sweep"&gt;13.5&lt;/a&gt;）。
这一节换一个角度，把这些零件放回时间轴上，看它们是怎样一步步长成今天这副模样的。&lt;/p&gt;
&lt;p&gt;回收器的演进史，读起来像一条始终朝同一个方向收敛的曲线。从 Go 1.0 到今天，停顿时间下降了约两个
数量级，而这一路上有一条不变的主线：每一步改动，都服务于「在不打扰用户代码的前提下完成回收」。
理解了这条主线，下面被采纳与被抛弃的诸多方案，就都能放到同一把尺子上衡量。&lt;/p&gt;
&lt;h2 id="13111-被采纳的方案从数百毫秒到亚毫秒"&gt;13.11.1 被采纳的方案：从数百毫秒到亚毫秒&lt;/h2&gt;
&lt;h3 id="go-1014朴素的停顿世界标记清扫"&gt;Go 1.0–1.4：朴素的停顿世界标记清扫&lt;/h3&gt;
&lt;p&gt;最早的回收器是教科书式的标记清扫（mark-sweep），且整个过程停顿世界（stop-the-world，STW）：
一旦回收开始，所有用户 Goroutine 停下，回收器在单个或少数线程上走完「标记存活、清扫死亡」，
再放用户代码继续。Go 1.3 把清扫阶段改为与标记并行（parallel，多个回收线程同时干活），
缩短了总时长，但用户代码仍然是被整段挂起的。&lt;/p&gt;
&lt;p&gt;这套设计的代价直白地写在停顿时间上：堆越大，需要扫描的对象越多，STW 越长。实测停顿落在数十到
数百毫秒的量级。对一个正在处理请求的 Web 服务，这意味着尾延迟（tail latency）随时可能被一次回收
拖出一道百毫秒级的尖峰。这一痛点，定下了此后十年的主题。&lt;/p&gt;
&lt;h3 id="go-152015并发标记低延迟的转向"&gt;Go 1.5（2015）：并发标记，低延迟的转向&lt;/h3&gt;
&lt;p&gt;Go 1.5 是分水岭。Richard Hudson 主导的这次重写，把标记阶段做成与用户代码&lt;strong&gt;并发&lt;/strong&gt;（concurrent）
执行：回收器一边标记，用户 Goroutine 一边运行、一边分配、一边改写指针。为了在指针被并发改写时
仍不漏标存活对象，引入了 Dijkstra 风格的写屏障（&lt;a href=".././barrier"&gt;13.2&lt;/a&gt;）。这次重写公开喊出的目标，
是把 STW 压到 10 毫秒以下，并在博客与 ISMM 2018 的报告里反复强调一句立场：宁可牺牲一点吞吐与
峰值内存，也要换来可预测的低延迟。这是一次明确的价值排序，而非单纯的性能优化。&lt;/p&gt;
&lt;h3 id="go-1617把毫秒继续往下压"&gt;Go 1.6、1.7：把毫秒继续往下压&lt;/h3&gt;
&lt;p&gt;并发标记落地后，剩下的 STW 集中在回收循环的起止两端。1.6 把回收器状态化、改进了标记终止
（mark termination）阶段的实现，1.7 让栈收缩（stack shrinking）独立于 STW 进行，停顿从 1.5 的
个位数毫秒进一步降到一两毫秒以内。这两个版本没有惊人的算法变动，做的是把 1.5 打下的并发框架
逐处打磨干净。&lt;/p&gt;</description></item><item><title>13.12 垃圾回收统一理论</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/unifiedgc/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/unifiedgc/</guid><description>&lt;h1 id="1312-垃圾回收统一理论"&gt;13.12 垃圾回收统一理论&lt;/h1&gt;
&lt;p&gt;本章一路看下来，Go 的并发标记清扫、混合写屏障、调步器，又对比了分代假设、请求制导回收等
别家的路子。算法五花八门，读者读到此处，心里或许积下一个疑问：这些机制之间，除了「都在做
垃圾回收」之外，有没有一条共同的脉络，把它们安放进同一个框架？&lt;/p&gt;
&lt;p&gt;有。Bacon、Cheng、Rajan 三人在 2004 年的论文《A Unified Theory of Garbage Collection》
里给出了一个出人意料的答案：人们习惯当作两大门派的&lt;strong&gt;追踪式&lt;/strong&gt;（tracing）与&lt;strong&gt;引用计数式&lt;/strong&gt;
（reference counting），在数学上是同一件事的两种写法，是&lt;strong&gt;对偶&lt;/strong&gt;的；而所有现实中的回收器，
都是这两端之间的某种混合。用这个理论收束全章，能让人从「记住了若干算法」上升到「看清了算法
空间的结构」,这正是一章理论收尾应当做的事。&lt;/p&gt;
&lt;h2 id="13121-两个门派与它们各自的缺口"&gt;13.12.1 两个门派与它们各自的缺口&lt;/h2&gt;
&lt;p&gt;先把两端摆清楚。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;追踪式&lt;/strong&gt;从根集合（栈、寄存器、全局变量）出发，沿指针逐步走遍对象图，凡能到达的都标记为
存活，走完后未被标记的即是垃圾。Go 用的就是这一类（&lt;a href=".././basic"&gt;13.1&lt;/a&gt;）。它的好处是天然处理
循环引用：一个互相指向却无人从外部引用的对象环，从根出发根本到不了，自然被判死。它的代价是
回收要成批进行：得等一轮遍历走完，才知道谁是垃圾，于是有「停顿」这件事，也才有本章前面那一
整套为削减停顿而生的并发标记与写屏障机制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;引用计数式&lt;/strong&gt;给每个对象配一个计数器，记录有多少指针指向它。每次指针赋值，给新指向的对象
加一、给旧指向的对象减一；计数归零，对象立即回收。它的好处是回收&lt;strong&gt;及时且分摊&lt;/strong&gt;：内存在最后
一个引用消失的瞬间就还回去，停顿被打散进每一次指针写入里。它的代价正好与追踪式相反：&lt;strong&gt;处理
不了循环引用&lt;/strong&gt;,环内对象彼此把对方的计数顶在一以上，谁也归不了零，于是泄漏。&lt;/p&gt;
&lt;p&gt;两个门派各有一个缺口，而且缺口的位置恰好互补。这种「恰好互补」不是巧合，它是下一节那条对偶
关系在工程表象上的投影。&lt;/p&gt;
&lt;h2 id="13122-追踪与计数是对偶"&gt;13.12.2 追踪与计数是对偶&lt;/h2&gt;
&lt;p&gt;统一理论的核心，是用同一个不动点（fixed point）方程刻画两种算法，二者只是求同一个解的两个
方向。&lt;/p&gt;
&lt;p&gt;把内存里的对象看成一张有向图。设对象集合为 $V$，指针构成的有向边的多重集为 $E$（多重，
因为一个对象可以有多个字段指向同一目标）。我们保守地假定：凡从根可达的对象，将来都可能被用
到，因而不可回收。记根集合为 $R$。对每个对象 $v \in V$，定义它的引用计数 $\rho(v)$ 为：来自
根的引用数，加上来自其他&lt;strong&gt;存活&lt;/strong&gt;对象的引用数。这是一个递归定义，写成不动点方程：&lt;/p&gt;
$$
\rho(v) = \bigl|[\, v : v \in R \,]\bigr| \;+\; \bigl|[\,(w, v) : (w, v) \in E \,\wedge\, \rho(w) &gt; 0 \,]\bigr|
$$&lt;p&gt;方括号取多重集，竖线取其基数。读法是：$v$ 的计数，等于「根直接引用它的次数」，加上「所有
$\rho(w) &gt; 0$ 的对象 $w$ 经由边 $(w, v)$ 引用它的次数」。注意右边又出现了 $\rho$，所以这是一个
递归方程，它的解是一个不动点。关键在于：&lt;strong&gt;这个方程有多个不动点，追踪式与引用计数式分别求出
其中一个。&lt;/strong&gt;&lt;/p&gt;</description></item><item><title>13.13 进一步阅读的参考文献</title><link>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/ref/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://golang.design/under-the-hood/zh-cn/part4memory/ch13gc/ref/</guid><description>&lt;h1 id="813-进一步阅读的参考文献"&gt;8.13 进一步阅读的参考文献&lt;/h1&gt;
&lt;!-- - [Dijkstra et al., 1978] Edsger W. Dijkstra, Leslie Lamport, A. J. Martin, C. S. Scholten, and E. F. M. Steffens. 1978. On-the-fly garbage collection: an exercise in cooperation. Commun. ACM 21, 11 (November 1978), 966–975. DOI:&lt;a href="https://doi.org/10.1145/359642.359655"&gt;https://doi.org/10.1145/359642.359655&lt;/a&gt; --&gt;
&lt;!-- - [Pirinen, 1998] Pekka P. Pirinen. 1998. Barrier techniques for incremental tracing. In Proceedings of the 1st international symposium on Memory management (ISMM '98). ACM, New York, NY, USA, 20-25.
- [Yuasa, 1990] T. Yuasa. 1990. Real-time garbage collection on general-purpose machines. J. Syst. Softw. 11, 3 (March 1990), 181-198.
- [Wilson, 1992] Raul R. Wilson. 1992. Uniprocessor Garbage Collection Techniques. In Proceedings of the International Workshop on Memory Management (IWMM '92), Yves Bekkers and Jaques Cohen (Eds.). Springer-Verlag, London, UK, UK, 1-42.
- [Dijkstra et al. 1978] Edsger W. Dijkstra, Leslie Lamport, A. J. Martin, C. S. Scholten, and E. F. M. Steffens. 1978. On-the-fly garbage collection: an exercise in cooperation. *Commun. ACM* 21, 11 (November 1978), 966-975. --&gt;
&lt;table class="bib"&gt;
&lt;tr&gt;
&lt;td&gt;[Clements and Hudson, 2016]&lt;/td&gt;&lt;td&gt;Austin Clements, Rick Hudson. "Eliminate STW stack re-scanning." October 21, 2016. &lt;a href="https://go.googlesource.com/proposal/+/master/design/17503-eliminate-rescan"&gt;https://go.googlesource.com/proposal/+/master/design/17503-eliminate-rescan&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Dijkstra et al. 1978]&lt;/td&gt;&lt;td&gt;Edsger W. Dijkstra, Leslie Lamport, A. J. Martin, C. S. Scholten, and E. F. M. Steffens. 1978. "On-the-fly garbage collection: an exercise in cooperation." Commun. ACM 21, 11 (November 1978), 966-975.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Hudson, 2015]&lt;/td&gt;&lt;td&gt;Rick Hudson. "Go GC: Latency Problem Solved." GopherCon Denver. July 8, 2015. &lt;a href="https://talks.golang.org/2015/go-gc.pdf"&gt;https://talks.golang.org/2015/go-gc.pdf&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements et al, 2015]&lt;/td&gt;&lt;td&gt;Austin Clements et al. "Discussion of 'Proposal: Garbage collector pacing'." March 10, 2015. &lt;a href="https://groups.google.com/forum/#"&gt;https://groups.google.com/forum/#&lt;/a&gt;!topic/golang-dev/YjoG9yJktg4 &lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2015a]&lt;/td&gt;&lt;td&gt;Austin Clements. "Concurrent garbage collector pacing and final implementation." Mar 10, 2015. &lt;a href="https://golang.org/s/go15gcpacing"&gt;https://golang.org/s/go15gcpacing&lt;/a&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2015b]&lt;/td&gt;&lt;td&gt;Austin Clements. "runtime: replace GC coordinator with state machine." Jul 31, 2015. &lt;a href="https://golang.org/issue/11970"&gt;https://golang.org/issue/11970&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2015c]&lt;/td&gt;&lt;td&gt;Austin Clements. "Proposal: Dense mark bits and sweep-free allocation." Sep 30, 2015. &lt;a href="https://go.googlesource.com/proposal/+/master/design/12800-sweep-free-alloc"&gt;https://go.googlesource.com/proposal/+/master/design/12800-sweep-free-alloc&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2015d]&lt;/td&gt;&lt;td&gt;Austin Clements. "runtime: replace free list with direct bitmap allocation." Sep 30, 2015. &lt;a href="https://golang.org/issue/12800"&gt;https://golang.org/issue/12800&lt;/a&gt;&lt;/td&gt;&lt;!--released in go1.6--&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2015e]&lt;/td&gt;&lt;td&gt;Austin Clements. Proposal: Decentralized GC coordination. October 25, 2015. &lt;a href="https://go.googlesource.com/proposal/+/master/design/11970-decentralized-gc"&gt;https://go.googlesource.com/proposal/+/master/design/11970-decentralized-gc&lt;/a&gt;&lt;/td&gt; &lt;!--released in go1.6--&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2016a]&lt;/td&gt;&lt;td&gt;Austin Clements. runtime: shrinkstack during mark termination significantly increases GC STW time. Jan 14, 2016. &lt;a href="https://golang.org/issue/12967#issuecomment-171466238"&gt;https://golang.org/issue/12967#issuecomment-171466238&lt;/a&gt;&lt;/td&gt; &lt;!--released in go1.7--&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2016b]&lt;/td&gt;&lt;td&gt;Austin Clements. runtime: mutator assists are over-aggressive, especially at high GOGC. Mar 24, 2016. &lt;a href="https://golang.org/issue/14951"&gt;https://golang.org/issue/14951&lt;/a&gt;&lt;/td&gt;&lt;!--released in go1.10--&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Hudson and Clements, 2016]&lt;/td&gt;&lt;td&gt;Rick Hudson and Austin Clements. Request Oriented Collector (ROC) Algorithm. June 2016. &lt;a href="https://golang.org/s/gctoc"&gt;https://golang.org/s/gctoc&lt;/a&gt;&lt;/td&gt; &lt;!--unreleased--&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Fitzpatrick, 2016]&lt;/td&gt;&lt;td&gt;Brad Fitzpatrick. runtime: mechanism for monitoring heap size. Aug 23, 2016. &lt;a href="https://golang.org/issue/16843"&gt;https://golang.org/issue/16843&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements and Hudson, 2016a]&lt;/td&gt;&lt;td&gt;Austin Clements. runtime: eliminate stack rescanning. Oct 18, 2016. &lt;a href="https://golang.org/issue/17503"&gt;https://golang.org/issue/17503&lt;/a&gt;&lt;/td&gt; &lt;!--released in go1.8 (hybrid barrier), go1.9 (remove re-scan), go1.12 (fix mark termination race)--&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements and Hudson, 2016b]&lt;/td&gt;&lt;td&gt;Austin Clements, Rick Hudson. Proposal: Concurrent stack re-scanning. Oct 18, 2016. &lt;a href="https://go.googlesource.com/proposal/+/master/design/17505-concurrent-rescan"&gt;https://go.googlesource.com/proposal/+/master/design/17505-concurrent-rescan&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2016c]&lt;/td&gt;&lt;td&gt;Austin Clements. runtime: perform concurrent stack re-scanning. Oct 18, 2016 &lt;a href="https://golang.org/issue/17505"&gt;https://golang.org/issue/17505&lt;/a&gt;&lt;/td&gt;&lt;!--unreleased--&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements and Hudson, 2016c]&lt;/td&gt;&lt;td&gt;Austin Clements, Rick Hudson. Proposal: Eliminate STW stack re-scanning. Oct 21, 2016 &lt;a href="https://go.googlesource.com/proposal/+/master/design/17503-eliminate-rescan"&gt;https://go.googlesource.com/proposal/+/master/design/17503-eliminate-rescan&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements 2017a]&lt;/td&gt;&lt;td&gt;Austin Clements. runtime/debug: add SetMaxHeap API. Jun 26 2017. &lt;a href="https://go-review.googlesource.com/c/go/+/46751/"&gt;https://go-review.googlesource.com/c/go/+/46751/&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2017b]&lt;/td&gt;&lt;td&gt;Austin Clements. Proposal: Separate soft and hard heap size goal. October 21, 2017. &lt;a href="https://go.googlesource.com/proposal/+/master/design/14951-soft-heap-limit"&gt;https://go.googlesource.com/proposal/+/master/design/14951-soft-heap-limit&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Hudson, 2018]&lt;/td&gt;&lt;td&gt;Richard L. Hudson. Getting to Go: The Journey of Go's Garbage Collector, in International Symposium on Memory Management (ISMM), June 18, 2018. &lt;a href="https://blog.golang.org/ismmkeynote"&gt;https://blog.golang.org/ismmkeynote&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2018a]&lt;/td&gt;&lt;td&gt;Austin Clements. Proposal: Simplify mark termination and eliminate mark 2. Aug 9, 2018. &lt;a href="https://go.googlesource.com/proposal/+/master/design/26903-simplify-mark-termination"&gt;https://go.googlesource.com/proposal/+/master/design/26903-simplify-mark-termination&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Clements, 2018b]&lt;/td&gt;&lt;td&gt;Austin Clements. runtime: simplify mark termination and eliminate mark 2. Aug 9, 2018. &lt;a href="https://golang.org/issue/26903"&gt;https://golang.org/issue/26903&lt;/a&gt;&lt;/td&gt;&lt;!--released go1.12--&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Taylor et al., 2018]&lt;/td&gt;&lt;td&gt;Ian Lance Taylor et al. Runtime: error message: P has cached GC work at end of mark termination. Oct 3, 2018. &lt;a href="https://golang.org/issue/27993"&gt;https://golang.org/issue/27993&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Knyszek, 2019a]&lt;/td&gt;&lt;td&gt;Michael Knyszek. Proposal: Smarter Scavenging. Feb 20, 2019. &lt;a href="https://go.googlesource.com/proposal/+/master/design/30333-smarter-scavenging"&gt;https://go.googlesource.com/proposal/+/master/design/30333-smarter-scavenging&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[Knyszek, 2019b]&lt;/td&gt;&lt;td&gt;Michael Knyszek. runtime: smarter scavenging. Feb 20, 2019. &lt;a href="https://golang.org/issue/30333"&gt;https://golang.org/issue/30333&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;</description></item></channel></rss>