“物理内存假性泄露”实为内存碎片化所致,表现为RSS持续上涨、MemAvailable下降,但无堆外大对象堆积;需通过pmap、/proc/PID/status及smaps分析匿名映射分布与页大小,定位Netty等高频小分配路径,并采用内存池、限制分配上限及malloc_trim等策略缓解。

非阻塞模式下长期运行引发的“物理内存假性泄露”,本质不是真泄漏,而是内存碎片化导致分配器无法复用已释放的空闲块,被迫持续向内核申请新页——表现为 RSS 持续上涨、free -h 中 MemAvailable 持续走低,但 jstat 或 pmap 显示并无明显堆外大对象堆积。排查需跳出“找泄漏点”思维,聚焦内存布局与分配行为。
确认是否为外部碎片主导的假性增长
先排除真实泄漏:用 jstat -gc PID 观察 OU/OC 比值是否稳定(如长期 >0.95 且 Full GC 回收量极少,才倾向真实堆泄漏);若 OU 平稳、RSS 却涨,更可能是碎片问题。关键验证步骤:
- 执行 pmap -x PID | awk '$3 > 1024 {print $0}' | sort -k3nr | head -10,查看是否存在大量 1–4MB 的匿名映射(anon),它们是 ptmalloc 分配的小块堆内存,高度离散即碎片特征
- 用 cat /proc/PID/status | grep -E "VmPeak|VmSize|VmRSS" 对比:若 VmSize 远大于 VmRSS(例如差 2GB+),说明虚拟地址空间被大量小块占据,但物理页未全部映射——这是碎片化的典型信号
- 检查 /proc/PID/smaps 中 MMUPageSize 和 MMUPSP 字段:若大量区域标记为 4kB(而非 2MB),说明未启用大页,加剧小页管理开销与碎片敏感度
定位碎片高发的内存分配路径
非阻塞 I/O(如 Netty、libevent)常伴随高频、变长的小对象分配(ByteBuf、task wrapper、channel context),极易触发 ptmalloc 的外部碎片。重点查:
- Netty 用户:检查是否启用了 PooledByteBufAllocator;若禁用(使用 Unpooled),则每次 allocate 都走 malloc,碎片风险陡增
- 自定义线程池:确认 ThreadFactory 是否设置了 setUncaughtExceptionHandler,未捕获异常导致线程提前退出,其栈上局部分配的 buffer 无法归还到 pool
- JNI 调用:用 gstack PID | grep -c "jni_" + pmap -x PID | grep "rw-p" | awk '{sum += $3} END {print sum/1024 " MB"}',若 JNI 堆外分配频繁且未显式 free,会直接加剧物理页碎片
缓解与收敛策略(不依赖重启)
碎片无法实时“整理”,但可通过约束分配行为降低恶化速度:
- 强制使用内存池:Netty 加 -Dio.netty.allocator.type=pooled;C/C++ 服务改用 jemalloc 或 tcmalloc(它们对碎片更友好,支持 arena 合并)
- 限制单次分配上限:在 ByteBuf 分配逻辑中加入 size check,拒绝超 16KB 的单次 direct buffer 申请,改用 heap buffer + copy,避免大块直连内存打散物理页
- 定期触发内存紧缩:对使用 ptmalloc 的进程,可周期性调用 malloc_trim(0)(通过 JNI 或 LD_PRELOAD 注入),将 top chunk 归还内核——注意仅对长时间空闲的进程有效,生产环境慎用
监控与基线化(预防复发)
碎片问题具有隐蔽性和渐进性,必须建立可持续观测指标:
- 采集 /proc/PID/smaps_rollup 中 MMUPageSize 为 4kB 的 anon pages 数量,趋势上升即碎片加剧
- 用 vmstat 1 持续观察 pgmajfault:若每秒 >5,说明频繁缺页,间接反映物理页分散、TLB 命中率低
- 记录每次 Full GC 后的 java.lang:type=MemoryPool,name=PS-Old-Gen 的 Usage.used 与 Runtime.totalMemory() - Runtime.freeMemory() 差值,若差值持续扩大,说明 JVM 堆内碎片(对象分布稀疏)也在贡献 RSS 增长

















