谈一谈Netty对堆外内存的管理,以及Netty是如何回收堆外内存的
个人博客地址:sillybaka的博客
今天在看别人的面经时,看到了这个题目谈一谈Netty对堆外内存的管理,以及Netty是如何回收堆外内存的
通过观看了几篇博客,以及个人对Netty部分源码的解读,作出了以下总结
首先,先谈谈为什么需要堆外内存?
因为在JVM内部执行IO操作
时,要先将数据从JVM堆内拷贝到堆外
,然后才能执行系统调用执行IO操作。
因为操作系统
不能够对JVM堆内存
进行直接读写
:
1、JVM堆内存对于操作系统来说是不可感知的
,同时JVM堆内存的布局也与操作系统不同,操作系统很难对其进行直接操作
2、 JVM在进行GC的时候会移动其中对象,就会导致内存地址变化
,而OS同样无法感知
该变化
但如果先拷贝到堆外,再执行IO操作,就平白无故地多了一次拷贝操作,
所以Netty基于零拷贝思想,在进行IO操作时都使用堆外内存,这样就能够避免从JVM到堆外的拷贝操作
再谈谈,为什么要对堆外内存进行管理?
因为堆外内存是指直接内存
,并不受JVM GC的自动回收机制管理
,所以就需要手动释放,防止出现内存泄漏的情况
Netty是如何管理堆外内存的?如何回收?
这里我们就要区分Java中的ByteBuffer 和 Netty中ByteBuf的区别 ,区分它们不同的回收策略
NIO中的堆外内存(DirectByteBuffer)
执行NIO包下的 ByteBuffer allocateDirect(int capacity)
,就会创建一个基于堆外内存的Buffer
Bits.reverseMemory(判断能否分配这么多内存)
基本的逻辑就是 先使用 Bits.reserveMemory()
判断能否分配这么多内存**(若不能,则尝试释放引用队列中的cleaner,然后再尝试GC,否则就休眠一段时间后循环尝试 多次失败就OOM)**
然后调用unsafe.allocateMemory(size)
从直接内存中分配
大小为size的内存
然后调用unsafe.setMemory
将这部分的内存初始化
为0,然后再将该内存地址赋值给ByteBuffer
再创建一个用于自动回收内存的Cleaner(虚引用类型 创建时会被放入引入队列中,当DirectByteBuffer被GC回收后,cleaner的引用就会不可达,然后监听该引用队列的后台线程 就会移除该cleaner 并且自动执行该cleaner的异步线程任务 在这里是回收内存的任务)
所以NIO中的DirectByteBuffer无需手动释放,当这个对象被GC回收时就会异步自动释放堆外内存,若是不小心进入了老年期,则会等待该方法中的System.gc()触发FullGC 来尝试回收
即 NIO中的DirectByteBuffer始终沿用着JAVA自动内存管理的思想,将堆外内存的回收同样交给GC来间接释放,而不是交给我们程序员
Netty中的堆外内存
实际上这里已经涉及到了Netty整个的内存管理机制,而不单只是堆外内存,还涉及到堆内内存
Netty中有很多种ByteBuf,其中xxxDirectByteBuf
底层就使用到了NIO的DirectByteBuffer
,也就是说采用的是堆外内存
其中又分为两种ByteBuf
- UnpooledDirectByteBuf 非池化的直接内存Buf
又分为 noCleaner 和 hasCleaner (本质上就是是否依赖GC回收cleaner,然后再回收堆外内存)
- PooledDirectByteBuf 池化的直接内存Buf
其中UnpooledDirectByteBuf
hasCleaner策略
的回收依赖的是DirectByteBuffer
底层的自动回收机制(通过GC顺带回收) 而 noCleaner
策略则是直接使用 UnSafe.freeMemory(内存地址)
来释放
而对于PoolDirectByteBuf
的管理,依赖的是Netty所维护的内存池
(这是Netty从内存中申请的一片连续内存,在Netty中进行页式管理),所以在使用完之后需要主动将其放入内存池中
而它们都需要主动回收,所以Netty就为ByteBuf提供了一种引用计数的机制,以及基于引用计数的回收机制,可以通过调用release让引用次数-1,也可以调用retain让引用次数+1,当ByteBuf的引用次数为0时就会被回收(直接释放或者返回内存池)
但,交由程序员自己来回收内存池的内存真的靠谱吗?
所以Netty为了避免这种问题,还提供了一种内存泄漏检测机制(只是检测,并不会帮你回收)
该机制主要针对的是池化的ByteBuf
,因为非池化的ByteBuf能够依赖GC来顺带回收
Netty每当创建一个池化的ByteBuf时,都会将其包装成一个LeakAwareByteBuf(可感知泄漏的ByteBuf)
这里会根据配置中设定的防止内存泄漏的等级,来选择不同的LeakAwareByteBuf实现类,(防漏力度不同)
Netty每当使用ByteBufAllocator创建ByteBuf
时,都会调用内存泄漏检测器的track方法
,从所有ByteBuf中取样1%进行追踪,会通过日志来告诉程序员命中的ByteBuf是否已经泄漏(底层采用的是Reference类,但我不知道是怎么检测的)
总结
- NIO中的堆外内存
DirectByteBuffer
,利用虚引用的cleaner和GC
来顺带回收堆外内存 - Netty中的堆外内存,从根本上分为两种
- 池化的堆外内存
PooledDirectByteBuf
由内存池来负责管理,实际上底层的回收跟非池化的相同
- 非池化的堆外内存
UnpooledDirectByteBuf
- hasCleaner:和 DirectByteBuf一样,利用
虚引用的cleaner和GC
来顺带回收堆外内存 - noCleaner:利用
UnSafe.freeMemory(内存地址)
来回收内存,减少了GC和cleaner的开销
- hasCleaner:和 DirectByteBuf一样,利用
- 池化的堆外内存
Netty使用引用计数机制来确定一个ByteBuf是否应该被回收,当引用计数为0时就回收
Netty提供了一种内存泄漏检测机制,用来提醒程序员哪些ByteBuf内存泄漏了