某核心应用的负责同学反馈应用存在少量机器OOM被OS kill的问题。看sunfire监控信息,的确如此。
初步收集到的信息:
容器内存=8G,Java 11,G1 GC=4G,MaxDirectMemorySize=1G。详见下图:
业务同学已经做过Java dump,可以看到堆外对象几乎没有,堆内的使用量也不大,<3G。上机器查看Java进程的内存使用量的确很大:
通过目前掌握到的信息来看,4G(Java堆)+1G(堆外)+512M(元空间)+250M(CodeCache)+其它,离6.8G还是有不少差距,无法简单的明确原因,需要深入排查分析了。
问题结论
省流版
中间件中多个不同的ClassLoader加载了多个netty的io.netty.buffer.PooledByteBufAllocator,每一个都有1G的内存配额,所以存在实际使用的堆外内存超出1G限制的问题。
通过Arthas可以看到存在这个类的7个不同的实例:
而其中rocketmq-client的这一个,已经基本用完1G的内存(其它几个使用量大多在100多M的样子):
详细版
中间件中多个不同的ClassLoader加载了多个netty的io.netty.buffer.PooledByteBufAllocator,每个Allocator都用自己的计数器在限制堆外内存的使用量,这个限制值大多数情况下取值至MaxDirectMemorySize,所以会存在无法限制堆外内存使用量在1G以内的问题。
这个应用是饿了么弹内的应用,io.netty.buffer.PooledByteBufAllocator,有7个ClassLoader加载了它,分别:
● sentinel's ModuleClassLoader:流量监控软件
● rocketmq-client's ModuleClassLoader:消息中间件
● tair-plugin's ModuleClassLoader:云数据库
● hsf's ModuleClassLoader:远程调用(类似dubbo)
● XbootModuleClassLoader
● pandora-qos-service's ModuleClassLoader:类似springboot
● ele-enhancer's ModuleClassLoader
相比弹内应用的4个(数据来自淘天集团的核心应用ump2,如下图),多了3个。
在Java8,以及Java11中(JVM参数设置了-Dio.netty.tryReflectionSetAccessible=true过后),netty会直接使用unsafe的方法申请堆外内存,不通过Java的DirectMemory分配API,所以通过监控看不到堆外内存的占用量,也不受JVM MaxDirectMemorySize的管控。
查看DirectByteBuffer实现代码可以发现,它限制MaxDirectMemorySize的方法是在Java层(代码标记处1),实际上在JVM底层是没有任何限制的,netty是直接用了这里代码标记处2的API分配内存。
排查过程
1.1.通过NativeMemoryTracking看Native内存的占用分布
通过在JVM参数上加上-XX:NativeMemoryTracking=detail,就可以打印出详细的内存分类的占用信息了,观察了一整天,发现主要的可疑变化是在Other部分,即堆外的部分,如下图。( Java NMT的详细使用可以参考相应的技术文章)
明明是限制的堆外1G,怎么超过了这么多。再多观察一会,发现它还会继续缓慢上涨的,最高达到接近1.5GB。这就和最开始查看Java进程的RSS占用对上了。