上篇文章介绍了Netty内存模型原理,因为Netty在使用不当会致使堆外内存泄漏,网上关于这方面的资料比较少,因此写下这篇文章,专门介绍排查Netty堆外内存相关的知识点,诊断工具,以及排查思路提供参考html
堆外内存泄漏的现象主要是,进程占用的内存较高(Linux下能够用top命令查看),但Java堆内存占用并不高(jmap命令查看),常见的使用堆外内存除了Netty,还有基于java.nio下相关接口申请堆外内存,JNI调用等,下面侧重介绍Netty堆外内存泄漏问题排查java
Netty堆外内存是基于原生java.nio的DirectByteBuffer对象的基础上实现的,因此有必要先了解下它的释放原理git
java.nio提供的DirectByteBuffer提供了sun.misc.Cleaner类的clean()方法,进行系统调用释放堆外内存,触发clean()方法的状况有2种github
ByteBuffer buf = ByteBuffer.allocateDirect(1);
((DirectBuffer) byteBuffer).cleaner().clean();
复制代码
Cleaner类继承了java.lang.ref.Reference,GC线程会经过设置Reference的内部变量(pending变量为链表头部节点,discovered变量为下一个链表节点),将可被回收的不可达的Reference对象以链表的方式组织起来安全
Reference的内部守护线程从链表的头部(head)消费数据,若是消费到的Reference对象同时也是Cleaner类型,线程会调用clean()方法(Reference#tryHandlePending())bash
介绍noCleaner策略以前,须要先理解带有Cleaner对象的DirectByteBuffer在初始化时作了哪些事情:微信
只有在DirectByteBuffer(int cap)构造方法中才会初始化Cleaner对象,方法中检查当前内存是否超过容许的最大堆外内存(可由-XX:MaxDirectMemorySize配置)markdown
若是超出,则会先尝试将不可达的Reference对象加入Reference链表中,依赖Reference的内部守护线程触发能够被回收DirectByteBuffer关联的Cleaner的run()方法架构
若是内存仍是不足, 则执行 System.gc(),触发full gc,来回收堆内存中的DirectByteBuffer对象来触发堆外内存回收,若是仍是超过限制,则抛出java.lang.OutOfMemoryError(代码位于java.nio.Bits#reserveMemory()方法)并发
而Netty在4.1引入能够noCleaner策略:建立不带Cleaner的DirectByteBuffer对象,这样作的好处是绕开带Cleaner的DirectByteBuffer执行构造方法和执行Cleaner的clean()方法中一些额外开销,当堆外内存不够的时候,不会触发System.gc(),提升性能
hasCleaner的DirectByteBuffer和noCleaner的DirectByteBuffer主要区别以下:
构造器方式不一样: noCleaner对象:由反射调用 private DirectByteBuffer(long addr, int cap)建立 hasCleaner对象:由 new DirectByteBuffer(int cap)建立
释放内存的方式不一样 noCleaner对象:使用 UnSafe.freeMemory(address); hasCleaner对象:使用 DirectByteBuffer 的 Cleaner 的 clean() 方法
**note:**Unsafe是位于sun.misc包下的一个类,能够提供内存操做、对象操做、线程调度等本地方法,这些方法在提高Java运行效率、加强Java语言底层资源操做能力方面起到了很大的做用,但不正确使用Unsafe类会使得程序出错的几率变大,程序再也不“安全”,所以官方不推荐使用,并可能在将来的jdk版本移除
Netty在启动时须要判断检查当前环境、环境配置参数是否容许noCleaner策略(具体逻辑位于PlatformDependent的static代码块),例如运行在Android下时,是没有Unsafe类的,不容许使用noCleaner策略,若是不容许,则使用hasCleaner策略
note: 能够调用PlatformDependent.useDirectBufferNoCleaner()方法查看当前Netty程序是否使用noCleaner策略
读到这里,也许有读者会问,若是Netty基于hasCleaner策略,经过GC触发Cleaner.clean(),自动回收堆外内存,是否是就能够不用考虑ByteBuf.release()方法的调用,不会内存泄漏?
固然不是,一方面缘由是自动触发不实时:须要ByteBuffer对象被GC线程回收才会触发,若是ByteBuffer对象进入老年代后才变得可回收,则须要等到发送频率较低老年代GC才会触发
另外一方面,Netty须要基于ByteBuf.release()方法执行其余操做,例如池化内存释放回内存池,不然该对象会被内存池一直标记为已使用
业界有一种误解认为 Netty 框架分配的 ByteBuf,框架会自动释放,业务不须要释放;业务建立的 ByteBuf 则须要本身释放,Netty 框架不会释放
产生这种误解是有缘由的,Netty框架是会在一些场景调用ByteBuf.release()方法:
当处理入站消息时,Netty会建立ByteBuf读取channel上的消息,并触发调用pipeline上的ChannelHandler处理,应用程序定义的使用ByteBuf的ChannelHandler须要负责release()
public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; try { ... } finally { buf.release(); } } 复制代码
若是该ByteBuf不禁当前ChannelHandler处理,则传递给pipeline上下一个handler:
public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; ... ctx.fireChannelRead(buf); } 复制代码
经常使用的咱们会经过继承ChannelInboundHandlerAdapter定义入站消息处理的handler,这种状况下若是全部程序的hanler都没有调用release()方法,该入站消息Netty最后并不会release(),会致使内存泄漏;
当在pipeline的handler处理中抛出异常以后,最后Netty框架是会捕捉该异常进行ByteBuf.release()的; 完整流程位于AbstractNioByteChannel.NioByteUnsafe#read(),下面抽取关键片断:
try { do { byteBuf = allocHandle.allocate(allocator); allocHandle.lastBytesRead(doReadBytes(byteBuf)); // 入站消息已读完 if (allocHandle.lastBytesRead() <= 0) { // ... break; } // 触发pipline上handler进行处理 pipeline.fireChannelRead(byteBuf); byteBuf = null; } while (allocHandle.continueReading()); // ... } catch (Throwable t) { // 异常处理中包括调用 byteBuf.release() handleReadException(pipeline, byteBuf, t, close, allocHandle); } 复制代码
不过,经常使用的还有经过继承SimpleChannelInboundHandler定义入站消息处理,在该类会保证消息最终被release:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { boolean release = true; try { // 该消息由当前handler处理 if (acceptInboundMessage(msg)) { I imsg = (I) msg; channelRead0(ctx, imsg); } else { // 不禁当前handler处理,传递给pipeline上下一个handler release = false; ctx.fireChannelRead(msg); } } finally { // 触发release if (autoRelease && release) { ReferenceCountUtil.release(msg); } } } 复制代码
不一样于入站消息是由Netty框架自动建立的,出站消息一般由应用程序建立,而后调用基于channel的write()方法或writeAndFlush()方法,这些方法内部会负责调用传入的byteBuf的release()方法
note: write()方法在netty-4.0.0.CR2前的版本存在问题,不会调用ByteBuf.release()
还有一种常见的误解就是,只要调用了ByteBuf的release()方法,或者ReferenceCountUtil.release()方法,对象的内存就保证释放了,其实不是
由于Netty的ByteBuf引用计数来管理ByteBuf对象的生命周期,ByteBuf继承了ReferenceCounted接口,对外提供retain()和release()方法,用于增长或减小引用计数值,当调用release()方法时,内部计数值被减为0才会触发内存回收动做
derived,派生的意思,在ByteBuf.duplicate(), ByteBuf.slice() 和 ByteBuf.order(ByteOrder) 等方法会建立出derived ByteBuf,建立出来的ByteBuf与原有ByteBuf是共享引用计数的,原有ByteBuf的release()方法调用,也会致使这些对象内存回收
相反ByteBuf.copy() 和 ByteBuf.readBytes(int)方法建立出来的对象并非derived ByteBuf,这些对象与原有ByteBuf不是共享引用计数的,原有ByteBuf的release()方法调用不会致使这些对象内存回收
配置堆外内存大小的参数有-XX:MaxDirectMemorySize和-Dio.netty.maxDirectMemory,这2个参数有什么区别?
note:-XX:MaxDirectMemorySize没法限制Netty中noCleaner策略的DirectByteBuffer堆外内存的大小
如何获取堆外内存的使用状况?
**note:**MXBean,Java提供的一系列用于监控统计的特殊Bean,经过不一样类型的MXBean能够获取JVM进程的内存,线程、类加载信息等监控指标
List<BufferPoolMXBean> bufferPoolMXBeans = ManagementFactoryHelper.getBufferPoolMXBeans(); BufferPoolMXBean directBufferMXBean = bufferPoolMXBeans.get(0); // hasCleaner的DirectBuffer的数量 long count = directBufferMXBean.getCount(); // hasCleaner的DirectBuffer的堆外内存占用大小,单位字节 long memoryUsed = directBufferMXBean.getMemoryUsed(); 复制代码
note: MappedByteBuffer:是基于FileChannelImpl.map进行进行mmap内存映射(零拷贝的一种实现)获得的另一种堆外内存的ByteBuffer,能够经过ManagementFactoryHelper.getBufferPoolMXBeans().get(1)获取到该堆外内存的监控指标
Netty也自带了内存泄漏检测工具,可用于检测出ByteBuf对象被GC回收,但ByteBuf管理的内存没有释放的状况,但不适用ByteBuf对象还没被GC回收内存泄漏的状况,例如任务队列积压
为了便于用户发现内存泄露,Netty提供4个检测级别:
使用方法是在命令行参数设置:
-Dio.netty.leakDetectionLevel=[检测级别]
复制代码
示例程序以下,设置检测级别为paranoid :
// -Dio.netty.leakDetectionLevel=paranoid public static void main(String[] args) { for (int i = 0; i < 500000; ++i) { ByteBuf byteBuf = UnpooledByteBufAllocator.DEFAULT.buffer(1024); byteBuf = null; } System.gc(); } 复制代码
能够看到控制台输出泄漏报告:
十二月 27, 2019 8:37:04 上午 io.netty.util.ResourceLeakDetector reportTracedLeak 严重: LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information. Recent access records: Created at: io.netty.buffer.UnpooledByteBufAllocator.newDirectBuffer(UnpooledByteBufAllocator.java:96) io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187) io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178) io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:115) org.caison.netty.demo.memory.BufferLeaksDemo.main(BufferLeaksDemo.java:15) 复制代码
内存泄漏的原理是利用弱引用,弱引用(WeakReference)建立时须要指定引用队列(refQueue),经过将ByteBuf对象用弱引用包装起来(代码入口位于AbstractByteBufAllocator#toLeakAwareBuffer()方法)
当发生GC时,若是GC线程检测到ByteBuf对象只被弱引用对象关联,会将该WeakReference加入refQueue; 当ByteBuf内存被正常释放,会调用WeakReference的clear()方法解除对ByteBuf的引用,后续GC线程不会再将该WeakReference加入refQueue;
Netty在每次建立ByteBuf时,基于抽样率,抽样命中时会轮询(poll)refQueue中的WeakReference对象,轮询返回的非null的WeakReference关联的ByteBuf即为泄漏的堆外内存(代码入口位于ResourceLeakDetector#track()方法)
在代码获取堆外内存的基础上,经过自定义接入一些监控工具定时检测获取,绘制图形便可,例如比较流行的Prometheus或者Zabbix
也能够经过jdk自带的Visualvm获取,须要安装Buffer Pools插件,底层原理是访问MXBean中的监控指标,只能获取hasCleaner的DirectByteBuffer的使用状况
此外,对于JNI调用产生的堆外内存分配,可使用google-perftools进行监控
堆外内存泄漏的具体缘由比较多,先介绍任务队列堆积的监控,再介绍通用堆外内存泄漏诊断思路
这里的任务队列是值NioEventLoop中的Queue taskQueue,提交到该任务队列的场景有:
ctx.channel().eventLoop().execute(runnable);
复制代码
channel.write(...)
channel.writeAndFlush(...)
复制代码
ctx.channel().eventLoop().schedule(runnable, 60, TimeUnit.SECONDS); 复制代码
当队列中积压任务过多,致使消息不能对channel进行写入而后进行释放,会致使内存泄漏
诊断思路是对任务队列中的任务数、积压的ByteBuf大小、任务类信息进行监控,具体监控程序以下(代码地址 github.com/caison/cais…):
public void channelActive(ChannelHandlerContext ctx) throws NoSuchFieldException, IllegalAccessException { monitorPendingTaskCount(ctx); monitorQueueFirstTask(ctx); monitorOutboundBufSize(ctx); } /** 监控任务队列堆积任务数,任务队列中的任务包括io读写任务,业务程序提交任务 */ public void monitorPendingTaskCount(ChannelHandlerContext ctx) { int totalPendingSize = 0; for (EventExecutor eventExecutor : ctx.executor().parent()) { SingleThreadEventExecutor executor = (SingleThreadEventExecutor) eventExecutor; // 注意,Netty4.1.29如下版本本pendingTasks()方法存在bug,致使线程阻塞问题 // 参考 https://github.com/netty/netty/issues/8196 totalPendingSize += executor.pendingTasks(); } System.out.println("任务队列中总任务数 = " + totalPendingSize); } /** 监控各个堆积的任务队列中第一个任务的类信息 */ public void monitorQueueFirstTask(ChannelHandlerContext ctx) throws NoSuchFieldException, IllegalAccessException { Field singleThreadField = SingleThreadEventExecutor.class.getDeclaredField("taskQueue"); singleThreadField.setAccessible(true); for (EventExecutor eventExecutor : ctx.executor().parent()) { SingleThreadEventExecutor executor = (SingleThreadEventExecutor) eventExecutor; Runnable task = ((Queue<Runnable>) singleThreadField.get(executor)).peek(); if (null != task) { System.out.println("任务队列中第一个任务信息:" + task.getClass().getName()); } } } /** 监控出站消息的队列积压的byteBuf大小 */ public void monitorOutboundBufSize(ChannelHandlerContext ctx) { long outBoundBufSize = ((NioSocketChannel) ctx.channel()).unsafe().outboundBuffer().totalPendingWriteBytes(); System.out.println("出站消息队列中积压的buf大小" + outBoundBufSize); } 复制代码
- note: 上面程序至少须要基于Netty4.1.29版本才能使用,不然有性能问题
实际基于Netty进行业务开发,耗时的业务逻辑代码应该如何处理?
先说结论,建议自定义一组新的业务线程池,将耗时业务提交业务线程池
Netty的worker线程(NioEventLoop),除了做为NIO线程处理链接数据读取,执行pipeline上channelHandler逻辑,另外还有消费taskQueue中提交的任务,包括channel的write操做。
若是将耗时任务提交到taskQueue,也会影响NIO线程的处理还有taskQueue中的任务,所以建议在单独的业务线程池进行隔离处理
Netty堆外内存泄漏的缘由多种多样,例如代码漏了写调用release();经过retain()增长了ByteBuf的引用计数值而在调用release()时引用计数值未清空;由于Exception致使未能release();ByteBuf引用对象提早被GC,而关联的堆外内存未能回收等等,这里没法所有列举,因此尝试提供一套通用的诊断思路提供参考
首先,须要能复现问题,为了避免影响线上服务的运行,尽可能在测试环境或者本地环境进行模拟。但这些环境一般没有线上那么大的并发量,能够经过压测工具来模拟请求
对于有些没法模拟的场景,能够经过Linux流量复制工具将线上真实的流量复制到到测试环境,同时不影响线上的业务,相似工具备Gor、tcpreplay、tcpcopy等
能复现以后,接下来就要定位问题所在,先经过前面介绍的监控手段、日志信息试试能不能直接找到问题所在; 若是找不到,就须要定位出堆外内存泄漏的触发条件,但有时应用程序比较庞大,对外提供的流量入口不少,没法逐一排查。
在非线上环境的话,能够将流量入口注释掉,每次注释掉一半,而后再运行检查问题是否还存在,若是存在,继续再注释掉剩下的一半,经过这种二分法的策略经过几回尝试能够很快定位出问题触发条件
定位出触发条件以后,再检查程序中在该触发条件处理逻辑,若是该处理程序很复杂,没法直接看出来,还能够继续注释掉部分代码,二分法排查,直到最后找出具体的问题代码块
整套思路的核心在于,问题复现、监控、排除法,也能够用于排查其余问题,例如堆内内存泄漏、CPU 100%,服务进程挂掉等
整篇文章侧重于介绍知识点和理论,缺乏实战环节,这里分享一些优质博客文章:
《netty 堆外内存泄露排查盛宴》 闪电侠手把手带如何debug堆外内存泄漏 www.jianshu.com/p/4e96beb37…
《Netty防止内存泄漏措施》,Netty权威指南做者,华为李林峰内存泄漏知识分享 mp.weixin.qq.com/s/IusIvjrth…
《疑案追踪:Spring Boot内存泄露排查记》,美团技术团队纪兵的案例分享 mp.weixin.qq.com/s/aYwIH0TN3…
《Netty入门与实战:仿写微信 IM 即时通信系统》,闪电侠的掘金小册(付费),我的就是学这个专栏入门Netty的 juejin.cn/book/684473…
更多精彩,欢迎关注公众号 分布式系统架构