一.結論
DirectByteBuffer 與 ByteBuffer 最大區別就在於緩沖區內存管理的方式。ByteBuffer使用的是堆內存,DirectByteBuffer 使用的是堆外內存,堆外內存的優點就是在執行I/O操作時數據拷貝的次數相對較少,因此也獲得了較高的性能。凡事總有但是,由於將緩沖區分配在堆外內存也引入一系列與內存分配和回收的問題,所幸JDK提供了一系列方案來解決問題,這些也是本文所要闡述的重點。
二.ByteBuffer 的缺點
I/O基本上可以視為一系列倒騰數據的操作。舉例來說,商品經中間商倒騰的次數越少,其價格越便宜;對於I/O來說,拷貝字節數組的次數越少,其I/O性能也就越高。而ByteBuffer性能低下的原因就是在使用ByteBuffer進行I/O操作時會執行以下操作:
1.將堆內存中緩沖區數據拷貝到臨時緩沖區
2.對臨時緩沖區的數據執行低層次I/O操作
3.臨時緩沖區對象離開作用域,並最終被回收成為無用數據
與之相對,DirectByteBuffer 由於將內存分配在了堆外內存因此可以直接執行較低層次的I/O操作數據,減少了拷貝次數因此也獲得了較高的性能。
問題來了,為什么不直接在堆內存中的緩沖區執行低層次的I/O操作呢?
推測最主要的原因就是,JVM的垃圾回收操作會移動對象在堆內存中的位置,以實現內存的清理,因此,如果直接在堆內存中的緩沖區執行可能會發現緩沖區內存地址變化的情況,也就無從執行I/O操作。
三.DirectByteBuffer 內存申請與回收
由於DirectByteBuffer的 API使用與ByteBuffer並無太大的區別,因此本文將集中研究DirectByteBuffer是如何執行內存申請操作,以及如何對其進行內存回收操作。
3.1.內存申請
在構造DirectByteBuffer時就已經執行了內存申請操作,其中我們主要關注 Bits.reserveMemory(size, cap) 以及 Cleaner.create(this, new Deallocator(base, size, cap))。
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; } //此行代碼用於實現當DirectByteBuffer被回收時,堆外內存也會被釋放 cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
Bits.reserveMemory
static void reserveMemory(long size, int cap) { // 設置最大內存設置 if (!memoryLimitSet && VM.isBooted()) { maxMemory = VM.maxDirectMemory(); memoryLimitSet = true; } // 樂觀地嘗試預定直接內存(DirectMemory)的內存 // optimist! if (tryReserveMemory(size, cap)) { return; } // 如果預定內存失敗,則對直接內存中無用的內存執行回收操作 final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess(); // retry while helping enqueue pending Reference objects // which includes executing pending Cleaner(s) which includes // Cleaner(s) that free direct buffer memory while (jlra.tryHandlePendingReference()) { if (tryReserveMemory(size, cap)) { return; } } // 觸發GC操作 // trigger VM's Reference processing System.gc(); // 執行多次循環,嘗試進行內存回收操作,如果多次嘗試失敗之后,則拋出OutOfMemory異常 // a retry loop with exponential back-off delays // (this gives VM some time to do it's job) boolean interrupted = false; try { long sleepTime = 1; int sleeps = 0; while (true) { if (tryReserveMemory(size, cap)) { return; } if (sleeps >= MAX_SLEEPS) { break; } if (!jlra.tryHandlePendingReference()) { try { Thread.sleep(sleepTime); sleepTime <<= 1; sleeps++; } catch (InterruptedException e) { interrupted = true; } } } // no luck throw new OutOfMemoryError("Direct buffer memory"); } finally { if (interrupted) { // don't swallow interrupts Thread.currentThread().interrupt(); } } }
tryReserveMemory 此方法的主要功能就是檢查當前DirectMemory內存是否足夠構建DirectByteBuffer的緩沖區,並通過CAS的方式設置當前已使用的內存
//嘗試預定內存
private static boolean tryReserveMemory(long size, int cap) { // -XX:MaxDirectMemorySize limits the total capacity rather than the // actual memory usage, which will differ when buffers are page // aligned. long totalCap;
//檢查內存是否足夠 while (cap <= maxMemory - (totalCap = totalCapacity.get())) {
//如果內存足夠,則嘗試CAS設置totalCapacity if (totalCapacity.compareAndSet(totalCap, totalCap + cap)) { reservedMemory.addAndGet(size); count.incrementAndGet(); return true; } } return false; }
jlra.tryHandlePendingReference 為什么可以執行內存回收操作呢?其原理如下節所示。
3.2.內存釋放
結論:DirectByteBuffer中的直接內存緩沖區釋放的方式有兩種
1.ReferenceHandler線程會自動檢查有無被回收的DirectByteBuffer,如果有則執行Cleaner.clean方法釋放其對應的直接內存
2.通過調用SharedSecrets.getJavaLangRefAccess()方法來釋放內存,具體見Reference類代碼分析。
3.2.1代碼分析
此句代碼便是直接內存釋放的關鍵了。
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
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); } }
Cleaner 內部維護着一個雙向隊列,此類的定義如下所示。
請注意以下關鍵點:
Cleaner 繼承了PhantomReference幽靈引用,並且維護了一個ReferenceQueue<Object> 隊列。
public class Cleaner extends PhantomReference<Object> { private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue(); private static Cleaner first = null; private Cleaner next = null; private Cleaner prev = null; private final Runnable thunk; private static synchronized Cleaner add(Cleaner var0) { if (first != null) { var0.next = first; first.prev = var0; } first = var0; return var0; } private Cleaner(Object var1, Runnable var2) { super(var1, dummyQueue); this.thunk = var2; } public static Cleaner create(Object var0, Runnable var1) { return var1 == null ? null : add(new Cleaner(var0, var1)); } // 執行Dealloactor.run() public void clean() { if (remove(this)) { try { this.thunk.run(); } catch (final Throwable var2) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { if (System.err != null) { (new Error("Cleaner terminated abnormally", var2)).printStackTrace(); } System.exit(1); return null; } }); } } } }
而幽靈引用的定義如下所示:
public class PhantomReference<T> extends Reference<T> { public T get() { return null; } public PhantomReference(T referent, ReferenceQueue<? super T> q) { super(referent, q); } }
問題來了,說了這么多到底是誰在調用DirectByteBuffer的內存回收代碼(Cleaner.clean() -> Deallocator.run())
Reference中代碼說明了一切:
static { ThreadGroup tg = Thread.currentThread().getThreadGroup(); for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent()); Thread handler = new ReferenceHandler(tg, "Reference Handler"); /* If there were a special system-only priority greater than * MAX_PRIORITY, it would be used here */ handler.setPriority(Thread.MAX_PRIORITY); handler.setDaemon(true); handler.start(); // provide access in SharedSecrets SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() { @Override public boolean tryHandlePendingReference() { return tryHandlePending(false); } }); }
ReferenceHandler的主要代碼如下所示,主要是不斷的執行tryHandlePending 方法
public void run() { while (true) { tryHandlePending(true); } }
static boolean tryHandlePending(boolean waitForNotify) { Reference<Object> r; Cleaner c; try { synchronized (lock) { if (pending != null) { r = pending; // 'instanceof' might throw OutOfMemoryError sometimes // so do this before un-linking 'r' from the 'pending' chain... c = r instanceof Cleaner ? (Cleaner) r : null; // unlink 'r' from 'pending' chain pending = r.discovered; r.discovered = null; } else { // The waiting on the lock may cause an OutOfMemoryError // because it may try to allocate exception objects. if (waitForNotify) { lock.wait(); } // retry if waited return waitForNotify; } } } catch (OutOfMemoryError x) { // Give other threads CPU time so they hopefully drop some live references // and GC reclaims some space. // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above // persistently throws OOME for some time... Thread.yield(); // retry return true; } catch (InterruptedException x) { // retry return true; } // Cleaner.clean 方法調用處 // Fast path for cleaners if (c != null) { c.clean(); return true; } ReferenceQueue<? super Object> q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); return true; }