有一阵子我们一台应用服务器每到下午就卡,free -m 一看,内存快见底了,swap 开始往里灌。奇怪的是 top 按内存排序,最高的进程也就占 1 个多 G,加起来跟总量对不上。

一开始我怀疑是 cache。buff/cache 那行确实高,但 cache 是可以回收的,真吃紧的时候内核会自动释放,不至于把 swap 都逼出来。所以我判断肯定有进程在偷偷吃内存,只是 top 没显示出来。

先查 /proc/meminfo,发现 Slab 那项特别高,好几个 G。slab 是内核自己用的内存,一般不会这么大。第一反应:是不是内核有内存泄漏?查内核版本,没听说有相关 bug,感觉不太像。

后来想起一个土办法:把每个进程的 /proc//smaps 里 RSS 加起来,跟 free 显示的总用量对比,看差在哪。写了个小脚本循环累加,结果发现有个 Java 进程的 PSS 比 top 显示的高出一大截——top 里它 RSS 显示 1.2G,但 smaps 累加出来有 3.8G。

怎么回事?后来查明白了,是堆外内存(off-heap)。那进程用的连接池和某些库直接分配了 native memory,不走 JVM 堆,top 的 RSS 列其实包含这部分,但 JVM 的监控面板上看不到。而 top 里 1.2G 是它显示的 RSS——这里我当时其实搞混了,top 的 RSS 应该是包含堆外的。

重新捋一下:当时我盯着 top 的 RES 列看,1.2G,觉得没问题。但 smaps 累加出来 3.8G,说明这进程实际占用的物理内存比 top 显示的还多。差距来自哪里?可能是 top 显示的是进程初始的 RES 快照,而 smaps 是实时累加——反正结论是:这个 Java 进程实际吃了 3.8G,它配置的 Xmx 才 2G,堆外内存超了快一倍。

根因找到了:连接池参数配得太大,空闲连接全挂着,底层每建一个连接都开一块 native buffer,日积月累就吃满了。把连接池上限调小,重启服务,内存占用直接掉了 2G 多,下午再也不卡了。

这事的教训:top 看内存不靠谱,特别是 Java 应用,堆外内存 top 也能看到但容易忽略。真想查清谁在吃内存,就把 /proc/*/smaps 累加一遍,谁大谁小一目了然。另外监控别只看内存总量,连接池这种"看起来不占内存"的配置,往往才是真凶。