What is RSS Ghost

定义

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 下降

RSS Ghost 的触发条件

不是简单的”频繁 alloc/free”,而是 分配模式交错

交错分配 (RSS Ghost):
vec1.reserve(1000); → 堆: [vec1_buf ]
vec2.reserve(1000); → 堆: [vec1_buf][vec2_buf ]
item1 = new Item(); → 堆: [vec1_buf][vec2_buf][item1 ]
item2 = new Item(); → 堆: [vec1_buf][vec2_buf][item1][item2 ]

delete item1, item2:
堆: [vec1_buf][vec2_buf][ free ][ free ]

堆顶被vec2_buf钉住,brk无法trim
中间free块在free list,OS看不到
RSS = 全部,实际只用 vec1+vec2 缓冲区

批量分配 (无RSS Ghost):
items.reserve(10000);
for (...) items.push_back(new Item());
堆: [items_buf][item1][item2]...[item10000]

delete all:
堆: [ 全部free ]
→ 堆顶无钉住对象,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)
freemunmapRSS 下降

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阈值有效