前
最近才接触到堆外内存的概念,为什么要使用堆外内存,最大的原因应该就是少一次内存拷贝,因为如果想在内核空间进行数据处理,就必须将数据从Java堆中拷贝到内核控件中,但是Java提供了一种更加直接的方法,就是直接在堆外(内核)申请内存,存储数据,减少数据拷贝,这也就是所谓的零拷贝(zero-copy)
API
NIO包下的ByteBuffer
下的allocateDirect
是帮助我们申请直接内存的,并创建一个DirectByteBuffer
对象,其中allocateDirect
方法的实现是Unsafe包下的native方法,所以当做黑盒吧.
直接内存和堆内存的区别还有一点,直接内存已经不在JVM的运行时数据区域了,所以不受GC的回收,但又不完全是这样,详细的看下面底层,这就很有意思了,具体的方法还是Unsafe包下的native方法
底层
之前我一直认为对外内存是不被GC回收的,但是看了Netty的内存池的实现后,发现其实和GC还是有关系的.
上面我们说到申请堆外内存是通过Unsafe类下的方法,堆外内存类是DirectByteBuffer,但这其实是一个堆上的对象,只存着基地址和大小等几个属性,和一个Cleaner,它所代表的是内存中的一大块区域,当这个堆上的对象被回收后,也就堆外的内存也会被回收.
手动回收
堆外内存是可以手动回收的,也就是刚才说的DIrectByteBuffer中的Cleaner对象,我们从DirectByteBuffer中拿到这个对象后,执行clean()方法,就可以释放了,事实上,JVM回收堆外内存也是这样,不过要复杂一点
JVM回收
我们知道Java中的引用类型有四种,但是经常使用的只有强引用,这里用到就是最后一种,虚引用.
看一下Cleaner的声明:
public class Cleaner extends PhantomReference<Object>
这个Cleaner继承了虚引用类,这就意味着它拥有类虚引用的特性,当GC发现Cleaner对象不在被持有(在这里就是DirectByteBuffer失效),会将这个对象放进 Reference类pending list静态变量里(这是虚引用特性),然后这里介绍一个最高优先级的守护线程ReferenceHandler,这个守护线程会一直扫描pending list队列,如果发现队列元素是Cleaner类型的,就执行其clean()方法,回收堆外内存.
Netty
Netty的高性能原因之一就是使用了直接内存,那么Netty作为一个框架,就一定要提供内存回收的方法,具体使用的内存管理机制是基于Jemalloc算法的,大致介绍一下:
首先会预申请一大块内存 Arena ,Arena 由许多 Chunk 组成,而每个 Chunk 默认由2048个page组成。
Chunk 通过 AVL 树的形式组织 Page ,每个叶子节点表示一个 Page ,而中间节点表示内存区域,节点自己记录它在整个 Arena 中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。大于 8k 的内存分配在 PoolChunkList 中,而 PoolSubpage 用于分配小于 8k 的内存,它会把一个 page 分割成多段,进行内存分配。
彩蛋
看一位大佬博客中的一句话,很好玩 :
C/C++ 和 java 中有个围城,城里的想出来,城外的想进去!