堆外内存之DirectByteBuffer

时间:2021-04-17 15:08:03
http://www.jianshu.com/p/007052ee3773

堆外内存

       堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。
       DirectByteBuffer是通过虚引用(Phantom Reference)来实现堆外内存的释放的。
       PhantomReference 是所有“弱引用”中最弱的引用类型。虚引用主要被用来跟踪对象被垃圾回收的状态,通过查看引用队列中是否包含对象所对应的虚引用来判断它是否即将被垃圾回收,从而采取行动。它并不被期待用来取得目标对象的引用,而目标对象被回收前,它的引用会被放入一个ReferenceQueue对象中,从而达到跟踪对象垃圾回收的作用。

DirectByteBuffer直接缓冲

       DirectByteBuffer是Java用于实现堆外内存的一个重要类,可以通过该类实现堆外内存的创建、使用和销毁。DirectByteBuffer该类本身还是位于Java内存模型的堆中。堆内内存是JVM可以直接管控、操纵。
       DirectByteBuffer的构造函数中有unsafe.allocateMemory(size),是个一个native方法,这个方法就是分配堆外内存,通过C的malloc来进行分配的。分配的内存是系统本地的内存,并不在Java的内存中,也不属于JVM管控范围,所以在DirectByteBuffer一定会存在某种方式来操纵堆外内存。
       在DirectByteBuffer的父类Buffer中有个address属性:
    // Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
       address只会被直接缓存给使用到。之所以将address属性升级放在Buffer中,是为了在JNI调用GetDirectBufferAddress时提升它调用的速率。
       address表示分配的堆外内存的地址,通过unsafe.allocateMemory(size)分配完堆外内存后就会返回分配的堆外内存基地址,并将这个地址赋值给了address属性。这样后面可以通过JNI对这个堆外内存操作时都是通过这个address来实现的了。

          在linux中内核态的权限是最高的,那么在内核态的场景下,操作系统是可以访问任何一个内存区域的,所以操作系统是可以访问到Java堆的这个内存区域的。
       那为什么操作系统不直接访问Java堆内的内存区域了?这是因为JNI方法访问的内存区域是一个已经确定了的内存区域地址,那么如果该内存地址指向的是Java堆内内存的话,假设在操作系统正在访问这个内存地址的时候,Java进行了GC操作,而GC操作会涉及到数据的移动操作,数据的移动会使JNI调用的数据错乱。所以JNI调用的内存是不能进行GC操作的。
       既然JNI调用的内存是不能进行GC操作的,那该如何解决了?
          A:堆内内存与堆外内存之间数据拷贝的方式(并且在将堆内内存拷贝到堆外内存的过程JVM会保证不会进行GC操作),比如要完成一个从文件中读数据到堆内内存的操作,即FileChannelImpl.read(HeapByteBuffer)。这里实际上File I/O会将数据读到堆外内存中,然后堆外内存再讲数据拷贝到堆内内存,这样就读到了文件中的内容。而写操作则反之,会将堆内内存的数据先写到堆外内存中,然后操作系统会将堆外内存的数据写入到文件中。
          B:直接使用堆外内存,如DirectByteBuffer,这种方式是直接在堆外分配一个内存(即,native memory)来存储数据,程序通过JNI直接将数据读/写到堆外内存中。因为数据直接写入到了堆外内存中,所以这种方式就不会再在JVM管控的堆内再分配内存来存储数据了,也就不存在堆内内存和堆外内存数据拷贝的操作了。这样在进行I/O操作时,只需要将这个堆外内存地址传给JNI的I/O的函数就好了。

堆外内存分配

   DirectByteBuffer(int cap) {                   // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));


// 保留总分配内存(按页分配)的大小和实际内存的大小
Bits.reserveMemory(size, cap);


long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收
//以实现当DirectByteBuffer被垃圾回收时,堆外内存也会被释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
    // These methods should be called whenever direct memory is allocated or    // freed.  They allow the user to control the amount of direct memory    // which a process may access.  All sizes are specified in bytes.    static void reserveMemory(long size, int cap) {        synchronized (Bits.class) {            if (!memoryLimitSet && VM.isBooted()) {                maxMemory = VM.maxDirectMemory();                memoryLimitSet = true;            }            // -XX:MaxDirectMemorySize limits the total capacity rather than the            // actual memory usage, which will differ when buffers are page            // aligned.            if (cap <= maxMemory - totalCapacity) {                reservedMemory += size;                totalCapacity += cap;                count++;                return;            }        }        System.gc();        try {            Thread.sleep(100);        } catch (InterruptedException x) {            // Restore interrupt status            Thread.currentThread().interrupt();        }        synchronized (Bits.class) {            if (totalCapacity + cap > maxMemory)                throw new OutOfMemoryError("Direct buffer memory");            reservedMemory += size;            totalCapacity += cap;            count++;        }    }
       Bits.reserveMemory用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小。该方法不管是直接内存的分配还是释放都会被调用,会检查直接内存的大小。reserveMemory方法会调用System.gc()。System.gc()会触发一个FULL GC,当然前提是没有显示的设置-XX:+DisableExplicitGC来禁用显式GC。
       注意,这里之所以用使用FULL GC的很重要的一个原因是:System.gc()会对新生代的老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及它们关联的堆外内存。

堆外内存回收

       Cleaner是PhantomReference的子类,并通过自身的next和prev字段维护的一个双向链表。PhantomReference的作用在于跟踪垃圾回收过程,并不会对对象的垃圾回收过程造成任何的影响。
       所以cleaner = Cleaner.create(this, new Deallocator(base, size, cap))用于对当前构造的DirectByteBuffer对象的垃圾回收过程进行跟踪。
       当DirectByteBuffer对象从pending状态变为 enqueue状态时,会触发Cleaner的clean(),而Cleaner的clean()的方法会实现通过unsafe对堆外内存的释放。也可以直接调用directByteBuffer.cleaner().clean()来主动释放:
   # Cleaner.java
public void clean() {
if(remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
if(System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}

System.exit(1);
return null;
}
});
}


}
}
       这里的thunk即DirectByteBuffer构造函数中指定的Deallocator:
    private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();

private long address;
private long size;
private int capacity;

private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}

public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}