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

image-20221114112243963

image-20221114114219607

Bits.reverseMemory(判断能否分配这么多内存)

image-20221114121323486

基本的逻辑就是 先使用 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整个的内存管理机制,而不单只是堆外内存,还涉及到堆内内存

image (3).png

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)

image-20221114152642488

这里会根据配置中设定的防止内存泄漏的等级,来选择不同的LeakAwareByteBuf实现类,(防漏力度不同)

image-20221114152655244

Netty每当使用ByteBufAllocator创建ByteBuf时,都会调用内存泄漏检测器的track方法从所有ByteBuf中取样1%进行追踪,会通过日志来告诉程序员命中的ByteBuf是否已经泄漏(底层采用的是Reference类,但我不知道是怎么检测的)

总结

  • NIO中的堆外内存 DirectByteBuffer ,利用虚引用的cleaner和GC来顺带回收堆外内存
  • Netty中的堆外内存,从根本上分为两种
    • 池化的堆外内存 PooledDirectByteBuf 由内存池来负责管理,实际上底层的回收跟非池化的相同
    • 非池化的堆外内存 UnpooledDirectByteBuf
      • hasCleaner:和 DirectByteBuf一样,利用虚引用的cleaner和GC来顺带回收堆外内存
      • noCleaner:利用 UnSafe.freeMemory(内存地址)来回收内存,减少了GC和cleaner的开销

Netty使用引用计数机制来确定一个ByteBuf是否应该被回收,当引用计数为0时就回收

Netty提供了一种内存泄漏检测机制,用来提醒程序员哪些ByteBuf内存泄漏了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值