java进程内存持续升高但gc不回收,大概率是堆外内存失控或容器层缓存堆积,而非堆内泄漏;需先用jstat -gc查ou/oc比值、jmap -heap核对堆使用率,并通过/proc/pid/status的vmrss与-xmx对比确认是否堆外问题。

Java进程内存持续升高但GC不回收,大概率不是堆内对象泄漏,而是堆外内存失控或容器层缓存堆积——先别急着 dump 堆快照。
确认是不是 JVM 堆内存真在涨
很多人一看到 top 里 RES 高就默认是堆问题,其实错得离谱。JVM 堆只是内存的一部分,RES 还包含堆外内存、线程栈、JIT 代码缓存、直接内存(ByteBuffer.allocateDirect)、甚至 mmap 映射的文件。
- 用
jstat -gc <pid></pid>看OU(老年代使用量)和OC(老年代容量):如果OU/OC稳定在 30%~60%,堆本身没涨,问题不在堆内 - 用
jmap -heap <pid></pid>对比used和max,确认堆是否真的接近上限 - 用
cat /proc/<pid>/status | grep -E "VmRSS|VmSize"</pid>查看进程实际物理内存(VmRSS)和虚拟内存(VmSize),若VmRSS远大于堆最大值(比如堆设了-Xmx4g,但VmRSS达到 12g),说明堆外有大头
重点查堆外内存和 Linux 页面缓存
生产环境最常被忽略的元凶:日志文件写入触发的 page cache 占用。Linux 把刚写入的文件页缓存在内存里,free -h 显示的 cached 或 buff/cache 会悄悄吃掉几 GB,而 top 的 RES 也会同步虚高。
- 执行
ls -lh /var/log/your-app/*.log看日志体积,5G+ 日志很常见,它会大量驻留 page cache - 用
cat /proc/meminfo | grep -i "cached\|buffers"查缓存总量;再用grep -i "pgpgin\|pgpgout\|pgmajfault" /proc/vmstat看近期 I/O 活动是否密集 - 临时验证:执行
sync && echo 3 > /proc/sys/vm/drop_caches(仅测试环境!),观察top中 RES 是否明显下降——如果掉了 3G+,基本就是它 - 更稳妥的长期方案:改用
logback的RollingFileAppender配置maxHistory和totalSizeCap,避免日志无节制膨胀
排查堆外内存泄漏(DirectByteBuffer、NIO、JNI)
一旦确认 VmRSS > 堆上限 + 元空间 + 线程栈总和,就要怀疑堆外泄漏。常见于 Netty、Kafka 客户端、自定义 ByteBuffer 使用、或 JNI 调用未释放资源。
- 用
jcmd <pid> VM.native_memory summary</pid>查堆外内存分布(需启动时加-XX:NativeMemoryTracking=summary);关注Internal和Other区域是否异常增长 - 检查代码中是否有
ByteBuffer.allocateDirect且未调用.cleaner().clean()(虽然不推荐手动触发),或是否在循环中反复申请未释放 - Netty 用户重点看
PooledByteBufAllocator是否配置不当,或ResourceLeakDetector.setLevel开启后日志里有没有 leak report - 用
pstack <pid> | grep -c "java"</pid>粗略判断线程数是否过多(每个线程默认栈 1MB),再用cat /proc/<pid>/maps | grep -i "anon\|heap" | wc -l</pid>看匿名映射段数量,暴涨说明可能有大量 direct buffer 或 mmap
别让 jmap -dump 成为压垮骆驼的最后一根稻草
在内存已经吃紧的容器里执行 jmap -dump,极大概率触发 OOM Killer 杀掉 Java 进程——因为 dump 过程需要额外内存拷贝堆数据,而容器内存限制不会因此放宽。
- 绝对不要在内存告警中直接跑
jmap -dump:format=b,file=heap.hprof <pid></pid> - 如果必须 dump,先用
jmap -histo:live <pid></pid>快速看前 20 类实例数,能定位大部分静态集合泄漏 - 真正要 dump 时,务必加
-J-Xmx2g限制 JVM 自身内存(如jmap -dump:format=b,file=heap.hprof -J-Xmx2g <pid></pid>),并确保容器剩余内存 >3G - 更安全的替代:用
jcmd <pid> VM.native_memory detail</pid>(带 NMT)或async-profiler采样堆外分配热点,无需暂停进程
真正难缠的内存问题,往往卡在「你以为在查堆,其实堆很干净」这个认知盲区。page cache、direct buffer、native code 里的 malloc,这些都不归 GC 管——盯住 /proc/<pid>/status</pid> 和 /proc/meminfo,比死磕 jmap 有用得多。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











