凌晨复盘时,VPS 的 RSS 已连续 7 天从 400MB 攀升到 800MB,内存翻倍却未触发 OOM;请求量已经回落,重启后几小时又复现。此时只盯 free 输出,很容易把缓存预热、容器限额和堆外内存误判成泄漏。下面按趋势、取证、复测交叉判断,让结论可复核。
如果你正在选机器,或者想先把线路和配置跑通再谈长期方案,可以先看看这几家的线路说明和技术参数:tmhhost(全球代理网络),先看清带宽线路、流量策略和退款条款,再决定要不要迁。
应用泄漏的典型特征,是业务对象失去引用后仍无法回收。Java 服务完成 Full GC 后,存活对象和堆基线仍逐轮抬升;堆转储类直方图中同类实例持续增加,支配树可定位长期引用链。此时 RSS 缓升只是结果,根因是可达对象异常增多。

缓存膨胀是程序主动保留数据。缓存键、索引或序列化缓冲区持续增长,缓存内存占比、命中率和淘汰速率却未闭环:只增不淘汰,或预热容量远大于真实工作集,都会推高 RSS。判断重点是缓存占用是否超过业务收益,不能只看一次 GC 后内存是否下降。
内存限额属于边界问题。进程可能贴近 cgroup 上限,或因瞬时大对象分配失败;内核日志、cgroup 事件与 OOM 记录会留下限额、分配栈或被杀进程。它不要求存在泄漏,单纯调大限额也不是修复。
先建立至少 7 天的 RSS增长曲线,同时采集 PSS、堆使用量、缓存大小、GC 次数和请求量。可用随身服务器定时采集,按进程拆分,把阶跃与发布、流量、预热和限额事件对齐。RSS 上涨只说明驻留页增加;PSS 可减少共享页误判,斜率和回落速度才有助区分持续泄漏与短时峰值。

再开启垃圾回收日志,核对 Full GC 前后存活对象、回收后基线与晋升速率。若存活量逐轮抬升,再抓堆转储取证,通过支配树、类直方图和对象路径寻找未释放的引用链;抓取前评估磁盘、停顿和敏感数据。若存活量稳定而缓存目录持续增大,应调整容量或淘汰策略。若曲线在固定阈值附近被截断,并出现 OOM事件溯因中的 cgroup 限制、分配失败或被杀记录,则优先处理限额与峰值分配。
常见坑包括把整机 page cache 归因于应用、把一次 Full GC 后下降当作根治、低流量时取证导致样本失真,以及只改 ulimit 而忽略 cgroup 上限。结论应同时有趋势、对象和回收行为证据;修复后按相同负载复测,确认 RSS、堆基线与缓存占比稳定回落。
不要只盯当前 RSS。先把RSS增长曲线按进程、容器和 cgroup 拆开,记录增长斜率、Full GC 后基线及异常时间点;再用 /proc/PID/status、smaps_rollup 和 cgroup memory.stat 区分匿名内存、文件映射与页缓存。曲线持续上升且回收后基线不回落,才更像泄漏;短时尖峰可能只是流量、批处理或缓存预热。
开启并保留垃圾回收日志,将每次 GC 的时间、耗时、回收前后用量与 RSS 采样对齐。若老年代在 Full GC 后仍逐轮抬升,应检查大对象滞留和引用链;若堆稳定而 RSS 上涨,则继续排查 DirectByteBuffer、线程栈、JNI 或本地库分配。容器达到限制时可能先触发频繁 GC,不能把“GC 变多”直接等同于“堆泄漏”。
取证时在可承受停顿的窗口生成堆转储取证文件,用 MAT 等工具比较 Dominator Tree、Retained Heap 与 GC Roots;最好间隔一段时间取两份,观察同类对象是否持续保留。堆转储看不到本地内存,高内存压力下生成文件还可能卡顿,因此应控制采样频率,并及时脱敏、转移文件。系统页缓存与业务缓存也要分开:前者通常可由内核回收,后者可能常驻堆内,应结合缓存内存占比及淘汰策略判断。

进行OOM事件溯因时,不要看到退出码 137 就下结论,它也可能来自人工终止。先汇总内核日志、容器 memory.events、memory.max、服务日志和监控曲线,确认被杀进程、触发时刻及当时内存构成。典型链路是分配速率上升、GC 抖动、RSS 触及上限、内核发送 SIGKILL;也可能是 cgroup 中其他进程越界或节点级压力导致。
最终,定位内存泄漏应形成“趋势发现回收验证对象取证限制核对复现修复”的闭环。每个结论都要能指向时间戳、指标和对象类型;否则即使 RSS 暂时下降,也可能只是流量回落或缓存被清空,并未真正消除根因。