glibc malloc 源自 ptmalloc2,但长期演进后不能把早期 ptmalloc2 的流程图当成当前全部实现。理解它最有用的方式是区分三层:应用拥有的对象、分配器掌握的 chunk 与缓存、操作系统映射与物理驻留。
对象大小与 chunk 大小不同
malloc 接收请求字节数,内部还要满足对齐、元数据和大小类别要求。因此分配器为一个 33 字节对象保留的空间可能更多,差额属于内部开销或内部碎片。
应用只能依接口契约访问合法对象范围,不能因为某次分配似乎多给了一点空间就越界写元数据。calloc 检查元素数乘大小的溢出,而手写 malloc(n * size) 可能在调用前就已经溢出。realloc 对非零目标大小失败时应保留原指针,不要直接覆盖唯一引用。malloc(3)
扩容必须先算得下,再转移所有权
realloc 可能移动对象;成功后旧指针及指向旧对象内部的指针都不能继续使用。失败且目标大小非零时,旧对象仍由调用者拥有。下面把乘法检查、非零约定和临时指针放在同一个可运行例子里。
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
static int resize_ints(int **items, size_t count) {
if (count == 0 || count > SIZE_MAX / sizeof **items) return 0;
int *replacement = realloc(*items, count * sizeof **items);
if (replacement == NULL) return 0;
*items = replacement;
return 1;
}
int main(void) {
int *items = NULL;
if (!resize_ints(&items, 2)) return 1;
items[0] = 7;
items[1] = 9;
if (!resize_ints(&items, 4)) { free(items); return 1; }
if (items[0] != 7 || items[1] != 9) { free(items); return 1; }
items[2] = 11;
items[3] = 13;
int *before_rejection = items;
if (resize_ints(&items, 0) || items != before_rejection) abort();
if (sizeof(int) > 1) {
size_t too_many = SIZE_MAX / sizeof(int) + 1;
if (resize_ints(&items, too_many) || items != before_rejection) abort();
}
printf("%d %d %d %d\n", items[0], items[1], items[2], items[3]);
free(items);
return 0;
}
保存为 resize-demo.c,用 cc -Wall -Wextra resize-demo.c -o resize-demo 编译,输出应为 7 9 11 13。count=0 被接口显式拒绝,避免把各标准和实现中复杂的 realloc(ptr, 0) 行为混入这个“保留旧对象或成功扩容”的契约。realloc(3)
这个小接口只演示拥有指针的更新,没有替调用者管理逻辑长度与容量。成功扩容后,调用者还应提交新容量;若还有元素构造等可能失败的步骤,就需要更完整的回滚或提交设计。零长度和溢出拒绝是可确定复现的;真正内存不足的分配失败应使用可控故障注入验证,不要靠申请一个巨大对象赌系统行为。
tcache 与 arena 是两个层次
tcache 是每线程缓存,适用请求可从中直接复用,减少进入 arena 的次数。arena 管理较大范围的分配状态和空闲结构,通常有同步保护;多个线程可以共享一个 arena,一个 arena 也不等于一个独立应用对象。
“每个线程一座独享 arena,因此全部无锁”是不准确的。线程缓存命中与 arena 竞争要分别测量;跨线程释放还会影响对象在哪一层被缓存、多久重新可用。
具体 tcache 可缓存大小、bin 数量、arena 上限与调优接口随版本变化。较新源码已存在用于更大 chunk 的 tcache 分类,而部分手册仍描述较早范围;查本机构建对应源码与文档,不要把几十字节、1 KiB 或 CPU 数乘某个常数写成所有 glibc 的不变规则。
空闲结构分别优化什么
| 结构 | 基本目的 | 需要保留的区别 |
| tcache | 线程局部复用 | 不等于 arena 自身的 bins |
| fastbins | 对适用小块快速入链、延迟合并 | 合并通常推迟到 consolidation 等路径 |
| unsorted bin | 给新进入的空闲块一次近期复用机会 | 不是所有 free 都先进入它 |
| small bins | 管理精确大小类别的块 | 类别宽度与边界依目标配置 |
| large bins | 按较大尺寸组织查找 | 不能概括成纯 FIFO 队列 |
| top chunk | arena 尾部可切分空间 | 与 bin 中独立空洞及独立 mmap 块不同 |
大致路径是先尝试合适缓存和现有空闲空间,需要时切分、合并或向系统申请更多。真正的分支顺序依大小与版本,不能画成“先用 128 KiB 判断,以上必定 mmap,以下严格逐 bin 扫描”的统一流程。glibc malloc 源码
free 把所有权交给了谁?
图中地址标识存储位置,#编号标识对象的一次生命周期。释放后再分配,地址可以相同而编号不同;旧指针不会因为新对象恰好占据同址就自动重新获得合法性。缓存减少重复申请,但保留块数与应用活跃块数因此可能分离。
正在呈现知识画面
free 把所有权交给了谁?free 结束应用对对象的访问权,但块可能保留在分配器中。再次分配可复用地址,却建立新的对象生命周期。
{"id":"allocator-reuse-layers","title":"free 把所有权交给了谁?","summary":"free 结束应用对对象的访问权,但块可能保留在分配器中。再次分配可复用地址,却建立新的对象生命周期。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"alloc\" data-i18n=\"alloc\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><p class=\"hint\" data-i18n=\"slots\"></p><div id=\"slots\" class=\"choices\"></div><div class=\"track-row\"><span data-i18n=\"cache\"></span><div id=\"cache\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"arena\"></span><div id=\"arena\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"live\"></span><strong id=\"live\"></strong></div><div><span data-i18n=\"reserved\"></span><strong id=\"reserved\"></strong></div><div><span data-i18n=\"source\"></span><strong id=\"source\"></strong></div></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.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.choices{grid-template-columns:repeat(2,minmax(0,1fr))}#cache,#arena{display:grid;grid-template-columns:repeat(4,minmax(0,1fr))}.metrics strong{font-size:17px}#source{min-height:5em}","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).classList.add('table');$('#'+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;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let live={},cache=[],arena=[0,1,2,3],generation=0,last='none';for(let i=0;i<4;i++){const b=document.createElement('button');b.dataset.chunk=i;b.onclick=()=>{if(live[i]){delete live[i];(cache.length<2?cache:arena).push(i);draw()}};$('#slots').append(b)}function draw(){document.querySelectorAll('[data-chunk]').forEach(e=>{const i=+e.dataset.chunk;e.textContent='0x'+(4096+64*i).toString(16)+' · '+(live[i]?'#'+live[i]:'∅');e.disabled=!live[i];});for(const k of ['cache','arena']){const a=k==='cache'?cache:arena;cells(k,Array.from({length:4},(_,i)=>({text:i<a.length?'C'+a[i]:'—',cls:i<a.length?'on':'empty'})));}set('live',Object.keys(live).length);set('reserved',4);set('source',t(last));$('#alloc').disabled=Object.keys(live).length===4;}$('#alloc').onclick=()=>{last=cache.length?'cache':'arena';const i=cache.length?cache.pop():arena.shift();if(i!==undefined)live[i]=++generation;draw()};$('#reset').onclick=()=>{live={};cache=[];arena=[0,1,2,3];generation=0;last='none';draw()};draw();","audio":false,"strings":{"active":"应用持有","alloc":"分配一个对象","arena":"arena 空闲","cache":"线程缓存","heading":"free 把所有权交给了谁?","intro":"对固定大小类别分配、释放,并观察线程缓存与后续复用。","live":"活跃块数","none":"尚未分配","reserved":"分配器保留块数","reset":"重置","scope":"抽象单线程缓存容量2,arena有4个同类块;不模拟 glibc 的具体 bin 顺序、块头或系统申请。初始四块始终保留。","scopeLabel":"模型范围与约定","slots":"点击活跃对象以释放","source":"上次分配来源"}}
mmap 阈值不是静态分界线
主 arena 常通过 program break 扩展,其他 arena 可建立 mmap 支持的区域;独立 mmap 大块分配又是另一种用途。小对象也可能住在 mmap 支持的 arena 里。
glibc 可以动态调整 mmap threshold。典型调整发生在释放符合条件的较大 mmap chunk 时,将阈值提高至相应大小,避免重复向系统申请和解除相似块;不是每次 free 大块就把阈值降低。
显式配置相应阈值会影响动态调整。阈值只是一项策略,还需结合现有可复用空间和其他限制理解。相关名称与含义见 glibc allocation tunables。
普通私有匿名 mmap 在 fork 后仍遵循 COW;它不是免复制共享映射。独立块容易单独解除映射,也不代表系统完全没有碎片或页表成本。
应用释放对象后,分配器可能把 chunk 保留在缓存或 arena,供下一次请求复用。即使一些字节空闲,它们所在页仍有活跃对象时,也未必能归还整页。
例如一页里交错分配了长寿命小对象和短寿命对象,后者全释放后,剩余长寿命对象仍把这页留住。简单相加所有 free 字节无法推导可归还页数,这属于布局和寿命造成的外部碎片问题。
malloc_trim 尝试归还可释放的空闲内存,可能通过缩小或释放页内容实现,不保证所有 arena 的所有空闲空间都归还,也不会让活跃对象消失。频繁 trim 可能增加随后缺页和重新分配成本。malloc_trim(3)
先算空闲字节,再算可归还页面
在一个忽略元数据的模型里,4 页中每页有 1 KiB 长寿命对象与 3 KiB 短寿命对象。短寿命对象释放后,活跃量为 4 KiB,空闲量为 12 KiB,但每页都还有活跃数据。若相同寿命对象聚集,4 KiB 活跃内容只占一页,另外三页才成为整页回收候选。
page = 4096
interleaved_live = [1024, 1024, 1024, 1024]
grouped_live = [4096, 0, 0, 0]
for live in [interleaved_live, grouped_live]:
assert sum(live) == 4096
assert sum(page - n for n in live) == 12288
assert sum(n == 0 for n in interleaved_live) == 0
assert sum(n == 0 for n in grouped_live) == 3
这不是 glibc 会严格按图摆放四个对象的承诺,也不能据此移动任意 malloc 对象:外部保存的指针使普通 C 分配器无法像某些 GC 那样自由压缩活跃对象。对明确同生共死的一批对象,应用层区域分配可以让整批释放更容易;若其中少数对象寿命特别长,整个区域又可能被它们留住。
12 KiB 空闲,为什么一页也收不回?
两种布局的对象数量和释放字节完全相同,差别只在长寿命对象是否占住每一页。先在交错布局释放,再切换聚集布局重做,观察空闲量相同但整页候选从 0 变为 3。普通 malloc 无法随意搬走长寿命对象,布局通常需要从分配策略和对象寿命入手。
正在呈现知识画面
12 KiB 空闲,为什么一页也收不回?空闲字节之和不决定可回收整页数量;少量长寿命对象分散在不同页上,会让每页都保留。
{"id":"allocation-page-fragmentation","title":"12 KiB 空闲,为什么一页也收不回?","summary":"空闲字节之和不决定可回收整页数量;少量长寿命对象分散在不同页上,会让每页都保留。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"layout\"><button data-mode=\"mixed\" data-i18n=\"mixed\"></button><button data-mode=\"grouped\" data-i18n=\"grouped\"></button></div><div id=\"heap\" class=\"heap\"></div><div class=\"actions\"><button id=\"release\" data-i18n=\"release\"></button><button id=\"restore\" data-i18n=\"restore\"></button></div><div class=\"metrics\"><div><span data-i18n=\"live\"></span><strong id=\"live\"></strong></div><div><span data-i18n=\"free\"></span><strong id=\"free\"></strong></div><div><span data-i18n=\"pages\"></span><strong id=\"pages\"></strong></div><div><span data-i18n=\"held\"></span><strong id=\"held\"></strong></div></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.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.heap{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.heap section{border:1px solid var(--rule);padding:8px;border-radius:8px}.heap header{font-size:12px;margin-bottom:6px}.objects{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px}.objects .cell{min-height:55px}","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).classList.add('table');$('#'+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;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let layout='mixed',released=false;function draw(){pressed(layout);$('#heap').replaceChildren(...Array.from({length:4},(_,p)=>{const s=document.createElement('section'),h=document.createElement('header'),grid=document.createElement('div');h.textContent='Page '+p;grid.className='objects';for(let i=0;i<4;i++){const long=layout==='mixed'?i===0:p===0,e=document.createElement('div');e.className='cell '+(long?'on':released?'empty':'gold');e.textContent=t(long?'long':released?'empty':'short');grid.append(e)}s.append(h,grid);return s}));const n=released?(layout==='grouped'?3:0):0;set('live',released?4:16);set('free',released?12:0);set('pages',n);set('held',4-n);$('#release').disabled=released;$('#restore').disabled=!released;}document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{layout=e.dataset.mode;released=false;draw()});$('#release').onclick=()=>{released=true;draw()};$('#restore').onclick=()=>{released=false;draw()};draw();","audio":false,"strings":{"empty":"空闲","free":"空闲(KiB)","grouped":"按寿命聚集","heading":"12 KiB 空闲,为什么一页也收不回?","held":"仍有活跃对象的页","intro":"切换对象的初始布局,再释放或恢复短寿命对象。","layout":"初始分配布局","live":"活跃(KiB)","long":"长寿命","mixed":"寿命交错","pages":"整页回收候选","release":"释放短寿命对象","reset":"重置","restore":"重新分配短寿命对象","scope":"4页,每页4个1 KiB对象,忽略元数据。切换布局表示重新分配的一次对照运行,不在原地移动仍被引用的 C 对象。整页空闲只是回收候选。","scopeLabel":"模型范围与约定","short":"短寿命"}}
锁竞争、伪共享和泄漏不同
多个线程争同一个 arena mutex 是锁竞争;两个线程修改同一缓存行上不同对象产生一致性流量,才是典型伪共享。限制 arena 数可能减少保留内存,却增加锁竞争,不能当作同时解决两种问题的万能选项。
泄漏通常指失去必要释放路径或不再需要却持续保留的对象;分配器保留空闲块是另一层现象。判断时对照活跃对象量、分配器统计、RSS/PSS 与负载阶段,见 进程内存布局。
观察与小步调参
#include <malloc.h>
#include <stdio.h>
int dump_allocator(void) {
return malloc_info(0, stdout);
}
这是 glibc 专用接口,输出 arena 等 XML 信息,不应理解成完整、精确到所有线程缓存对象的业务存活清单。malloc_stats 与 mallinfo2 也各有统计口径和限制。
GLIBC_TUNABLES=glibc.malloc.arena_max=2 ./prog
GLIBC_TUNABLES=glibc.malloc.tcache_count=0 ./prog
这两条用于分别做对照实验,不表示应同时部署到所有程序。记录吞吐、尾延迟、分配调用栈和内存曲线,避免仅以运行结束时 RSS 评价长期服务。
MALLOC_TRACE 环境变量本身并不保证当前 glibc 自动记录全部分配;mtrace 的启用与调试库要求须按版本配置。普通越界和释放后使用优先通过 ASan 或 Memcheck 定位,不能把调低阈值当成修复内存错误。
观测要沿负载阶段取样
| 阶段 | 应用自己记录 | 分配器与系统侧记录 | 用来判断 |
| 预热前 | 初始活跃对象数和字节 | arena 信息、RSS/PSS | 基线 |
| 峰值时 | 大小分布、峰值并发、寿命 | 保留空间与驻留 | 峰值由什么驱动 |
| 释放短命对象后 | 已释放多少,剩余哪些 | 空闲结构与 RSS/PSS | 对象释放是否变成整页回收 |
| 下一轮相同负载 | 新请求量、分配延迟 | 是否复用、是否再增长 | 缓存是否稳定复用 |
若业务活跃字节降回基线,RSS 保持较高,但下一轮峰值不再增长,保留空间可能在发挥复用作用。若相同循环中业务活跃量持续上涨,应先找持有者和释放路径。若活跃量稳定而系统占用持续上升,再查碎片、线程/arena 数量、映射及统计盲区,不能只凭一条曲线宣布泄漏。
malloc_info 可输出多个 arena 的信息,但它不是业务对象账本。采样本身也有成本;不要在每次 malloc 后格式化整份 XML 再把所得时间当成分配器原始延迟。malloc_info(3)
替换分配器时比较什么
Google TCMalloc 的当前设计可使用 per-CPU 缓存,也有不同模式;它与 gperftools 的 tcmalloc 不能仅凭库名字就混为同一个版本。jemalloc 采用 arena、线程缓存和 extent 等机制,统计与回收行为可配置。TCMalloc 设计、jemalloc 手册
没有跨全部负载稳定成立的“大分配固定阈值、碎片高低排名”。应测试对象大小分布、寿命、跨线程释放、并发和 RSS 曲线。堆 profiler 也不等于 CPU profiler,报告维度需要确认。
LD_PRELOAD 只对适用的动态链接和符号解析路径有效;静态链接、自定义分配接口、受限执行环境及多个运行库可能改变效果。分配和释放必须由兼容的分配器协议配对,不能一边更换 malloc、一边让其他组件用不匹配的 free。
关键词:malloc, glibc, ptmalloc2, tcache, arena, bins, fragmentation, mmap threshold, RSS, malloc_trim, jemalloc, TCMalloc