堆外内存 堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建的对象都处于堆内内存中,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理它们的内存。那么堆外内存就是存在于JVM管控之外的一块内存区域,因此它是不受JVM的管控。 可以使用ByteBuffer等类来操纵堆外内存了,使用ByteBuffer分配本地内存则非常简单,直接:
1 ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
可以通过指定JVM参数来确定堆外内存大小限制:-XX:MaxDirectMemorySize=512m 对于这种direct buffer内存不够的时候会抛出错误: java.lang.OutOfMemoryError: Direct buffer memory 堆外内存泄露的问题定位通常比较麻烦,可以借助google-perftools这个工具,它可以输出不同方法申请堆外内存的数量。
使用ByteBuffer堆外内存时,必须要由我们自己释放;必须先释放堆内存中的对象(引用),才能释放堆外内存,但是我们又不能强制JVM释放堆内存。例如:ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。 再者,假设堆内 ByteBuffer对象的引用升级到了老年代,导致这个引用会长期存在无法回收,这时堆外的内存将长期无法得到回收。
堆外内存分配 堆外内存申请:ByteBuffer.allocateDirect(cap); 进行内存申请的时候,会调用:DirectByteBuffer(int cap)构造函数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // 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) { // 获取最大可以申请的对外内存大小,默认值是64MB // 可以通过参数-XX:MaxDirectMemorySize=<size>设置这个大小 if (!memoryLimitSet && VM.isBooted()) { maxMemory = VM.maxDirectMemory(); memoryLimitSet = true; } // -XX:MaxDirectMemorySize限制的是用户申请的大小,而不考虑对齐情况 // 所以使用两个变量来统计: // reservedMemory:真实的目前保留的空间 // totalCapacity:目前用户申请的空间 if (cap <= maxMemory - totalCapacity) { reservedMemory += size; totalCapacity += cap; count++; return; // 如果空间足够,更新统计变量后直接返回 } } // 如果已经没有足够空间,则尝试GC System.gc(); try { Thread.sleep(100); } catch (InterruptedException x) { // Restore interrupt status Thread.currentThread().interrupt(); } synchronized (Bits.class) { // GC后再次判断,如果还是没有足够空间,则抛出OOME if (totalCapacity + cap > maxMemory) throw new OutOfMemoryError("Direct buffer memory"); reservedMemory += size; totalCapacity += cap; count++; } }
Bits.reserveMemory用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小。该方法不管是直接内存的分配还是释放都会被调用,会检查直接内存的大小。如果空间不足,会调用System.gc()尝试释放内存,然后再进行判断,如果还是没有足够的空间,抛出OOME。System.gc()会触发一个FULL GC,当然前提是没有显示的设置-XX:+DisableExplicitGC来禁用显式GC。 最后,DirectByteBuffer使用Cleaner机制进行空间回收。
注意,这里之所以用使用FULL GC的很重要的一个原因是:System.gc()会对新生代的老生代都会进行内存回收,这样会比较彻底地回收DirectByteBuffer对象以及它们关联的堆外内存。 sun.misc.Unsafe.allocateMemory这个函数是通过JNI调用C的malloc来申请内存; 申请内存时,可以通过-XX:+PageAlignDirectMemory指定申请的内存是否需要按页对齐,默认不对其;
堆外内存回收 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()来主动释放:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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); } }
说明:sun.misc.Unsafe.freeMemory方法使用C标准库的free函数释放内存空间。
默认情况下,可以申请的DirectByteBuffer大小为Runtime.getRuntime().maxMemory(),而这个值等于可用的最大Java堆大小,也就是我们-Xmx参数指定的值。
参考:http://www.importnew.com/29817.html
https://blog.csdn.net/liuxiao723846/article/details/89814005
http://www.jianshu.com/p/007052ee3773
主动分配和回收堆外内存:
ByteBuffer byteBuffer = ByteBuffer.allocateDirect();
((DirectBuffer)byteBuffer).cleaner().clean();