
上一篇文章介绍了Netty内存模型的原理。由于Netty使用不当,将导致堆外内存泄漏。 Internet上关于此的信息相对较少,因此我写这篇文章专门介绍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()方法
ByteBuffer buf = ByteBuffer.allocateDirect(1);
((DirectBuffer) byteBuffer).cleaner().clean();
Cleaner类继承了java.lang.ref.Reference,GC线程将设置Reference的内部变量(待处理变量是链接列表的头节点,发现的变量是下一个链接列表节点),可以回收利用且无法访问的参考对象被组织在一个链表中
引用的内部守护线程从链接列表的开头消耗数据。如果消耗的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()来触发完整的gc,以回收堆内存中的DirectByteBuffer对象,以触发堆外内存恢复,如果仍然超出限制,则抛出java.lang。 OutOfMemoryError(代码位于java.nio.Bits#reserveMemory()方法中)
Netty在4. 1中引入了noCleaner策略:创建一个没有Cleaner的DirectByteBuffer对象。这样做的好处是,绕过Directer的Cleaner执行构造方法以及Cleaner的clean()方法中的一些额外开销。当堆外内存不足时,将不会触发System.gc()来提高性能
hasCleaner的DirectByteBuffer和noCleaner的DirectByteBuffer之间的主要区别如下:
注意:Unsafe是位于sun.misc包中的类,它可以提供本地方法,例如内存操作,对象操作,线程调度等。这些方法在提高Java操作效率和提高Java效率方面起着非常重要的作用。增强Java语言基础资源的操作能力。效果很好,但是错误使用Unsafe类会增加程序错误的可能性,并且该程序不再“安全”,因此政府不建议使用,并且可能在以后的jdk版本中删除
Netty启动时,需要判断当前环境和环境配置参数是否允许使用noCleaner策略(特定逻辑位于PlatformDependent的静态代码块中)。例如,在Android下运行时,没有Unsafe类并且不允许noCleaner策略。如果不允许,请使用hasCleaner策略
注意:您可以调用PlatformDependent.useDirectBufferNoCleaner()方法来检查当前Netty程序是否使用noCleaner策略
阅读此内容后,一些读者可能会问Netty是否基于hasCleaner策略通过GC触发Cleaner.clean()并自动回收堆外内存,是否有可能忽略ByteBuf.release()的调用方法?内存会泄漏吗?
当然不是。一方面,原因是自动触发不是实时的:GC线程需要回收ByteBuffer对象才能触发。如果ByteBuffer对象在输入旧数据后变得可回收,则需要等待直到旧GC的发送频率将触发
另一方面,Netty需要基于ByteBuf.release()方法执行其他操作,例如将池化的内存释放回内存池,否则该对象将始终被标记为已被内存池使用
ByteBuf.release()触发机制
在行业中存在一个误解,即由Netty框架分配的ByteBuf将被自动释放,而业务则不需要被释放;业务创建的ByteBuf需要自行释放,而Netty框架不会释放它。

这种误解是有原因的。在某些情况下,Netty框架将调用ByteBuf.release()方法:
1入站邮件处理
在处理入站消息时,Netty将创建一个ByteBuf来读取通道上的消息,并触发对管道上ChannelHandler的调用以对其进行处理。由使用ByteBuf的应用程序定义的ChannelHandler需要负责release()
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
...
} finally {
buf.release();
}
}
如果当前ChannelHandler未处理ByteBuf,它将被传递到管道中的下一个处理程序:
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
...
ctx.fireChannelRead(buf);
}
很常见,我们将通过继承ChannelInboundHandlerAdapter定义用于入站消息处理的处理程序。在这种情况下,如果所有程序的处理程序都未调用release()方法,则入站消息Netty最终不会释放(),这将导致内存泄漏;
当在管道处理程序处理中引发异常时,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来定义入站消息处理,这将确保最终释放消息:
@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);
}
}
}
2出站邮件处理
与Netty框架自动创建的入站消息不同,出站消息通常由应用程序创建,然后调用基于通道的write()方法或writeAndFlush()方法。这些方法在内部负责调用传入的byteBuf release()方法
注意:netty- 4. 0. 0. CR2之前的版本中的write()方法存在问题,并且不会调用ByteBuf.release()
3 release()注意事项
还有一个常见的误解,即只要调用ByteBuf的release()方法或ReferenceCountUtil.release()方法,就可以保证对象的内存被释放,但是不会被释放。
由于Netty的ByteBuf引用计数管理ByteBuf对象的生命周期,因此在调用release()方法时,ByteBuf继承了ReferenceCounted接口并提供了外部keep()和release()方法来增加或减少引用计数值。 ,内部计数值减小为0以触发内存回收操作
派生的意思是派生的。在ByteBuf.duplicate(),ByteBuf.slice()和ByteBuf.order(ByteOrder)方法中,将创建派生的ByteBuf。创建的ByteBuf与原始ByteBuf共享参考计数。最初的ByteBuf的release()方法调用也会导致这些对象的内存被回收
相反,由ByteBuf.copy()和ByteBuf.readBytes(int)方法创建的对象不是派生的ByteBuf。这些对象和原始ByteBuf不共享引用计数,并且原始ByteBuf release()方法调用不会导致这些对象被回收
堆外内存大小控制参数
用于配置堆外内存大小的参数为-XX:MaxDirectMemorySize和ty.maxDirectMemory。这两个参数有什么区别?
注意:-XX:MaxDirectMemorySize不能限制Netty中noCleaner策略的DirectByteBuffer堆外内存的大小
堆外内存监视
如何使用堆外内存?

1个代码工具
注意:MXBean是Java提供的用于监视统计信息的一系列特殊bean,通过不同类型的MXBean,您可以获得诸如JVM进程的内存,线程和类加载信息之类的监视指标
List bufferPoolMXBeans = ManagementFactoryHelper.getBufferPoolMXBeans();
BufferPoolMXBean directBufferMXBean = bufferPoolMXBeans.get(0);
// hasCleaner的DirectBuffer的数量
long count = directBufferMXBean.getCount();
// hasCleaner的DirectBuffer的堆外内存占用大小,单位字节
long memoryUsed = directBufferMXBean.getMemoryUsed();
注意:MappedByteBuffer:这是通过基于FileChannelImpl.map的mmap内存映射(零复制的实现)获得的另一个堆外内存ByteBuffer,可以通过ManagementFactoryHelper.getBufferPoolMXBeans()。get(1)获取堆外内存的监视指标
2 Netty带有内存泄漏检测工具
Netty还附带了一个内存泄漏检测工具,该工具可用于检测GC已回收ByteBuf对象,但尚未释放由ByteBuf管理的内存,但不适用于这种情况。 GC尚未回收ByteBuf对象的位置。内存泄漏,例如任务队列积压
为了方便用户发现内存泄漏,Netty提供了4种检测级别:
使用方法是在命令行上设置参数:
-Dio.netty.leakDetectionLevel=[检测级别]
示例程序如下,将检测级别设置为偏执:
// -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时,都会在采样达到时轮询refQueue中的WeakReference对象。与轮询返回的非空WeakReference关联的ByteBuf是泄漏的堆外内存(代码入口在ResourceLeakDetector#track()方法中)
3种图形工具
在获取堆外内存的代码的基础上,通过自定义访问某些监视工具来定期检测和获取并绘制图形,例如较流行的Prometheus或Zabbix
您还可以通过jdk附带的Visualvm来获取它,您需要安装缓冲池插件。基本原理是访问MXBean中的监视指示器,并且只能使用hasCleaner的DirectByteBuffer

此外,对于JNI调用生成的堆外内存分配,您可以使用google-perftools进行监视
堆外内存泄漏诊断
堆外内存泄漏有许多特定原因。首先介绍对任务队列累积的监视,然后介绍一般的堆外内存泄漏诊断思路
1个任务队列累积

此处的任务队列是值NioEventLoop中的Queue taskQueue。提交到任务队列的方案是:
ctx.channel().eventLoop().execute(runnable);
channel.write(...)
channel.writeAndFlush(...)
ctx.channel().eventLoop().schedule(runnable, 60, TimeUnit.SECONDS);
当队列中的任务太多时,消息将无法写入通道然后再释放,这将导致内存泄漏
诊断思想是监视任务队列中的任务数量,ByteBuf的待办事项大小以及任务信息。具体的监控程序如下(代码地址):
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) 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);
}
在基于Netty的实际业务开发中,如何处理耗时的业务逻辑代码?
让我们先谈谈结论。建议自定义一组新的业务线程池,然后将耗时的业务提交给业务线程池
Netty的工作线程(NioEventLoop)除了将连接数据读取处理为NIO线程外,还在管道上执行channelHandler逻辑,并使用在taskQueue中提交的任务,包括通道写操作。
如果将耗时的任务提交给taskQueue,它也会影响NIO线程的处理以及taskQueue中的任务。因此,建议将处理隔离在单独的业务线程池中
2个一般诊断思路
Netty发生堆外内存泄漏的原因很多,例如缺少调用release()的代码;通过retain()增加ByteBuf的参考计数值,但是在调用release()时不会清除参考计数值;由于Exception导致release()失败; ByteBuf引用的对象预先是GC,并且相关的堆外内存无法回收,等等,因此无法在此处列出,因此请尝试提供一组常规诊断思路以供参考
首先,需要重现该问题。为了不影响服务的运行,请尝试在测试环境或本地环境中进行模拟。但是,这些环境通常不具有与相同的并发量,可以通过压力测试工具来模拟请求。
对于某些无法模拟的场景,可以通过Linux流量复制工具将实际的流量复制到测试环境,而不会影响业务。类似的工具包括Gor,tcpreplay,tcpcopy等。
再现后,下一步是查找问题,首先尝试直接通过上面介绍的监视方法和日志信息来查找问题;
如果找不到它,则需要找到堆外内存泄漏的触发条件,但有时应用程序相对较大,并且提供了许多通往外部世界的流量入口,无法检查一一。
在非环境中,您可以注释掉流量条目,每次注释掉一半,然后运行以检查问题是否仍然存在,如果存在,请继续注释掉剩余的一半,直到经过几次尝试,该策略可以快速找到问题的触发条件
找到触发条件后,请在程序中检查触发条件的处理逻辑。如果处理过程非常复杂且无法直接看到,则可以继续注释掉部分代码并检查二分法,直到特定的问题代码块为止
整个想法的核心在于问题的重现,监视和消除方法。它也可以用于解决其他问题,例如堆内存泄漏,CPU 100%,服务进程挂起等。
摘要
整篇文章着重介绍知识点和理论,但缺乏实际联系。以下是一些高质量的博客文章:
“ Netty堆外内存泄漏调查盛宴” Flash显示了如何调试堆外内存泄漏。
《 Netty防止内存泄漏的措施》,《 Netty权威指南》,华为李林峰内存泄漏知识共享
美团技术团队季兵的案例“神秘跟踪:春季启动内存泄漏调查”
“ Netty简介和实战:模仿微信即时通讯系统”,Flash Nuggets手册(收费),个人将学习此专栏以开始使用Netty

本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-359694-1.html
写的不好怪谁