堆外内存的回收机制分析
占小狼堆外内存
转载请注明原创出处,谢谢!
JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,比如Netty,广泛使用了堆外内存,但是这部分的内存并不归JVM管理,GC算法并不会对它们进行回收,所以在使用堆外内存时,要格外小心,防止内存一直得不到释放,造成线上故障。
堆外内存的申请和释放
JDK的
ByteBuffer
类提供了一个接口allocateDirect(int capacity)
进行堆外内存的申请,底层通过unsafe.allocateMemory(size)
实现,接下去看看在JVM层面是如何实现的。文章图片
可以发现,最底层是通过
malloc
方法申请的,但是这块内存需要进行手动释放,JVM并不会进行回收,幸好Unsafe
提供了另一个接口freeMemory
可以对申请的堆外内存进行释放。文章图片
堆外内存的回收机制
如果每次申请堆外内存,都需要在代码中显示的释放,对于Java这门语言的设计来说,显然不够合理,既然JVM不会管理这些堆外内存,它们是如何回收的呢?
DirectByteBuffer JDK中使用
DirectByteBuffer
对象来表示堆外内存,每个DirectByteBuffer
对象在初始化时,都会创建一个对用的Cleaner
对象,这个Cleaner
对象会在合适的时候执行unsafe.freeMemory(address)
,从而回收这块堆外内存。当初始化一块堆外内存时,对象的引用关系如下:
文章图片
其中
first
是Cleaner
类的静态变量,Cleaner
对象在初始化时会被添加到Clener
链表中,和first
形成引用关系,ReferenceQueue
是用来保存需要回收的Cleaner
对象。【堆外内存的回收机制分析】如果该
DirectByteBuffer
对象在一次GC中被回收了文章图片
此时,只有
Cleaner
对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次FGC时,把该Cleaner
对象放入到ReferenceQueue
中,并触发clean
方法。Cleaner
对象的clean
方法主要有两个作用:1、把自身从
Clener
链表删除,从而在下次GC时能够被回收2、释放堆外内存
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
如果JVM一直没有执行FGC的话,无效的
Cleaner
对象就无法放入到ReferenceQueue中,从而堆外内存也一直得不到释放,内存岂不是会爆?其实在初始化
DirectByteBuffer
对象时,如果当前堆外内存的条件很苛刻时,会主动调用System.gc()
强制执行FGC。文章图片
不过很多线上环境的JVM参数有
-XX:+DisableExplicitGC
,导致了System.gc()
等于一个空函数,根本不会触发FGC,这一点在使用Netty框架时需要注意是否会出问题。推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量