使用netty框架做客户端、服务端通讯服务,项目运行一段时间后发现服务端出现如下异常
2021-06-17 23:57:40,879 WARN DefaultChannelPipeline:151 - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16777216 byte(s) of direct memory (used: 1895825408, max: 1908932608)
at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:640) ~[netty-common-4.1.25.Final.jar:4.1.25.Final]
at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:594) ~[netty-common-4.1.25.Final.jar:4.1.25.Final]
at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:764) ~[netty-buffer-4.1.25.Final.jar:4.1.25.Final]
at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:740) ~[netty-buffer-4.1.25.Final.jar:4.1.25.Final]
at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:244) ~[netty-buffer-4.1.25.Final.jar:4.1.25.Final]
at io.netty.buffer.PoolArena.allocate(PoolArena.java:214) ~[netty-buffer-4.1.25.Final.jar:4.1.25.Final]
at io.netty.buffer.PoolArena.allocate(PoolArena.java:146) ~[netty-buffer-4.1.25.Final.jar:4.1.25.Final]
at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:324) ~[netty-buffer-4.1.25.Final.jar:4.1.25.Final]
at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:185) ~[netty-buffer-4.1.25.Final.jar:4.1.25.Final]
at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:176) ~[netty-buffer-4.1.25.Final.jar:4.1.25.Final]
at io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:137) ~[netty-buffer-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114) ~[netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:147) [netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:647) [netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:582) [netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:499) [netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:461) [netty-transport-4.1.25.Final.jar:4.1.25.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:884) [netty-common-4.1.25.Final.jar:4.1.25.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-common-4.1.25.Final.jar:4.1.25.Final]
at java.lang.Thread.run(Thread.java:745) [?:1.8.0_111]
当时以为是内存不足导致的,没有具体的去分析为什么会抛OutOfDirectMemoryError(堆外内存溢出),总觉得是内存不足。调整内存后重新上线。几天后习惯性的打开服务器查看运行情况,结果发现服务器又出现这个问题。先是查看了服务器相关配置。
多次寻找资料无果
重新打开日志,看着这堆讨厌的 OOM 日志发着呆,突然一道光在眼前一闪而过,在 OOM 下方的几行日志变得耀眼起来:
这几行字是 …PlatformDependent.incrementMemoryCounter…。我去,原来,堆外内存是否够用,是 netty 这边自己统计的,那是不是可以找到统计代码,找到统计代码之后我们就可以看到 netty 里面的对外内存统计逻辑了?于是,接下来翻翻代码,找到这段逻辑,在 PlatformDepedent 这个类里面
这个地方,是一个对已使用堆外内存计数的操作,计数器为DIRECT_MEMORY_COUNTER,如果发现已使用内存大于堆外内存的上限(用户自行指定),就抛出一个自定义 OOM Error,异常里面的文本内容正是我们在日志里面看到的。
去官网查看,以下内容摘自官网:
ByteBuf is a reference-counted object which has to be released explicitly via the release() method.
官网说明:ByteBuf是一个引用计数对象,必须通过release()方法释放它。当调用write()或writeAndFlush()时,也就是我们有响应数据时,该方法会帮我们调用release()方法。而我服务端的代码,在收到消息后,并不是每条数据都会响应,大多都是只接收的,并且没有调用release()方法,最终导致gc问题。gc推荐使用ReferenceCountUtil.release(msg)(write()方法底层也是用的这个方法)。
Netty的回收机制和虚拟机的垃圾回收策略不一样,Netty是采用计数得方法回收,这样得话,netty框架中所用的内存没有得到释放,就会一直存在。由于是平台接入底层物联网网关数据(客户端),数据几乎一直都处于传输,从而发现内存24小时间,服务端一直没有释放一点内存,基本都是笔直得向上,直接到达分配最大内存,而客户端正常。当出现这样现象后,再在测试环境中,增加-Dio.netty.leakDetectionLevel=advanced 这个指令配置再环境中,打印netty哪些内存得不到释放,通过这些日志回得到问题得所在。
上图中标红色得地方,就是netty中,ByteBuf。这个是在netty4后,引进得,但是需要自己在释放掉这个bytebuf,原因是一直处于连接中,一直分配得是directMemory,但是采用得是计数回收,没有采用realse方法,则该bytebuf一直被分配,从没被回收,最后则会造成directMemoryError错误。
注意:release()方法实际上是把ByteBuf对象的引用计数refCnt进行-1操作(retain()将+1)。refCnt默认为1,当为0时,资源将会被释放,再次调用ByteBuf将抛出IllegalReferenceCountException异常,因此不可随意重复调用release()。
public class DispatcheRunnable implements Runnable {
private Channel channel = null;
private ByteBuf message = null;
public DispatcheRunnable(Channel channel, ByteBuf message){
this.channel = channel;
this.message = message;
}
@Override
public void run() {
try {
int size = message.readableBytes();
if( size <= 4){
throw new PacketException("validate packet, Length is error, length="+size);
}
...
String type = common.getType();
PacketHandler handler = PacketHandlers.get(type);
if( handler != null)
handler.handle(channel, document, common);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放ByteBuf 避免内存泄漏
message.release();
// ReferenceCountUtil.release(msg);
}
}
}
谨记哪里消费buffer,哪里释放,即调用ReferenceCountUtil.release(msg)/message.release();
通常的经验法则是谁最后访问(access)了引用计数对象,谁就负责销毁(destruction)它。具体来说是以下两点:
- 如果组件(component)A把一个引用计数对象传给另一个组件B,那么组件A通常不需要销毁对象,而是把决定权交给组件B。
-如果一个组件不再访问一个引用计数对象了,那么这个组件负责销毁它。
一个简单的例子:
public ByteBuf a(ByteBuf input) {
input.writeByte(42);
return input;
}
public ByteBuf b(ByteBuf input) {
try {
output = input.alloc().directBuffer(input.readableBytes() + 1);
output.writeBytes(input);
output.writeByte(42);
return output;
} finally {
input.release();
}
}
public void c(ByteBuf input) {
System.out.println(input);
input.release();
}
public void main() {
...
ByteBuf buf = ...;
// This will print buf to System.out and destroy it.
c(b(a(buf)));
assert buf.refCnt() == 0;
}