内存使用率不断上升,开始用 SWAP 内存,同时 GC 时间飙升,线程 Block ,top 命令发现 Java 进程RES 超过-Xmx 大小。确定堆外内存泄漏
ps:RES:resident memory usage 常驻内存:
1、进程当前用的内存大小,包含其他进程共享,不包括swap out
2、如申请100m的内存,实际用10m,只增长10m,与VIRT相反
3、统计加载的库文件所占内存大小
一、主要两种原因:
1)通过 UnSafe#allocateMemory,ByteBuffer#allocateDirect 主动申请堆外内存没释放,常见于 NIO、Netty 等相关组件。
2)代码通过 JNI 调用 Native Code 申请内存没释放
二、策略
哪种原因造成的堆外内存泄漏?
1)用 NMT(NativeMemoryTracking) 分析。在项目中添加 -XX:NativeMemoryTracking=detail JVM参数重启项目(NMT 会带来 5%~10% 的性能损耗)。
2)用命令 jcmd pid VM.native_memory detail 查看内存分布。重点观察 total 中的 committed,因为 jcmd 命令显示的内存包含堆内内存、Code 区域、通过 Unsafe.allocateMemory 和 DirectByteBuffer 申请的内存,但不包含其他 Native Code(C 代码)申请的堆外内存。
total 中 committed 和 top 中的 RES 相差不大,为主动申请未释放,相差较大,JNI 调用造成的
原因一:主动申请未释放
1、控制申请最大值 -XX:MaxDirectMemorySize=size 默认和 -Xmx 相等
2、NIO 和 Netty 都取 -XX:MaxDirectMemorySize 值限制大小。NIO 和 Netty 有计数器字段,计算已申请堆外内存大小,监控堆外内存使用情况,超过最大值限制,抛OOM
ps:NIO 中是 java.nio.Bits#totalCapacity、Netty中 io.netty.util.internal.PlatformDependent#DIRECT_MEMORY_COUNTER。
NIO 中是:OutOfMemoryError: Direct buffer memory。
Netty 中是:OutOfDirectMemoryError: failed to allocate capacity byte(s) of direct memory (used: usedMemory , max: DIRECT_MEMORY_LIMIT )。
3、解决:Debug确定是否释放内存。另外检查是否有System.gc
原因二:JNI 调 Native Code 申请的内存未释放
排查困难, Google perftools + Btrace分析问题代码在哪
gperftools 是 Google 开发的一款非常实用的工具集,它的原理是在 Java 应用程序运行时,当调用 malloc 时换用它的 libtcmalloc.so,这样就能对内存分配情况做一些统计。我们使用 gperftools 来追踪分配内存的命令。如下图所示,通过 gperftools 发现 Java_java_util_zip_Inflater_init 比较可疑。
接下来可以使用 Btrace,尝试定位具体的调用栈。Btrace 是 Sun 推出的一款 Java 追踪、监控工具,可以在不停机的情况下对线上的 Java 程序进行监控。如下图所示,通过 Btrace 定位出项目中的 ZipHelper 在频繁调用 GZIPInputStream ,在堆外内存分配对象。
最终定位到是,项目中对 GIPInputStream 的使用错误,没有正确的 close()。
除了项目本身的原因,还可能有外部依赖导致的泄漏,如 Netty 和 Spring Boot,详细情况可以学习下这两篇文章:《疑案追踪:Spring Boot内存泄露排查记》、《Netty堆外内存泄露排查盛宴》。
总结
用 NMT + jcmd 分析泄漏的堆外内存是哪里申请,确定原因后,用不同手段,进行原因定位
https://mp.weixin.qq.com/s/RFwXYdzeRkTG5uaebVoLQw