网络编程为避免频繁的在用户空间与内核空间拷贝数据,通常会直接从内核空间中申请内存,存放数据,在Java中,把内核空间的内存称之为直接内存,nio包中的ByteBuffer的allocateDirect方法,就是申请直接内存
DirectByteBuffer对象是ByteBuffer的子类,对于直接内存的分配,就是在这个类中实现的。
java中
- 直接内存的申请与释放是通过Unsafe类的allocateMemory方法和freeMemory方法
- 处置从allocateMemory或reallocateMemory获得的本地内存块。 传递给此方法的地址可以为null,在这种情况下,不采取任何措施。
分配给定大小的新本地内存块(以字节为单位)。 存储器的内容未初始化; 它们通常是垃圾。 结果本机指针永远不会为零,并且将针对所有值类型进行对齐。 通过调用freeMemory处理此内存,或使用reallocateMemory调整其大小。
- 直接内存的释放,必须手工调用freeMemory方法,因为JVM只能帮我们管理堆内存,直接内存不在其管理范围之内。
DirectByteBuffer帮我们简化了直接内存的使用,我们不需要直接操作Unsafe类来进行直接内存的申请与释放,那么其是如何实现的呢?
直接内存的申请:
在DirectByteBuffer实例通过构造方法创建的时候,会通过Unsafe类的allocateMemory方法 帮我们申请直接内存资源。
直接内存的释放:
DirectByteBuffer本身是一个Java对象,其是位于堆内存中的,JDK的GC机制可以自动帮我们回收,但是其申请的直接内存,不再GC范围之内,无法自动回收。好在JDK提供了一种机制,可以为堆内存对象注册一个钩子函数(其实就是实现Runnable接口的子类),当堆内存对象被GC回收的时候,会回调run方法,我们可以在这个方法中执行释放DirectByteBuffer引用的直接内存,即在run方法中调用Unsafe 的freeMemory 方法。注册是通过sun.misc.Cleaner类来实现的。
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer { .... //构造方法 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); //分配直接内存,base表示的是直接内存的开始地址 } 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 = Cleaner.create(this, new Deallocator(base, size, cap));//注册钩子函数,释放直接内存 att = null; } .... }
可以看到构造方法中的确是用了unsafe.allocateMemory方法帮我们分配了直接内存,另外,在构造方法的最后,通过 Cleaner.create方法注册了一个钩子函数,用于清除直接内存的引用。
Cleaner.create方法声明如下所示:
public static Cleaner create(Object heapObj, Runnable task)
其中第一个参数是一个堆内存对象,第二个参数是一个Runnable任务,表示这个堆内存对象被回收的时候,需要执行的回调方法。我们可以看到在DirectByteBuffer的最后一行中,传入的这两个参数分别是this,和一个Deallocator(实现了Runnable接口),其中this表示就是当前DirectByteBuffer实例,也就是当前DirectByteBuffer被回收的时候,回调Deallocator的run方法
Deallocator就是用于清除DirectByteBuffer引用的直接内存,代码如下所示:
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); } }
System.gc
在DirectByteBuffer实例创建时,分配内存之前调用了Bits.reserveMemory,如果分配失败调用了Bits.unreserveMemory,同时在Deallocator释放完直接内存的时候,也调用了Bits.unreserveMemory方法。
这两个方法,主要是记录jdk已经使用的直接内存的数量,当分配直接内存时,需要进行增加,当释放时,需要减少,源码如下:
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);//休眠100秒,等待垃圾回收完成 } catch (InterruptedException x) { // Restore interrupt status Thread.currentThread().interrupt(); } synchronized (Bits.class) {//休眠100毫秒后,增加直接内存引用的计数 if (totalCapacity + cap > maxMemory) throw new OutOfMemoryError("Direct buffer memory"); reservedMemory += size; totalCapacity += cap; count++; } }
//释放内存时,减少引用直接内存的计数 static synchronized void unreserveMemory(long size, int cap) { if (reservedMemory > 0) { reservedMemory -= size; totalCapacity -= cap; count--; assert (reservedMemory > -1); } }
通过上面代码的分析,我们事实上可以认为Bits类是直接内存的分配担保,当有足够的直接内存可以用时,增加直接内存应用计数,否则,调用System.gc,进行垃圾回收,需要注意的是,System.gc只会回收堆内存中的对象,但是我们前面已经讲过,DirectByteBuffer对象被回收时,那么其引用的直接内存也会被回收,试想现在刚好有其他的DirectByteBuffer可以被回收,那么其被回收的直接内存就可以用于本次DirectByteBuffer直接的内存的分配。
一些文章讲解在使用Nio的时候,不要禁用System.gc,也就是启动JVM的时候,不要传入-XX:+DisableExplicitGC参数,因为这样可能会造成直接内存溢出。
因为直接内存的释放与获取比堆内存更加耗时,每次创建DirectByteBuffer实例分配直接内存的时候,都调用System.gc,可以让已经使用完的DirectByteBuffer得到及时的回收。
虽然System.gc只是建议JVM垃圾回收,可能JVM并不会立即回收,但频繁建议,JVM不会视而不见。这不是绝对的,因为System.gc导致FullGC,会暂停用户线程,对于一些要求延时比较短的应用,不希望JVM频繁FullGC。
建议禁用System.gc,调大最大可以使用的直接内存。
-XX:+DisableExplicitGC -XX:MaxDirectMemorySize=256M