垃圾回收自动管理部分对象的内存生命周期。这里的“垃圾”通常按语言与运行时可观察的引用关系判断,不能理解为收集器知道程序未来再也不会用到什么。一个被全局缓存长期引用的无用对象仍可能被保留。
理解 GC 时,要把几个维度分开:怎样识别可回收对象、是否移动对象、是否分代、工作是否并行或并发,以及编译器如何报告引用。这些可以组合,并不是互斥的几种产品。
根、对象图与保守近似
追踪式 GC 从根引用出发遍历对象图。根可能位于线程栈、寄存器、全局槽或运行时句柄中,但不是这些区域的每个 word 都必然是指针。
可达性:没有根入口的环也可以是垃圾
根引用
对象 A
对象 B
对象 X
对象 Y
X 与 Y 互相引用,但从根不可达;单纯引用计数不会仅凭互相引用消失而回收它们。
精确 GC 依靠类型布局与栈映射辨认引用槽;保守 GC 可能把看起来像地址的数值也当作潜在引用,因此会误保留一些对象,并限制安全移动的方式。精确与保守描述引用识别能力,不直接决定是否分代或并发。
引用计数则在引用变化时维护计数,计数归零时回收。它不必每次从根遍历全图,但互相引用的不可达环不会自然归零,通常需要其他机制处理循环。不能把“所有 GC 都执行同一种根遍历”当作定义。
Mark-Sweep:找出存活对象,再回收未标记空间
标记阶段遍历根可达的对象;清扫阶段根据标记信息回收未标记对象的空间。实现可以按页、span 或分配元数据处理,不一定逐字节扫描整个预留地址空间。
非移动清扫不改变对象地址,便于某些外部引用与固定地址需求,但会留下大小不一的空洞。分配器的尺寸分级与空闲管理可以缓解碎片;“空闲总量足够”仍不保证能满足所有连续大对象分配。
标记成本不仅由活对象字节量决定,还取决于指针数量、对象布局与局部性。一个大字节数组和等大小的密集指针图,追踪工作可能差别很大。
复制与压缩:回收时改变对象地址
经典双半区收集器把分配区分成 from-space 与 to-space,将存活对象复制到目标区,修正引用,再交换两区角色。Cheney 算法用目标区的 scan 与 free 两个位置组织广度优先扫描:新复制对象追加到 free,scan 依次处理其引用,直到两者相遇。
forwarding 信息确保多条引用指向同一对象时只复制一次,并统一找到新地址。回收原区域表示其可重新分配,不意味着必须立刻归还操作系统。
双半区的简单形式预留一半区域作为复制目标,但不能把这个比例推广为所有移动 GC 的总体利用率。区域式、分代和压缩算法有不同空间策略。移动还要求处理内部指针、FFI 句柄与固定对象,不能只更新根就结束。
让 scan 与 free 真正走完一个对象图
用整数代表旧对象地址:10 是 A,指向 B 和 C;20 是 B,也指向 C;30 是 C,没有子引用。另有 40 与 50 构成不可达环。根只有 A。把对象大小统一为一个槽,只保留指针字段,便能把复制算法缩成下面的完整模型。
heap = { 10 : [ 20 , 30 ] , 20 : [ 30 ] , 30 : [ ] , 40 : [ 50 ] , 50 : [ 40 ] }
def collect ( heap , roots ) :
forwarding , copied , queue = { } , [ ] , [ ]
def evacuate ( old ) :
if old not in forwarding :
forwarding [ old ] = len ( copied )
copied . append ( None ) queue . append ( old )
return forwarding [ old ]
new_roots = [ evacuate ( old ) for old in roots ]
scan , trace = 0 , [ ]
while scan < len ( copied ) : old = queue [ scan ]
copied [ scan ] = [ evacuate ( child ) for child in heap [ old ] ]
scan += 1
trace . append ( ( scan , len ( copied ) ) )
return new_roots , copied , forwarding , trace
roots , copied , moved , trace = collect ( heap , [ 10 , 10 ] )
assert roots == [ 0 , 0 ] assert copied == [ [ 1 , 2 ] , [ 2 ] , [ ] ]
assert moved == { 10 : 0 , 20 : 1 , 30 : 2 }
assert trace == [ ( 1 , 3 ) , ( 2 , 3 ) , ( 3 , 3 ) ]
assert collect ( heap , [ 40 ] ) [ 1 ] == [ [ 1 ] , [ 0 ] ] assert collect ( heap , [ ] ) [ 1 ] == [ ]
完成的动作 scan / free 为什么这样变化
复制根 A 0 / 1 已有一个副本,但其字段尚未扫描
扫描 A 1 / 3 首次发现 B、C,追加两个副本
扫描 B 2 / 3 C 已有 forwarding,复用槽 2
扫描 C 3 / 3 无新引用,待扫描区为空
这个顺序揭示 forwarding 的两个职责:防止重复复制,并保持对象身份。若 A 与 B 原先共享 C,回收后它们仍须共享同一个 C,不能各复制一份后悄悄改变修改的可见性。先登记转发再扫描字段,还让可达环能够终止。
模型里的新地址是数组下标,真实收集器则按对象实际大小和对齐推进字节指针,还要处理复制目标容量不足。这里假设收集期间对象图不变,没有实现并发屏障、弱引用、终结器或内部指针。
复制区同时承担存储空间和待扫描队列
A → [B, C]
已扫描
B → [C]
待扫描
C → []
待扫描
未分配
复制目标余量
scan = 1
free = 3
这是扫描 A 之后的状态;B 再次引用 C 时,free 不增加。
两条引用找到同一个对象,为什么只复制一次?
scan 与 free 分开,是因为“已经复制”不等于“字段已经更新”。下面目标区的待扫描对象仍可能携带旧址,只有处理到它时才修正字段。A 和 B 对 C 的引用最终必须落到同一个新槽;把 D 加入根后,D/E 的循环也靠先登记 forwarding 避免重复复制。这里只把对象大小统一成一个槽,不宣称真实对象等长。
[ 0 , scan ) scanned , [ scan , free ) pending
正在呈现知识画面 重新加载
两条引用找到同一个对象,为什么只复制一次? 复制前先登记 forwarding,后续引用复用同一个新地址。scan 左侧已扫描,scan 与 free 之间待扫描;两者相遇时结束,共享与环都不会重复复制。
{"id":"cheney-forwarding","title":"两条引用找到同一个对象,为什么只复制一次?","summary":"复制前先登记 forwarding,后续引用复用同一个新地址。scan 左侧已扫描,scan 与 free 之间待扫描;两者相遇时结束,共享与环都不会重复复制。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"cycle\"><span data-i18n=\"cycle\"></span></label><div class=\"control\"><label for=\"scan\" data-i18n=\"scan\"></label><output for=\"scan\" id=\"scan-value\"></output><input id=\"scan\" type=\"range\" min=\"0\" max=\"3\" value=\"1\" step=\"1\"></div><svg id=\"objects\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\"></svg><div id=\"space\" class=\"track\"></div><div class=\"metrics\"><div><span data-i18n=\"free\"></span><strong id=\"free\"></strong></div></div><section class=\"panel\"><h3 data-i18n=\"forward\"></h3><div id=\"forward\"></div></section><section class=\"panel\"><h3 data-i18n=\"refs\"></h3><div id=\"refs\"></div></section><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#forward,#refs{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;min-height:9em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}const old={A:['B','C'],B:['C'],C:[],D:['E'],E:['D']},address={A:10,B:20,C:30,D:40,E:50};function build(){const forwarding={},to=[],refs=[],snapshots=[];function copy(name){if(forwarding[name]!==undefined)return forwarding[name];const at=to.length;forwarding[name]=at;to.push(name);refs.push(old[name].map(n=>'old:'+address[n]));return at}copy('A');if($('#cycle').checked)copy('D');function snap(scan){snapshots.push({scan,to:[...to],forwarding:{...forwarding},refs:refs.map(r=>[...r])})}snap(0);let scan=0;while(scan<to.length){refs[scan]=old[to[scan]].map(copy);scan++;snap(scan)}return snapshots}function draw(){const snapshots=build();$('#scan').max=snapshots.length-1;const index=Math.min(+$('#scan').value,snapshots.length-1),s=snapshots[index];set('scan-value',index);const names=Object.keys(old),pos=[[55,35],[180,105],[305,35],[100,205],[260,205]],edges=[[0,1],[0,2],[1,2],[3,4],[4,3]];graph('objects',names.map((name,i)=>({name,x:pos[i][0],y:pos[i][1],cls:s.forwarding[name]!==undefined?'on':'',sub:String(address[name])})),edges);$('#objects').setAttribute('viewBox','0 0 360 255');cells('space',Array.from({length:5},(_,i)=>({text:i+': '+(s.to[i]||'∅'),cls:i<s.scan?'on':i<s.to.length?'gold':'empty'})));set('free',s.to.length);set('forward',names.map(n=>address[n]+' '+n+' → '+(s.forwarding[n]??'∅')).join('\\n'));set('refs',Array.from({length:5},(_,i)=>s.to[i]?i+' '+s.to[i]+' → '+fmt(s.refs[i]):'·').join('\\n'));$('#space').dataset.snapshot=JSON.stringify(s);}$('#scan').oninput=draw;$('#cycle').onchange=()=>{$('#scan').value=0;draw()};draw();","audio":false,"strings":{"cycle":"把 D 也作为根","forward":"旧址 → 新址","free":"复制对象数 free","heading":"两条引用找到同一个对象,为什么只复制一次?","intro":"选择扫描位置,比较旧地址、转发表与 to-space。也可把不可达环 D↔E 加入根,观察同一算法如何处理环。","refs":"新对象中的引用","reset":"重置","scan":"已扫描对象数 scan","scope":"五个等大对象槽,A→B,C;B→C;D↔E。暂停式收集,容量足够,无弱引用、内部指针或并发修改。新地址为槽编号。","scopeLabel":"模型范围与约定","space":"目标区:已扫描 / 待扫描"}}
分代:少扫描长期存活对象
弱分代假设是许多程序中的大量对象很快死亡。若该假设适合工作负载,可以频繁收集小的年轻代,减少重复扫描长期存活对象。晋升阈值、代数和各代算法由实现决定,年轻代不必遵循统一的大小或次数。
只收集年轻代时,老年代的引用也可能是入口
根
老对象 A
年轻对象 B
记录 A 所在卡片或引用槽,年轻代收集时扫描
Remembered set 避免每次都扫描全部老对象;它不等于所有 GC 写屏障的目的。
卡表通常以一小段内存区域为粒度记录潜在变化,remembered set 可以采用更精细的对象或引用槽表示。它们让年轻代收集知道哪些老对象可能提供入口,而不必扫描全部老年代。
如果程序长寿命对象多、跨代写入频繁或年轻对象迅速晋升,分代的额外记录与复制成本可能减弱收益。分代是基于负载的优化,不是所有 GC 的必备阶段。
并行、并发和增量不是同义词
并行表示多个收集线程共同工作,应用可以仍然暂停;并发表示收集器与应用 mutator 同时执行;增量表示把工作拆成较小步骤。这些维度可以同时出现。
Mark-Sweep 或复制既可以用于暂停式方案,也可以在配套协议下并发执行。并发仍可能需要短暂停顿建立根视图、转换阶段或完成收尾,不能直接称作应用永不停顿。
屏障:维护具体算法要求的信息
三色抽象把对象分为白色(未发现)、灰色(已发现待扫描)和黑色(已扫描)。在简单的强三色不变量下,不允许黑对象指向未被保护的白对象;并发修改会改变图,因此需要屏障和工作队列维护正确性。
例如收集器已经扫描 A,应用随后把 B 的引用写入 A,而 B 原先的最后一条待扫描路径又被移除。如果没有对应保护,B 可能仍被应用引用却没有被标记。
插入屏障可以记录新写入的引用;删除屏障或 SATB 风格屏障可以保留被覆盖的旧引用;混合屏障组合不同动作。不同算法维护的不变量不同,不能把它们全缩写为一个 if black then gray。
分代屏障主要记录跨代引用,并发标记屏障保护追踪正确性,移动收集器的读屏障还可能处理 forwarding。选择读或写屏障首先取决于算法需要,读写频率影响成本,但不能单独决定正确方案。
Go 的官方 GC 指南描述其并发、非移动的追踪收集器;运行时写屏障源码说明混合屏障的目的。它不能作为“年轻代复制、老年代清扫”的通用分代例子。Go GC 指南 、Go 写屏障实现说明
黑对象新增引用,谁负责发现那个白对象?
这不是让所有屏障共用一条规则,而是固定一种增量更新模型来观察不变量。A 已经扫描后再增加 A→B,若 B 仍白,就需要留下新的追踪任务。先建立这条边、再删掉 G→B,可以让原有灰路径消失;开启插入屏障时,B 自己已在灰集合中,所以信息不会随旧路径一起丢掉。
正在呈现知识画面 重新加载
黑对象新增引用,谁负责发现那个白对象? 插入屏障在黑 A 获得白 B 时把 B 标灰。即使原来的 G→B 随后消失,B 仍会被扫描;没有屏障则可能留下根可达但未标记的对象。
{"id":"tricolor-insertion-barrier","title":"黑对象新增引用,谁负责发现那个白对象?","summary":"插入屏障在黑 A 获得白 B 时把 B 标灰。即使原来的 G→B 随后消失,B 仍会被扫描;没有屏障则可能留下根可达但未标记的对象。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"barrier\" checked><span data-i18n=\"barrier\"></span></label><div class=\"actions\"><button id=\"link\" data-i18n=\"link\"></button><button id=\"unlink\" data-i18n=\"unlink\"></button><button id=\"scanG\" data-i18n=\"scanG\"></button><button id=\"scanB\" data-i18n=\"scanB\"></button><button id=\"finish\" data-i18n=\"finish\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><svg id=\"graph\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\"></svg><div id=\"colors\" class=\"track\"></div><div class=\"metrics\"><div><span data-i18n=\"pending\"></span><strong id=\"pending\"></strong></div><div><span data-i18n=\"missed\"></span><strong id=\"missed\"></strong></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"safe\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"work\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"lost\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.metrics strong{font-size:18px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let refs,color;function reset(){refs={A:null,G:'B',B:null};color={A:'black',G:'gray',B:'white'};draw()}function scan(n){if(color[n]!=='gray')return;const v=refs[n];if(v&&color[v]==='white')color[v]='gray';color[n]='black'}function draw(){const names=['A','G','B'],gray=names.filter(n=>color[n]==='gray'),reachable=new Set(['A','G']);for(let i=0;i<3;i++)for(const n of [...reachable])if(refs[n])reachable.add(refs[n]);const missed=[...reachable].filter(n=>color[n]==='white');graph('graph',names.map((name,i)=>({name,x:[65,295,180][i],y:i===2?155:40,cls:color[name]==='black'?'on':color[name]==='gray'?'gold':''})),names.flatMap((n,i)=>refs[n]?[[i,names.indexOf(refs[n])]]:[]));cells('colors',names.map(n=>({text:n+': '+color[n],cls:color[n]==='black'?'on':color[n]==='gray'?'gold':'empty'})));set('pending',fmt(gray));set('missed',fmt(missed));$('#link').disabled=refs.G===null||refs.A===refs.G;$('#unlink').disabled=refs.G===null;$('#scanG').disabled=color.G!=='gray';$('#scanB').disabled=color.B!=='gray';$('#finish').disabled=gray.length===0;set('explanation',t(gray.length?'work':missed.length?'lost':'safe'));$('#missed').dataset.unsafe=String(gray.length===0&&missed.length>0);}$('#link').onclick=()=>{refs.A=refs.G;if($('#barrier').checked&&color.A==='black'&&color[refs.A]==='white')color[refs.A]='gray';draw()};$('#unlink').onclick=()=>{refs.G=null;draw()};$('#scanG').onclick=()=>{scan('G');draw()};$('#scanB').onclick=()=>{scan('B');draw()};$('#finish').onclick=()=>{let guard=0;while(Object.values(color).includes('gray')&&guard++<10)scan(Object.keys(color).find(n=>color[n]==='gray'));draw()};$('#reset').onclick=reset;$('#barrier').onchange=reset;reset();","audio":false,"strings":{"barrier":"启用插入屏障","finish":"扫完剩余灰对象","heading":"黑对象新增引用,谁负责发现那个白对象?","intro":"A 已扫描,G 待扫描,G 原来指向 B。可先把 B 接到 A,再移除 G→B,观察屏障是否留下追踪任务。","link":"A.ref ← G.ref","lost":"灰集合已空,却仍有可达白对象;此时清扫会错误回收它。","missed":"仍可达的白对象","pending":"灰色待扫描集合","reset":"重置","safe":"灰集合为空且可达对象都已标记,可以进入本模型的清扫阶段。","scanB":"扫描灰 B","scanG":"扫描灰 G","scope":"简单增量更新插入屏障,根为 A/G,不移动对象。不是 SATB、Go 混合屏障或完整并发 GC。切换屏障重新开始一段执行。","scopeLabel":"模型范围与约定","unlink":"G.ref ← null","work":"还有灰对象待扫描,当前不是最终回收判定。"}}
编译器必须提供什么
支持 为什么需要
引用布局与栈映射 区分引用和普通数据,找到存活引用
安全点或相应暂停协议 在可解释的执行状态下扫描或更新引用
屏障 记录修改并维护收集算法不变量
移动后的引用更新规则 防止继续使用旧地址
与 FFI、异常展开及优化协作 避免引用被隐藏或恢复成过期状态
安全点不必只存在于函数调用或循环头,实现还可以支持受限制的异步暂停机制。编译器消除某个屏障也必须证明符合具体算法;“对象刚分配”不是所有屏障都可以省略的统一理由。LLVM 将这些协作需求作为 GC 集成接口的一部分。LLVM GC 文档
成本与生命周期速查
增大堆预算常能减少收集频率,却增加驻留内存;降低暂停可能把更多工作摊到 mutator 屏障和并发 CPU 上;移动改善紧凑性,却增加复制与引用修正。评估应同时观察分配率、存活集、GC CPU、暂停分布和峰值内存。
GC 也不替代文件、锁或连接的及时释放。Finalizer 的运行时机通常不适合承担必须准时完成的业务清理,应使用语言提供的显式生命周期机制。
用一个受限预算模型分开“频率”和“一次工作量”
假设每轮回收后的存活集固定为 100 MiB,下一轮在堆占用达到 200 MiB 时触发,应用持续以 50 MiB/s 分配。可分配余量 100 MiB,理想化轮间隔就是 2 秒。若阈值改成 300 MiB,余量为 200 MiB,间隔变成 4 秒;这并没有直接证明一次暂停缩短。
from fractions import Fraction
live , allocation_rate = 100 , 50 intervals = [ Fraction ( limit - live , allocation_rate ) for limit in ( 200 , 300 ) ]
assert intervals == [ 2 , 4 ]
cpu_per_second = [ Fraction ( 1 , 10 ) / interval for interval in intervals ]
assert cpu_per_second == [ Fraction ( 1 , 20 ) , Fraction ( 1 , 40 ) ]
上述摊销工作量从每秒 0.05 CPU 秒降到 0.025 CPU 秒,是“固定每轮成本、只改变频率”的模型结论。真实运行时会提前启动并发标记,根与存活集会变化,也可能发生分配辅助、晋升或内存限制压力,因此不能直接把这两个数当作产品性能预测。
如果暂停仍很长,应进一步辨认根扫描、复制存活对象还是收尾阶段占主导;如果总 CPU 高而暂停短,则可能是频繁收集或大量并发工作。把每轮耗时、每秒轮数和应用吞吐放在一起,才能判断增加堆预算解决的是哪一种成本。Go GC 成本模型
参考
Keywords: GC, tracing, reference counting, root map, mark-sweep, copying, generational, remembered set, barrier, safepoint