jvm调优四:netty堆外内存泄露
Posted 二周目Solo
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jvm调优四:netty堆外内存泄露相关的知识,希望对你有一定的参考价值。
底层通讯使用netty4.1版本,然而在使用的时候出现了oom异常,这里记录一次堆外内存泄露排查过程,方便后续定位。
一:知识点
堆外内存,就是非堆内内存空间,java可以通过Unsafe类的native方法进行操作,由于不受制于jvm管理,所以堆外内存对象生命周期结束时,需要手动释放。java还是借助于gc释放。
通过上图可以看到,即使堆外一个大的内存对象,在堆内表示可能都非常小,这就是冰山对象,在堆内占用内存非常小。如果触发了几次ygc后,对象bytebuf进入到老年代,由于老年代fullgc次数比较少,那么可能就无法触发回收操作。
在java中存在四种引用类型:
强引用
软引用
弱引用
虚引用:上面三种不做分析,感兴趣的可以查阅相关文档;在java中,directByteBuffer主要是通过虚引用判断堆内对象是否可达。jvm中会有一个单独的线程处理不可达对象,不可达对象添加到引用队列之前,会判断该对象是否为Cleanner。
接上文,堆外内存基于gc机制的回收。本身因为DirectByteBuffer对象很小,只要不触发fullgc,那么一大块的内存一直存在。这个时候,只能依靠jvm的System.gc()调用了。它会中断整个进程,并且当前线程停顿100ms,如果在100ms时间内没有完成,就会抛出oom异常。所以,不能在vm参数中设置:-DisableExplicitGC。
二:netty内存泄露检测
netty所谓的内存泄露,主要是针对池化的ByteBuf。ByteBuf对象在gc掉之前,没有调用release把底下的DirectByteBuffer或byte[]归还到池里。那netty本身也提供了检测机制:
LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()
所在当出现leak字样时,需要开启:
-Dio.netty.leakDetectionLevel=paranoid 进行检测
三:排查过程
1:问题
netty服务端在运行了一段时间后,报错日志如下:
错误代码段如下,看上去是超出DIRECT_MEMORY_LIMIT(netty直接内存)上限了:
2:初步定位
使用前文说的定位方法,(本示例使用jmap+jvvm)输出如下:
发现是PoolSubpage(PoolSubpage是netty内存管理中的最小单位)可能存在内存泄露leak,jvm的heap内存只用了几百M,但是物理内存却达到几个G,这更加验证了直接内存(heap堆外内存)泄露。于是重新观察服务端输出日志发现了下面这段leak字样:
见上文我们加入:-Dio.netty.leakDetectionLevel=paranoid重新跑一次server,一段时间后打印如下:
3:代码诊断
找到RouterServerHandler,发现主要处理逻辑都在channelRead方法里:
印象中,netty框架分配的ByteBuf框架会自动释放,业务线程不需要手动释放。但是上面的示例明显是有问题的,于是定位netty源码,发现:基于内存池请求的ByteBuf(主要是PooledDirectByteBuf和PooledHeapByteBuf),在netty 的nioEventLoop线程处理过程中,需要在业务SimpleChannelInboundHandler处理完请求后释放,实现它的抽象方法channelRead()
于是对netty自定义handler重新改造:
运行半小时发现系统无异常,问题解决。
以上是关于jvm调优四:netty堆外内存泄露的主要内容,如果未能解决你的问题,请参考以下文章