JDK 源碼閱讀 : DirectByteBuffer

在文章JDK源碼閱讀-ByteBuffer中,我們學習了ByteBuffer的設計。但是他是一個抽象類,真正的實現分為兩類:HeapByteBuffer與DirectByteBuffer。HeapByteBuffer是堆內ByteBuffer,使用byte[]存儲數據,是對數組的封裝,比較簡單。DirectByteBuffer是堆外ByteBuffer,直接使用堆外內存空間存儲數據,是NIO高性能的核心設計之一。本文來分析一下DirectByteBuffer的實現。

如何使用DirectByteBuffer

如果需要實例化一個DirectByteBuffer,可以使用java.nio.ByteBuffer#allocateDirect這個方法:


public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}

DirectByteBuffer實例化流程

我們來看一下DirectByteBuffer是如何構造,如何申請與釋放內存的。先看看DirectByteBuffer的構造函數:


DirectByteBuffer(int cap) { // package-private
// 初始化Buffer的四個核心屬性
super(-1, 0, cap, cap);
// 判斷是否需要頁面對齊,通過參數-XX:+PageAlignDirectMemory控制,默認為false
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 {
// 調用unsafe方法分配內存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
// 分配失敗,釋放內存
Bits.unreserveMemory(size, cap);
throw x;
}

// 初始化內存空間為0
unsafe.setMemory(base, size, (byte) 0);
// 設置內存起始地址
if (pa && (base % ps != 0)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 使用Cleaner機制註冊內存回收處理函數
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

申請內存前會調用java.nio.Bits#reserveMemory判斷是否有足夠的空間可供申請:


// 該方法主要用於判斷申請的堆外內存是否超過了用例指定的最大值
// 如果還有足夠空間可以申請,則更新對應的變量
// 如果已經沒有空間可以申請,則拋出OOME
// 參數解釋:
// size:根據是否按頁對齊,得到的真實需要申請的內存大小
// cap:用戶指定需要的內存大小(<=size)
static void reserveMemory(long size, int cap) {
// 因為涉及到更新多個靜態統計變量,這裡需要Bits類鎖
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++;
}
}
/<size>

在java.nio.Bits#reserveMemory方法中,如果空間不足,會調用System.gc()嘗試釋放內存,然後再進行判斷,如果還是沒有足夠的空間,拋出OOME。

如果分配失敗,則需要把預留的統計變量更新回去:


static synchronized void unreserveMemory(long size, int cap) {
if (reservedMemory > 0) {
reservedMemory -= size;
totalCapacity -= cap;
count--;
assert (reservedMemory > -1);
}
}

從上面幾個函數中我們可以得到信息:

  1. 可以通過-XX:+PageAlignDirectMemor參數控制堆外內存分配是否需要按頁對齊,默認不對齊。
  2. 每次申請和釋放需要調用調用Bits的reserveMemory或unreserveMemory方法,這兩個方法根據內部維護的統計變量判斷當前是否還有足夠的空間可供申請,如果有足夠的空間,更新統計變量,如果沒有足夠的空間,調用System.gc()嘗試進行垃圾回收,回收後再次進行判斷,如果還是沒有足夠的空間,拋出OOME。
  3. Bits的reserveMemory方法判斷是否有足夠內存不是判斷物理機是否有足夠內存,而是判斷JVM啟動時,指定的堆外內存空間大小是否有剩餘的空間。這個大小由參數-XX:MaxDirectMemorySize=<size>設置。/<size>
  4. 確定有足夠的空間後,使用sun.misc.Unsafe#allocateMemory申請內存
  5. 申請後的內存空間會被清零
  6. DirectByteBuffer使用Cleaner機制進行空間回收

可以看出除了判斷是否有足夠的空間的邏輯外,核心的邏輯是調用sun.misc.Unsafe#allocateMemory申請內存,我們看一下這個函數是如何申請對外內存的:


// 申請一塊本地內存。內存空間是未初始化的,其內容是無法預期的。
// 使用freeMemory釋放內存,使用reallocateMemory修改內存大小

public native long allocateMemory(long bytes);


// openjdk8/hotspot/src/share/vm/prims/unsafe.cpp
UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size))
UnsafeWrapper("Unsafe_AllocateMemory");
size_t sz = (size_t)size;
if (sz != (julong)size || size < 0) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
if (sz == 0) {
return 0;
}

sz = round_to(sz, HeapWordSize);
// 調用os::malloc申請內存,內部使用malloc函數申請內存
void* x = os::malloc(sz, mtInternal);
if (x == NULL) {
THROW_0(vmSymbols::java_lang_OutOfMemoryError());
}
//Copy::fill_to_words((HeapWord*)x, sz / HeapWordSize);
return addr_to_java(x);
UNSAFE_END
可以看出sun.misc.Unsafe#allocateMemory使用malloc這個C標準庫的函數來申請內存。
DirectByteBuffer回收流程

在DirectByteBuffer的構造函數的最後,我們看到了這樣的語句:


// 使用Cleaner機制註冊內存回收處理函數
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

這是使用Cleaner機制進行內存回收。因為DirectByteBuffer申請的內存是在堆外,DirectByteBuffer本身支持保存了內存的起始地址而已,所以DirectByteBuffer的內存佔用是由堆內的DirectByteBuffer對象與堆外的對應內存空間共同構成。堆內的佔用只是很小的一部分,這種對象被稱為冰山對象。

堆內的DirectByteBuffer對象本身會被垃圾回收正常的處理,但是對外的內存就不會被GC回收了,所以需要一個機制,在DirectByteBuffer回收時,同時回收其堆外申請的內存。

Java中可選的特性有finalize函數,但是finalize機制是Java官方不推薦的,官方推薦的做法是使用虛引用來處理對象被回收時的後續處理工作,可以參考JDK源碼閱讀-Reference。同時Java提供了Cleaner類來簡化這個實現,Cleaner是PhantomReference的子類,可以在PhantomReference被加入ReferenceQueue時觸發對應的Runnable回調。

DirectByteBuffer就是使用Cleaner機制來實現本身被GC時,回收堆外內存的能力。我們來看一下其回收處理函數是如何實現的:


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方法釋放內存
unsafe.freeMemory(address);
address = 0;
// 更新統計變量
Bits.unreserveMemory(size, capacity);
}

}

sun.misc.Unsafe#freeMemory方法使用C標準庫的free函數釋放內存空間。同時更新Bits類中的統計變量。

DirectByteBuffer讀寫邏輯


public ByteBuffer put(int i, byte x) {
unsafe.putByte(ix(checkIndex(i)), ((x)));
return this;
}

public byte get(int i) {
return ((unsafe.getByte(ix(checkIndex(i)))));
}

private long ix(int i) {
return address + (i << 0);
}

DirectByteBuffer使用sun.misc.Unsafe#getByte(long)和sun.misc.Unsafe#putByte(long, byte)這兩個方法來讀寫堆外內存空間的指定位置的字節數據。不過這兩個方法本地實現比較複雜,這裡就不分析了。

默認可以申請的堆外內存大小

上文提到了DirectByteBuffer申請內存前會判斷是否有足夠的空間可供申請,這個是在一個指定的堆外大小限制的前提下。用戶可以通過-XX:MaxDirectMemorySize=<size>這個參數來控制可以申請多大的DirectByteBuffer內存。但是默認情況下這個大小是多少呢?/<size>

DirectByteBuffer通過sun.misc.VM#maxDirectMemory來獲取這個值,可以看一下對應的代碼:


// A user-settable upper limit on the maximum amount of allocatable direct
// buffer memory. This value may be changed during VM initialization if
// "java" is launched with "-XX:MaxDirectMemorySize=<size>".
//

// The initial value of this field is arbitrary; during JRE initialization
// it will be reset to the value specified on the command line, if any,
// otherwise to Runtime.getRuntime().maxMemory().
//
private static long directMemory = 64 * 1024 * 1024;

// Returns the maximum amount of allocatable direct buffer memory.
// The directMemory variable is initialized during system initialization
// in the saveAndRemoveProperties method.
//
public static long maxDirectMemory() {
return directMemory;
}
/<size>

這裡directMemory默認賦值為64MB,那對外內存的默認大小是64MB嗎?不是,仔細看註釋,註釋中說,這個值會在JRE啟動過程中被重新設置為用戶指定的值,如果用戶沒有指定,則會設置為Runtime.getRuntime().maxMemory()。

這個過程發生在sun.misc.VM#saveAndRemoveProperties函數中,這個函數會被java.lang.System#initializeSystemClass調用:


public static void saveAndRemoveProperties(Properties props) {
if (booted)
throw new IllegalStateException("System initialization has completed");

savedProps.putAll(props);

// Set the maximum amount of direct memory. This value is controlled
// by the vm option -XX:MaxDirectMemorySize=<size>.
// The maximum amount of allocatable direct buffer memory (in bytes)
// from the system property sun.nio.MaxDirectMemorySize set by the VM.
// The system property will be removed.
String s = (String)props.remove("sun.nio.MaxDirectMemorySize");
if (s != null) {
if (s.equals("-1")) {
// -XX:MaxDirectMemorySize not given, take default
directMemory = Runtime.getRuntime().maxMemory();
} else {
long l = Long.parseLong(s);
if (l > -1)

directMemory = l;
}
}

//...
}
/<size>

所以默認情況下,可以申請的DirectByteBuffer大小為Runtime.getRuntime().maxMemory(),而這個值等於可用的最大Java堆大小,也就是我們-Xmx參數指定的值。

所以最終結論是:默認情況下,可以申請的最大DirectByteBuffer空間為Java最大堆大小的值。

和DirectByteBuffer有關的JVM選項

根據上文的分析,有兩個JVM參數與DirectByteBuffer直接相關:

  • -XX:+PageAlignDirectMemory:指定申請的內存是否需要按頁對齊,默認不對其
  • -XX:MaxDirectMemorySize=<size>,可以申請的最大DirectByteBuffer大小,默認與-Xmx相等/<size>


分享到:


相關文章: