Java教程:9种常见的CMS GC问题详解(十二)

开课吧开课吧锤锤2021-03-24 11:03

    Java编程语言是一种简单、面向对象、分布式、解释型、健壮安全、与系统无关、可移植、高性能、多线程和动态的语言。如今Java已经广泛应用于各个领域的编程开发。

Java

    4.8场景八:堆外内存OOM

    4.8.1现象

    内存使用率不断上升,甚至开始使用SWAP内存,同时可能出现GC时间飙升,线程被Block等现象,通过top命令发现Java进程的RES甚至超过了-Xmx的大小。出现这些现象时,基本可以确定是出现了堆外内存泄漏。

    4.8.2原因

    JVM的堆外内存泄漏,主要有两种的原因:

    通过UnSafe#allocateMemory,ByteBuffer#allocateDirect主动申请了堆外内存而没有释放,常见于NIO、Netty等相关组件。

    代码中有通过JNI调用NativeCode申请的内存没有释放。

    4.8.3策略

    哪种原因造成的堆外内存泄漏?

    首先,我们需要确定是哪种原因导致的堆外内存泄漏。这里可以使用NMT(NativeMemoryTracking)进行分析。在项目中添加-XX:NativeMemoryTracking=detailJVM参数后重启项目(需要注意的是,打开NMT会带来5%~10%的性能损耗)。使用命令jcmdpidVM.native_memorydetail查看内存分布。重点观察total中的committed,因为jcmd命令显示的内存包含堆内内存、Code区域、通过Unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包含其他NativeCode(C代码)申请的堆外内存。

    如果total中的committed和top中的RES相差不大,则应为主动申请的堆外内存未释放造成的,如果相差较大,则基本可以确定是JNI调用造成的。

    原因一:主动申请未释放

    JVM使用-XX:MaxDirectMemorySize=size参数来控制可申请的堆外内存的最大值。在Java8中,如果未配置该参数,默认和-Xmx相等。

    NIO和Netty都会取-XX:MaxDirectMemorySize配置的值,来限制申请的堆外内存的大小。NIO和Netty中还有一个计数器字段,用来计算当前已申请的堆外内存大小,NIO中是java.nio.Bits#totalCapacity、Netty中io.netty.util.internal.PlatformDependent#DIRECT_MEMORY_COUNTER。

    当申请堆外内存时,NIO和Netty会比较计数器字段和最大值的大小,如果计数器的值超过了最大值的限制,会抛出OOM的异常。

    NIO中是:OutOfMemoryError:Directbuffermemory。

    Netty中是:OutOfDirectMemoryError:failedtoallocatecapacitybyte(s)ofdirectmemory(used:usedMemory,max:DIRECT_MEMORY_LIMIT)。

    我们可以检查代码中是如何使用堆外内存的,NIO或者是Netty,通过反射,获取到对应组件中的计数器字段,并在项目中对该字段的数值进行打点,即可准确地监控到这部分堆外内存的使用情况。

    此时,可以通过Debug的方式确定使用堆外内存的地方是否正确执行了释放内存的代码。另外,需要检查JVM的参数是否有-XX:+DisableExplicitGC选项,如果有就去掉,因为该参数会使System.gc失效。(场景二:显式GC的去与留)

    原因二:通过JNI调用的NativeCode申请的内存未释放

    这种情况排查起来比较困难,我们可以通过Googleperftools+Btrace等工具,帮助我们分析出问题的代码在哪里。

    gperftools是Google开发的一款非常实用的工具集,它的原理是在Java应用程序运行时,当调用malloc时换用它的libtcmalloc.so,这样就能对内存分配情况做一些统计。我们使用gperftools来追踪分配内存的命令。如下图所示,通过gperftools发现Java_java_util_zip_Inflater_init比较可疑。

Java

    接下来可以使用Btrace,尝试定位具体的调用栈。Btrace是Sun推出的一款Java追踪、监控工具,可以在不停机的情况下对线上的Java程序进行监控。如下图所示,通过Btrace定位出项目中的ZipHelper在频繁调用GZIPInputStream,在堆外内存分配对象。

Java

    最终定位到是,项目中对GIPInputStream的使用错误,没有正确的close()。

Java

    除了项目本身的原因,还可能有外部依赖导致的泄漏,如Netty和SpringBoot,详细情况可以学习下这两篇文章,SpringBoot引起的“堆外内存泄漏”排查及经验总结、Netty堆外内存泄露排查盛宴。

    4.8.4小结

    首先可以使用NMT+jcmd分析泄漏的堆外内存是哪里申请,确定原因后,使用不同的手段,进行原因定位。

Java

    以上内容由开课吧老师、新宇、湘铭、祥璞提供,更多Java教程尽在开课吧广场Java教程频道。

有用
分享