1、堆外内存定义

内存对象分配在JVM中堆以外的内存,也可以称为直接内存,这些内存直接受操作系统管理(而不是JVM),这样做的好处是能够在一定程度上减少垃圾回收对应用程序造成的影响。一般我们使用Unsafe和NIO包下ByteBuffer来创建堆外内存。

2、为什么使用堆外内存

1、减少了垃圾回收

使用堆外内存的话,堆外内存是直接受操作系统管理( 而不是虚拟机 )。这样做的结果就是能保持一个较小的堆内内存,以减少垃圾收集对应用的影响。

2、提升复制速度(io效率)

堆内内存由JVM管理,属于“用户态”;而堆外内存由OS管理,属于“内核态”。如果从堆内向磁盘写数据时,数据会被先复制到堆外内存,即内核缓冲区,然后再由OS写入磁盘,使用堆外内存避免了这个操作。




设置java堆外内存 jvm设置堆外内存_初始化


3、堆外内存申请

JDK的ByteBuffer类提供了一个接口allocateDirect(int capacity)进行堆外内存的申请,底层通过unsafe.allocateMemory(size)实现。Netty、Mina等框架提供的接口也是基于ByteBuffer封装的。


设置java堆外内存 jvm设置堆外内存_初始化_02


源码分析如下:


设置java堆外内存 jvm设置堆外内存_JVM_03


4、堆外内存释放

虽然堆外内存直接受操作系统管理,但是不代表JVM不进行内存回收,要了解堆外内存释放,必须了解以下内容:

1)当初始化一块堆外内存时,对象的引用关系如下:


设置java堆外内存 jvm设置堆外内存_系统管理_04


其中first是Cleaner类的静态变量,Cleaner对象在初始化时会被添加到Clener链表中,和first形成引用关系,ReferenceQueue是用来保存需要回收的Cleaner对象。

源码分析如下(和对象的引用关系对照):

cleaner对象在以下代码中创建


设置java堆外内存 jvm设置堆外内存_JVM_05


first对象和ReferenceQueue对象是在以下代码中,


设置java堆外内存 jvm设置堆外内存_JVM_06


2)如果该DirectByteBuffer对象在一次GC中被回收了,会发生什么?


设置java堆外内存 jvm设置堆外内存_初始化_07


此时,只有Cleaner对象唯一保存了堆外内存的数据(开始地址、大小和容量),在下一次FGC时,把该Cleaner对象放入到ReferenceQueue中,并触发clean方法。

Cleaner对象的clean方法主要有两个作用: 1、把自身从Clener链表删除,从而在下次GC时能够被回收 2、释放堆外内存

如果JVM一直没有执行FullGC的话,无效的Cleaner对象就无法放入到ReferenceQueue中,从而堆外内存也一直得不到释放,内存岂不是会爆?

其实在初始化DirectByteBuffer对象时,如果当前堆外内存的条件很苛刻时(不够时),会主动调用System.gc()强制执行FullGC,代码如下:


设置java堆外内存 jvm设置堆外内存_初始化_08


设置java堆外内存 jvm设置堆外内存_系统管理_09


5、总结

如果我们大面积使用堆外内存并且没有限制,那迟早会导致内存溢出,毕竟程序是跑在一台资源受限的机器上,因为这块内存的回收不是你直接能控制的,当然你可以通过别的一些途径,比如反射,直接使用Unsafe接口等,但是这些务必给你带来了一些烦恼,Java与生俱来的优势被你完全抛弃了—开发不需要关注内存的回收,由gc算法自动去实现。另外上面的gc机制与堆外内存的关系也说了,如果一直触发不了cms gc或者full gc,那么后果可能很严重。