凌晨巡检一台2GB的VPS,业务请求量没有明显变化,某个工作进程的RSS驻留内存却从320MB连续爬到1.4GB,最终触发OOM,进程被内核杀掉。更棘手的是,free显示仍有可用内存,页缓存也能被回收,现场重启后问题又暂时消失。真正要查的不是“内存还剩多少”,而是哪类占用在增长、增长后能否回收,以及持续保留对象的分配调用栈藏在哪里。
如果你正在选机器,或者想先把线路和配置跑通再谈长期方案,可以先看看这几家的线路说明和技术参数:hhost(海外服务器),先看清带宽线路、流量策略和退款条款,再决定要不要迁。
一、先分清应用缓存、进程开销与内核占用
判断泄漏前,先把内存归到三个账本。应用缓存是为提速主动保存的数据,例如结果缓存、连接池和对象池;它通常随流量呈锯齿形增长,在访问峰值后应逐步回落。若缓存没有容量上限、TTL过期机制或LRU、LFU等缓存淘汰策略,看似正常的缓存也会演变成持续增长。
进程开销包括堆对象、线程栈、代码页、共享库及分配器保留的空闲内存。RSS上涨并不等于堆里还有更多活对象:某些分配器释放对象后仍保留内存页供后续复用,表现为RSS不降,但对象数量可能已经稳定。线程数、协程数与连接数也要同步核对,避免把基础设施开销误判成缓存泄漏。
内核占用主要来自页缓存、目录项、inode、socket缓冲区及内核对象。在容器环境中,还应查看cgroup的memory.stat,不能直接用宿主机free、top的汇总值推断进程状态。这样拆分后,才能确定增长发生在用户态堆、应用缓存,还是内核记账路径。

二、沿增长趋势追到堆快照和分配栈
先固定相同负载,在相同时间间隔采集RSS、堆使用量、对象数、线程数和连接数,形成内存增长趋势。周期回落说明对象能够释放;阶梯式上涨且阶梯不断抬升,才值得怀疑保留链。比较稳妥的做法是持续观察多个业务周期,而不是只比较重启前后两个点,否则很容易把缓存高水位或分配器保留误报为泄漏。
随后在增长点采集堆内存快照,按对象大小、保留路径和引用关系排序,并用两次快照做差分。重点找“本轮新增且快照后仍被引用”的对象,再沿其保留路径找到分配位置。运行时分配工具记录的分配调用栈可补足快照信息,但要区分累计分配量与仍存活对象:调用栈高频出现只说明创建频繁,不一定说明对象泄漏。
验证时,应记录基线,实施缓存容量限制、引用释放或连接池收缩,再用原流量重复压测多个周期。若对象数下降、RSS最终进入稳定平台,且cgroup和内核占用没有同步异常,才算形成“采样定位修复复测”的闭环。常见坑包括只盯RSS、不核对堆存活量,以及忽略快照采集本身可能暂停进程并改变内存状态。

由增长趋势反查分配来源
先区分RSS驻留内存与堆占用:RSS还包含线程栈、共享库、mmap文件、直接内存及页缓存,增长可能来自内核回收延迟,而非业务对象持续增多。应按固定采样间隔记录内存增长趋势,同时采集进程RSS、cgroup memory.current、堆使用量和分配速率;若曲线只呈阶梯状,优先检查定时任务、连接池扩容与批量导入,而不是立即重启进程。
拿到堆内存快照后,先比较相邻快照的存活对象及保留大小,再从高占比对象回溯到分配调用栈。Java可结合JFR、Native Memory Tracking和MAT;Go可用pprof的heap、allocs查看对象数量与调用栈;C/C++可使用heap profiler、jemalloc profiling或Valgrind。快照只能证明对象“仍被引用”,不能单独证明泄漏;采样期间的短命大对象应结合allocs profile判断。

让缓存增长纳入淘汰设计
缓存淘汰策略不能只看命中率。对象大小差异大时,应同时设置条目数、总字节数和TTL;LRU适合访问局部性明显的场景,LFU更适合热点稳定的场景,扫描型任务则可能造成缓存污染。可采用TinyLFU或分段淘汰,并限制单次写入批量。缓存达到上限后,应明确拒绝、写穿或降级,避免“自动扩容”掩盖问题。
常见误区是free后RSS不降:分配器保留空闲页供复用,未必是真泄漏;先验证堆存活对象,再考虑定期释放或调整arena。容器内还要排除tmpfs、页缓存及fork后的写时复制;cgroup统计可能包含这些内存,因此OOM分析与进程堆分析必须对照进行。
- 确认采样口径:RSS、堆和cgroup分开记录。
- 复现问题并标注流量、任务周期及版本。
- 连续采集heap或allocs,比较增长斜率。
- 按对象大小和分配调用栈排序。
- 核对缓存字节上限、TTL及淘汰命中率。
- 压测后等待多轮GC,再判断内存是否回落。
- 保留原始数据、快照和变更记录。
检查完成的标准不是“内存暂时下降”,而是增长斜率稳定、快照中没有异常支配对象、缓存命中与字节成本可解释,并且在限流或扩容后系统仍能按预算运行。


喜欢
顶
难过
囧
围观
无聊





