Netty 堆外内存泄漏

发布时间 2023-07-28 16:01:20作者: 爱,诗意永存

异常堆栈信息:

 1 LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
 2 Recent access records: 
 3 Created at:
 4 \tio.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:385)
 5 \tio.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
 6 \tio.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)
 7 \tio.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:115)
 8 \tio.netty.buffer.ByteBufUtil.readBytes(ByteBufUtil.java:465)
 9 \tio.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder.decode(WebSocket08FrameDecoder.java:314)
10 \tio.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:501)
11 \tio.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:440)
12 \tio.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
13 \tio.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
14 \tio.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
15 \tio.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
16 \tio.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
17 \tio.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
18 \tio.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
19 \tio.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
20 \tio.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
21 \tio.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
22 \tio.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
23 \tio.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
24 \tio.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
25 \tio.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
26 \tio.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
27 \tio.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
28 \tjava.lang.Thread.run(Thread.java:748)

 

堆外内存是个啥
堆外内存也叫直接内存,因为这部分内存就是机器的物理内存
举个通俗一点的例子?:
假如操作系统就是你所在小区,小区的居民就是不同的jvm或者其他的进程(程序)。有天你想装修,装修材料放在家里,也就是自己的空间,随便玩,放哪里你自己管理就好了,对应到虚拟机就是你常规理解的内存,包括heap,栈等等 但是把装修材料放到家里不方便,而且还要运进来,所以你想我能不能放在公共区域,比如在楼下找块空地放装修材料,这部分就是你要申请使用的堆外内存,也就是机器的物理内存。

堆外内存的优缺点
优点?:
  1 减少了垃圾回收的工作,理论上能减小GC暂停时间,因为堆外内存的释放不受虚拟机管理(虚拟机只是释放句柄,而真正的内存是操作系统释放)
  2 省去了不必要的内存复制,实现zero copy,数据不需要再native memory和jvm memory中来回copy。
缺点?️:
  1 堆外内存难以控制,在发生内存泄漏的时候不易排查。谨慎使用。
  2 堆外内存相对来说,不适合存储很复杂的对象。一般则是放大块的内存。格式需要自己定义。

堆外内存的控制参数
可以通过设置-XX:MaxDirectMemorySize=500M 控制堆外内存的大小。超过此内存则会报错。

DirectByteBuffer
堆外内存可以通过java.nio的ByteBuffer来创建,调用allocateDirect方法申请即可,
ByteBuffer.allocateDirect(1024);
JVM在堆内只保存堆外内存的引用,用DirectByteBuffer对象来表示,类似指针的概念。
每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象。
这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存。一般在full gc的时候回收
当DirectByteBuffer对象在某次YGC中被回收,只有Cleaner对象知道堆外内存的地址。
当下一次FGC执行时,Cleaner对象会将自身Cleaner链表上删除,并触发clean方法清理堆外内存。
此时,堆外内存将被回收,Cleaner对象也将在下次YGC时被回收。
如果JVM一直没有执行FGC的话,无法触发Cleaner对象执行clean方法,从而堆外内存也一直得不到释放

Unsafe
sun.misc.Unsafe提供了一组方法来进行堆外内存的分配,重新分配,以及释放。
public native long allocateMemory(long size); —— 分配一块内存空间。
public native long reallocateMemory(long address, long size); —— 重新分配一块内存,把数据从address指向的缓存中拷贝到新的内存块。
public native void freeMemory(long address); —— 释放内存。