Netty 堆外内存泄漏
异常堆栈信息:
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); —— 释放内存。