定义
RSS Ghost:进程 free/delete 了内存,但 RSS(Resident Set Size)不下降的现象。
内存已归还给 malloc 的 free list,但未归还给操作系统,进程看起来仍在”占用”。
机制
glibc malloc 两条分配路径
malloc(size) ├── size < mmap_threshold (默认128KB) │ → brk/sbrk 堆扩展 │ → free: 归还 free list,堆顶不收缩 │ → RSS 不降 │ └── size >= mmap_threshold → mmap 独立映射 → free: munmap 立即归还 OS → RSS 下降
|
不是简单的”频繁 alloc/free”,而是 分配模式交错:
交错分配 (RSS Ghost): vec1.reserve(1000); → 堆: vec2.reserve(1000); → 堆: item1 = new Item(); → 堆: item2 = new Item(); → 堆:
delete item1, item2: 堆: ↑ 堆顶被vec2_buf钉住,brk无法trim 中间free块在free list,OS看不到 RSS = 全部,实际只用 vec1+vec2 缓冲区
批量分配 (无RSS Ghost): items.reserve(10000); for (...) items.push_back(new Item()); 堆: ...
delete all: 堆: → 堆顶无钉住对象,brk可以trim → RSS 回落
|
x86_64 (4K 页)
实测数据
| 场景 |
初始 RSS |
释放后 RSS |
Ghost |
回落? |
| 批量小对象 48B |
3.41MB |
4.09MB |
0.68MB |
部分 |
| 批量中等 60KB |
3.61MB |
3.61MB |
0 |
完全 |
| 批量大对象 200KB |
3.61MB |
3.61MB |
0 |
完全(mmap) |
| 混合分配交错 |
3.61MB |
15.40MB |
11.79MB |
不回落 |
| 生命周期交错 |
15.40MB |
15.46MB |
+0.06MB |
不回落 |
特征
- 4K 页粒度小,碎片浪费相对温和:48B 对象钉住 4KB 页,浪费率 85 倍
- glibc arena 有 trim 机制,批量释放后多数场景能回落
- 交错分配仍会产生 Ghost,但绝对值较小(~12MB 级别)
- jemalloc 在 4K 页下 dirty page purge 正常工作,可缓解部分 Ghost
ARM aarch64 (64K 页)
实测数据
| 场景 |
初始 RSS |
释放后 RSS |
Ghost |
回落? |
| 批量小对象 48B |
2.19MB |
2.19MB |
0 |
完全 |
| 批量中等 60KB |
3.25MB |
3.25MB |
0 |
完全 |
| 批量大对象 200KB |
3.25MB |
3.25MB |
0 |
完全(mmap) |
| 混合分配交错 |
3.25MB |
11.44MB |
8.19MB |
不回落 |
| 生命周期交错 |
11.44MB |
15.12MB |
+3.69MB |
不回落 |
| 业务循环 |
15.12MB |
15.12MB |
不回落 |
不回落 |
特征
- 碎片放大 16 倍:48B 对象钉住 64KB 页,浪费率 1365 倍(vs 4K 页 85 倍)
- brk trim 在 64K 页下更难触发:每个碎片至少占 64KB,堆顶更容易被钉住
- jemalloc 完全不可用:dirty page purge 失效
jemalloc 在 64K 页下的失效
| 场景 |
glibc |
jemalloc |
jemalloc/glibc |
| 60KB 释放后 |
3.31MB |
65.44MB |
19.8x |
| 混合释放后 |
11.50MB |
74.19MB |
6.5x |
| 业务循环 |
15.19MB |
74.19MB |
4.9x |
失效原因:
jemalloc dirty page purge: 1. free → 标记 extent 为 dirty 2. 达到 decay 阈值 → madvise(MADV_DONTNEED) 3. madvise 要求: 地址按 OS 页大小(64KB) 对齐
问题: jemalloc 内部以 4KB 粒度管理 extent → 一个 64KB OS 页内有多个 4KB extent → 部分 extent dirty,部分 in-use → 无法对齐 64KB 边界调用 madvise → 整个 64KB 页无法 purge → RSS 完全不降
--with-lg-hugepage=21 --with-lg-page=16 编译参数 只影响内部 chunk/extent 分层,不解决 madvise 对齐问题 → 无改善
|
x86_64 vs ARM 对比
| 维度 |
x86_64 (4K页) |
ARM aarch64 (64K页) |
| 页大小 |
4096B |
65536B |
| 48B对象碎片浪费 |
85x |
1365x |
| brk trim 难度 |
较易 |
困难(每碎片64KB) |
| glibc Ghost 绝对值 |
~12MB |
~8-15MB |
| glibc 批量释放回落 |
多数能回落 |
部分能回落 |
| jemalloc purge |
正常 |
完全失效 |
| jemalloc vs glibc |
略优或持平 |
jemalloc 4-6x 更差 |
缓解方案
MALLOC_MMAP_THRESHOLD_=16384
将 mmap 阈值从 128KB 降到 16KB,使大部分分配走 mmap,free 时 munmap 直接归还 OS。
ARM 64K 页实测效果:
| 场景 |
默认 glibc |
MMAP_THRESHOLD=16KB |
降幅 |
| 混合分配释放后 |
11.44MB |
3.31MB |
-71% |
| 生命周期释放后 |
15.12MB |
6.25MB |
-59% |
| 释放长期对象后 |
15.12MB |
3.88MB |
回落基线 |
| 业务循环释放后 |
15.12MB |
3.88MB |
-74% |
原理:
阈值 16KB: malloc(48B) → brk (太小,不影响) malloc(60KB) → mmap (超过16KB) free → munmap → RSS 下降
brk 堆只有 vector 缓冲区等小分配 大块全走 mmap,释放即回收 碎片可控,RSS 准确反映实际使用
|
代价:mmap 是系统调用,分配略慢,需压测验证性能可接受。
不推荐的方案
| 方案 |
原因 |
| 换 jemalloc |
64K 页下 purge 失效,RSS 更差 |
| jemalloc + lg-page=16 |
不解决 madvise 对齐,无改善 |
| mallopt(M_TRIM_THRESHOLD) |
brk trim 只收缩堆顶,无法解决中间碎片 |
| malloc_trim() 手动触发 |
同上,且频繁调用性能差 |
总结
RSS Ghost = free 不归还 OS 的内存
触发: 分配模式交错 (非简单 alloc/free) 放大: 页大小 × 碎片数量 恶化: TPS 潮涌 × 长期运行
x86_64: 存在但温和,jemalloc 可缓解 ARM 64K: 严重,jemalloc 反而恶化,glibc + 低mmap阈值有效
|