衡阳网站建设技术外包,郑州做网站比较专业的机构,广州购物网站建设报价,网站如何更新上篇文章介绍了Netty内存模型原理#xff0c;由于Netty在使用不当会导致堆外内存泄漏#xff0c;网上关于这方面的资料比较少#xff0c;所以写下这篇文章#xff0c;专门介绍排查Netty堆外内存相关的知识点#xff0c;诊断工具#xff0c;以及排查思路提供参考现象堆外内…上篇文章介绍了Netty内存模型原理由于Netty在使用不当会导致堆外内存泄漏网上关于这方面的资料比较少所以写下这篇文章专门介绍排查Netty堆外内存相关的知识点诊断工具以及排查思路提供参考现象堆外内存泄漏的现象主要是进程占用的内存较高(Linux下可以用top命令查看)但Java堆内存占用并不高(jmap命令查看)常见的使用堆外内存除了Netty还有基于java.nio下相关接口申请堆外内存JNI调用等下面侧重介绍Netty堆外内存泄漏问题排查堆外内存释放底层实现1 java.nio堆外内存释放Netty堆外内存是基于原生java.nio的DirectByteBuffer对象的基础上实现的所以有必要先了解下它的释放原理java.nio提供的DirectByteBuffer提供了sun.misc.Cleaner类的clean()方法进行系统调用释放堆外内存触发clean()方法的情况有2种(1) 应用程序主动调用ByteBuffer buf ByteBuffer.allocateDirect(1);((DirectBuffer) byteBuffer).cleaner().clean();(2) 基于GC回收Cleaner类继承了java.lang.ref.ReferenceGC线程会通过设置Reference的内部变量(pending变量为链表头部节点discovered变量为下一个链表节点)将可被回收的不可达的Reference对象以链表的方式组织起来Reference的内部守护线程从链表的头部(head)消费数据如果消费到的Reference对象同时也是Cleaner类型线程会调用clean()方法(Reference#tryHandlePending())2 Netty noCleaner策略介绍noCleaner策略之前需要先理解带有Cleaner对象的DirectByteBuffer在初始化时做了哪些事情只有在DirectByteBuffer(int cap)构造方法中才会初始化Cleaner对象方法中检查当前内存是否超过允许的最大堆外内存(可由-XX:MaxDirectMemorySize配置)如果超出则会先尝试将不可达的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() 方法noteUnsafe是位于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()方法执行其他操作例如池化内存释放回内存池否则该对象会被内存池一直标记为已使用ByteBuf.release()触发机制业界有一种误解认为 Netty 框架分配的 ByteBuf框架会自动释放业务不需要释放业务创建的 ByteBuf 则需要自己释放Netty 框架不会释放产生这种误解是有原因的Netty框架是会在一些场景调用ByteBuf.release()方法1 入站消息处理当处理入站消息时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上下一个handlerpublic 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定义入站消息处理在该类会保证消息最终被releaseOverridepublic 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上下一个handlerrelease false;ctx.fireChannelRead(msg);}} finally {// 触发releaseif (autoRelease release) {ReferenceCountUtil.release(msg);}}}2 出站消息处理不同于入站消息是由Netty框架自动创建的出站消息通常由应用程序创建然后调用基于channel的write()方法或writeAndFlush()方法这些方法内部会负责调用传入的byteBuf的release()方法note write()方法在netty-4.0.0.CR2前的版本存在问题不会调用ByteBuf.release()3 release()注意事项(1) 引用计数还有一种常见的误解就是只要调用了ByteBuf的release()方法或者ReferenceCountUtil.release()方法对象的内存就保证释放了其实不是因为Netty的ByteBuf引用计数来管理ByteBuf对象的生命周期ByteBuf继承了ReferenceCounted接口对外提供retain()和release()方法用于增加或减少引用计数值当调用release()方法时内部计数值被减为0才会触发内存回收动作(2) derived ByteBufderived派生的意思在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个参数有什么区别-XX:MaxDirectMemorySize用于限制Netty中hasCleaner策略的DirectByteBuffer堆外内存的大小默认值是JVM能从操作系统申请的最大内存如果内存本身没限制则值为Long.MAX_VALUE个字节(默认值由Runtime.getRuntime().maxMemory()返回)代码位于java.nio.Bits#reserveMemory()方法中note-XX:MaxDirectMemorySize无法限制Netty中noCleaner策略的DirectByteBuffer堆外内存的大小-Dio.netty.maxDirectMemory用于限制noCleaner策略下Netty的DirectByteBuffer分配的最大堆外内存的大小如果该值为0则使用hasCleaner策略代码位于PlatformDependent#incrementMemoryCounter()方法中堆外内存监控如何获取堆外内存的使用情况1 代码工具(1) hasCleaner的DirectByteBuffer监控对于hasCleaner策略的DirectByteBufferjava.nio.Bits类是有记录堆外内存的使用情况但是该类是包级别的访问权限不能直接获取可以通过MXBean来获取noteMXBeanJava提供的一系列用于监控统计的特殊Bean通过不同类型的MXBean可以获取JVM进程的内存线程、类加载信息等监控指标List 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)获取到该堆外内存的监控指标(2) noCleaner的DirectByteBuffer监控Netty中noCleaner的DirectByteBuffer的监控比较简单直接通过PlatformDependent.usedDirectMemory()访问即可2 Netty自带内存泄漏检测工具Netty也自带了内存泄漏检测工具可用于检测出ByteBuf对象被GC回收但ByteBuf管理的内存没有释放的情况但不适用ByteBuf对象还没被GC回收内存泄漏的情况例如任务队列积压为了便于用户发现内存泄露Netty提供4个检测级别disabled 完全关闭内存泄露检测simple 以约1%的抽样率检测是否泄露默认级别advanced 抽样率同simple但显示详细的泄露报告paranoid 抽样率为100%显示报告信息同advanced使用方法是在命令行参数设置-Dio.netty.leakDetectionLevel[检测级别]示例程序如下设置检测级别为paranoid // -Dio.netty.leakDetectionLevelparanoidpublic 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 its 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加入refQueueNetty在每次创建ByteBuf时基于抽样率抽样命中时会轮询(poll)refQueue中的WeakReference对象轮询返回的非null的WeakReference关联的ByteBuf即为泄漏的堆外内存(代码入口位于ResourceLeakDetector#track()方法)3 图形化工具在代码获取堆外内存的基础上通过自定义接入一些监控工具定时检测获取绘制图形即可例如比较流行的Prometheus或者Zabbix也可以通过jdk自带的Visualvm获取需要安装Buffer Pools插件底层原理是访问MXBean中的监控指标只能获取hasCleaner的DirectByteBuffer的使用情况此外对于JNI调用产生的堆外内存分配可以使用google-perftools进行监控堆外内存泄漏诊断堆外内存泄漏的具体原因比较多先介绍任务队列堆积的监控再介绍通用堆外内存泄漏诊断思路1 任务队列堆积这里的任务队列是值NioEventLoop中的QueuetaskQueue提交到该任务队列的场景有(1) 用户自定义普通任务ctx.channel().eventLoop().execute(runnable);(2) 对channel进行写入channel.write(...)channel.writeAndFlush(...)(3) 用户自定义定时任务ctx.channel().eventLoop().schedule(runnable, 60, TimeUnit.SECONDS);当队列中积压任务过多导致消息不能对channel进行写入然后进行释放会导致内存泄漏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/8196totalPendingSize 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) 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中的任务因此建议在单独的业务线程池进行隔离处理2 通用诊断思路Netty堆外内存泄漏的原因多种多样例如代码漏了写调用release()通过retain()增加了ByteBuf的引用计数值而在调用release()时引用计数值未清空因为Exception导致未能release()ByteBuf引用对象提前被GC而关联的堆外内存未能回收等等这里无法全部列举所以尝试提供一套通用的诊断思路提供参考首先需要能复现问题为了不影响线上服务的运行尽量在测试环境或者本地环境进行模拟。但这些环境通常没有线上那么大的并发量可以通过压测工具来模拟请求对于有些无法模拟的场景可以通过Linux流量复制工具将线上真实的流量复制到到测试环境同时不影响线上的业务类似工具有Gor、tcpreplay、tcpcopy等能复现之后接下来就要定位问题所在先通过前面介绍的监控手段、日志信息试试能不能直接找到问题所在如果找不到就需要定位出堆外内存泄漏的触发条件但有时应用程序比较庞大对外提供的流量入口很多无法逐一排查。在非线上环境的话可以将流量入口注释掉每次注释掉一半然后再运行检查问题是否还存在如果存在继续再注释掉剩下的一半通过这种二分法的策略通过几次尝试可以很快定位出问题触发条件定位出触发条件之后再检查程序中在该触发条件处理逻辑如果该处理程序很复杂无法直接看出来还可以继续注释掉部分代码二分法排查直到最后找出具体的问题代码块整套思路的核心在于问题复现、监控、排除法也可以用于排查其他问题例如堆内内存泄漏、CPU 100%服务进程挂掉等总结整篇文章侧重于介绍知识点和理论缺少实战环节这里分享一些优质博客文章《netty 堆外内存泄露排查盛宴》 闪电侠手把手带如何debug堆外内存泄漏https://www.jianshu.com/p/4e96beb37935