在 java 开发中, 如果遇到堆内存的使用瓶颈, 或许可以借助于堆外内存解决问题; 本文总结了直接内存的使用方式和优缺点;
直接内存的介绍 定义 我们经常会提到堆外内存, 广义上的堆外内存就是指 jvm 堆内存之外的一切内存, 这块内存可以再分为 jvm 内及 jvm 外两大部分; jvm 内的堆外内存包括: Metaspace 内存、garbage collector 自身运行占用内存、线程与虚拟机栈的内存、JIT 编译器占用内存及其 codecache 等, 而 jvm 外的堆外内存则被称为 直接内存 (direct memory), 这块内存不受 jvm 管理; 狭义上的堆外内存其实就是指直接内存, 而本文为了明确概念, 将严格区分堆外内存与直接内存;
jvm 内存布局
优点 使用直接内存有如下收益:
当进行网络 IO 操作、文件读写时, java 堆内存都需要转换为直接内存, 然后再与底层设备进行交互; 如果能使用直接内存, 可以减少拷贝次数, 降低开销;
直接内存不受 JVM 管理, 可降低 JVM GC 对应用程序的影响;
直接内存可以实现进程之间、JVM 多实例之间的数据共享, 减少虚拟机间的数据拷贝;
直接内存的使用 DirectByteBuffer 使用 ByteBuffer 类提供的统一接口申请并使用直接内存:
1 2 final ByteBuffer byteBuf = ByteBuffer.allocateDirect(10 * 1024 * 1024L );
上述代码生成的 DirectByteBuffer, 可以为我们在该 java 对象被回收的时候间接地回收这块直接内存; 同时该方法在判断不能申请到内存时, 会触发一次 System.gc()
回收一次内存, 如果还是不能申请到内存, 则抛出 OutOfMemoryError;
直接内存回收的原理 在初始化 DirectByteBuffer 的时候, 会初始化一个 Cleaner 对象, 它初始化了一个 Deallocator 的 Runnable:
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 DirectByteBuffer(int cap) { 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 )) { address = base + ps - (base & (ps - 1 )); } else { address = base; } cleaner = Cleaner.create(this , new Deallocator(base, size, cap)); att = null ; }
调用 Cleaner 的 clean() 方法便会执行 Deallocator 的清理逻辑:
1 2 3 4 5 6 7 8 9 10 11 public void clean () { if (!remove(this )) return ; try { thunk.run(); } catch (final Throwable x) { ...... } }
Deallocator 的清理逻辑是释放 DirectByteBuffer 申请的那块内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private static class Deallocator implements Runnable { 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 ) { return ; } UNSAFE.freeMemory(address); address = 0 ; Bits.unreserveMemory(size, capacity); } }
DirectByteBuffer 作为一个普通的 java 对象, 当其与自己内部的 cleaner 成员不可达时, garbage collector 会在适当的时机将它们的引用挂到 pending-reference 链上, 然后 Reference 中的 ReferenceHandler 线程会处理 pending-reference 链上的引用:
1 2 3 4 5 6 7 8 private static class ReferenceHandler extends Thread { ...... public void run () { while (true ) { processPendingReferences(); } } }
针对 Cleaner 类型的引用, 直接调用其 clean() 方法完成清理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private static void processPendingReferences () { waitForReferencePendingList(); Reference<Object> pendingList; synchronized (processPendingLock) { pendingList = getAndClearReferencePendingList(); ...... } while (pendingList != null ) { Reference<Object> ref = pendingList; pendingList = ref.discovered; ref.discovered = null ; if (ref instanceof Cleaner) { ((Cleaner)ref).clean(); ...... } else { ReferenceQueue<? super Object> q = ref.queue; if (q != ReferenceQueue.NULL) q.enqueue(ref); } ...... } }
最大直接内存的限制 jvm 除了能为我们自动回收 DirectByteBuffer 的关联的堆外内存, 还可以限制程序申请堆外内存的最大限制, 即配置如下启动参数:
1 -XX:MaxDirectMemorySize=<size>
限制原理如下: DirectByteBuffer 的构造器会调用如下方法:
1 Bits.reserveMemory(size, 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 static void reserveMemory (long size, long cap) { if (!MEMORY_LIMIT_SET && VM.initLevel() >= 1 ) { MAX_MEMORY = VM.maxDirectMemory(); MEMORY_LIMIT_SET = true ; } if (tryReserveMemory(size, cap)) { return ; } ...... } private static boolean tryReserveMemory (long size, long cap) { long totalCap; while (cap <= MAX_MEMORY - (totalCap = TOTAL_CAPACITY.get())) { if (TOTAL_CAPACITY.compareAndSet(totalCap, totalCap + cap)) { RESERVED_MEMORY.addAndGet(size); COUNT.incrementAndGet(); return true ; } } return false ; }
当希望申请的新容量 + 已经申请的总容量 超过了 MaxDirectMemorySize 设置的最大值, 申请失败;
直接使用 Unsafe jdk.internal.misc.Unsafe
对象不能直接获得, 但可以通过反射获取:
1 2 3 4 5 6 7 8 9 10 11 private static final Unsafe unsafe = null ;static { try { final Field getUnsafe = Unsafe.class.getDeclaredField("theUnsafe" ); getUnsafe.setAccessible(true ); unsafe = (Unsafe) getUnsafe.get(null ); } catch (NoSuchFieldException | IllegalAccessException e) { throw new ...... } }
通过 Unsafe 申请的内存, 需要手动回收:
1 2 3 4 final long addr = unsafe.allocateMemory(10 * 1024 * 1024L );...... unsafe.freeMemory(addr);
这种原始的操纵方式很不安全, 一般只有在必须要能完全自主控制直接内存的申请/释放的场景下才会遇到; 然而如果真有这样的场景, 其实更推荐使用原生的 JNI 方式;
netty 对直接内存的使用
使用 JNI malloc 1 2 3 4 final long addr = Native.malloc(10 * 1024 * 1024L );...... Native.free(addr);
jdk 17+: 使用 Memory Segments 直接内存使用方式的比较
DirectByteBuffer: 使用限制比较大, jvm 对其管控介入程度较深, 内存的回收时机和容量限制均由虚拟机负责, 适用于非核心、对性能要求不高的场景;
Unsafe / Native: 完全由程序自己管控, 需要自己管控好申请与释放内存的时机, 适用于对直接内存具有重要核心依赖, 对性能要求高, 对内存管控有自身特殊要求的场景; 使用 Native.malloc 比 Unsafe.allocateMemory 效率要高: JNI faster than Unsafe ;
其他使用经验
建议在 linux 上预装 jemalloc 取代默认的 ptmalloc 以提高内存管理性能;
参考链接