介紹
? ? 最近在工作中使用到了DirectBuffer來(lái)進(jìn)行臨時(shí)數(shù)據(jù)的存放硅确,由于使用的是堆外內(nèi)存,省去了數(shù)據(jù)到內(nèi)核的拷貝明肮,因此效率比用ByteBuffer要高不少菱农。之前看過(guò)許多介紹DirectBuffer的文章,在這里從源碼的角度上來(lái)看一下DirectBuffer的原理晤愧。
用戶態(tài)和內(nèi)核態(tài)
? ? Intel的 X86架構(gòu)下大莫,為了實(shí)現(xiàn)外部應(yīng)用程序與操作系統(tǒng)運(yùn)行時(shí)的隔離,分為了Ring0-Ring3四種級(jí)別的運(yùn)行模式官份。Linux/Unix只使用了Ring0和Ring3兩個(gè)級(jí)別只厘。Ring0被稱為用戶態(tài),Ring3被稱為內(nèi)核態(tài)舅巷。普通的應(yīng)用程序只能運(yùn)行在Ring3羔味,并且不能訪問(wèn)Ring0的地址空間。操作系統(tǒng)運(yùn)行在Ring0钠右,并提供系統(tǒng)調(diào)用供用戶態(tài)的程序使用赋元。如果用戶態(tài)的程序的某一個(gè)操作需要內(nèi)核態(tài)來(lái)協(xié)助完成(例如讀取磁盤(pán)上的某一段數(shù)據(jù)),那么用戶態(tài)的程序就會(huì)通過(guò)系統(tǒng)調(diào)用來(lái)調(diào)用內(nèi)核態(tài)的接口,請(qǐng)求操作系統(tǒng)來(lái)完成某種操作搁凸。
? ? 下圖是用戶態(tài)調(diào)用內(nèi)核態(tài)的示意圖:
DirectBuffer的創(chuàng)建
? ? 使用下面一行代碼就可以創(chuàng)建一個(gè)1024字節(jié)的DirectBuffer:
ByteBuffer.allocateDirect(1024);
? ? 該方法調(diào)用的是new DirectByteBuffer(int cap)媚值。DirectByteBuffer的構(gòu)造函數(shù)是包級(jí)私有的,因此外部是調(diào)用不到的护糖。
下面我們來(lái)看一下這行代碼背后的邏輯:
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned(); //是否頁(yè)對(duì)齊
int ps = Bits.pageSize(); //獲取pageSize大小
long size = Math.max(1L, (long) cap + (pa ? ps : 0)); //如果是頁(yè)對(duì)齊的話褥芒,那么就加上一頁(yè)的大小
Bits.reserveMemory(size, cap); //對(duì)分配的直接內(nèi)存做一個(gè)記錄
long base = 0;
try {
base = unsafe.allocateMemory(size); //實(shí)際分配內(nèi)存
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0); //初始化內(nèi)存
//計(jì)算地址
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
//生成Cleaner
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
? ? DirectBuffer的構(gòu)造函數(shù)主要做以下三個(gè)事情:
1、根據(jù)頁(yè)對(duì)齊和pageSize來(lái)確定本次的要分配內(nèi)存實(shí)際大小
2嫡良、實(shí)際分配內(nèi)存锰扶,并且記錄分配的內(nèi)存大小
3、聲明一個(gè)Cleaner對(duì)象用于清理該DirectBuffer內(nèi)存
需要注意的是DirectBuffer的創(chuàng)建是比較耗時(shí)的寝受,所以在一些高性能的中間件或者應(yīng)用下一般會(huì)做一個(gè)對(duì)象池坷牛,用于重復(fù)利用DirectBuffer。
DirectBuffer的使用
? ? 查看DirectBuffer類(lèi)的方法聲明很澄,對(duì)于DirectBuffer的使用主要有兩類(lèi)方法京闰,putXXX和getXXX。
putXXX方法(以putInt為例):
public ByteBuffer putInt(int x) {
putInt(ix(nextPutIndex((1 << 2))), x);
return this;
}
private ByteBuffer putInt(long a, int x) {
if (unaligned) {
int y = (x);
unsafe.putInt(a, (nativeByteOrder ? y : Bits.swap(y)));
} else {
Bits.putInt(a, x, bigEndian);
}
return this;
}
? ? putInt方法會(huì)根據(jù)是否是內(nèi)存對(duì)齊分別調(diào)用unsafe.putInt或者Bits.putInt來(lái)把數(shù)據(jù)放到直接內(nèi)存中痴怨。Bits.putInt實(shí)際上會(huì)根據(jù)是大端或者是小端來(lái)區(qū)分如何把數(shù)據(jù)放到直接內(nèi)存中忙干,放的方式同樣是調(diào)用unsage.putInt。
getXXX方法(以getInt為例):
public int getInt() {
return getInt(ix(nextGetIndex((1 << 2))));
}
private int getInt(long a) {
if (unaligned) {
int x = unsafe.getInt(a);
return (nativeByteOrder ? x : Bits.swap(x));
}
return Bits.getInt(a, bigEndian);
}
? ? 首先判斷是否是頁(yè)對(duì)齊浪藻,如果不是頁(yè)對(duì)齊捐迫,那么直接通過(guò)unsafe.getInt來(lái)獲取數(shù)據(jù);如果是頁(yè)對(duì)齊爱葵,那么通過(guò)Bits.getInt方法來(lái)獲取數(shù)據(jù)施戴。Bits.getInt同樣是根據(jù)大端還是小端,調(diào)用unsafe.getInt來(lái)獲取數(shù)據(jù)萌丈。
DirectBuffer內(nèi)存回收
? ? DirectBuffer內(nèi)存回收主要有兩種方式赞哗,一種是通過(guò)System.gc來(lái)回收,另一種是通過(guò)構(gòu)造函數(shù)里創(chuàng)建的Cleaner對(duì)象來(lái)回收辆雾。
System.gc回收
在DirectBuffer的構(gòu)造函數(shù)中肪笋,用到了Bit.reserveMemory這個(gè)方法,該方法如下
static void reserveMemory(long size, int cap) {
······
if (tryReserveMemory(size, cap)) {
return;
}
······
while (jlra.tryHandlePendingReference()) {
if (tryReserveMemory(size, cap)) {
return;
}
}
System.gc();
// 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();
}
}
}
? ? reserveMemory方法首先嘗試分配內(nèi)存度迂,如果分配成功的話藤乙,那么就直接退出。如果分配失敗那么就通過(guò)調(diào)用tryHandlePendingReference來(lái)嘗試清理堆外內(nèi)存(最終調(diào)用的是Cleaner的clean方法惭墓,其實(shí)就是unsafe.freeMemory然后釋放內(nèi)存)坛梁,清理完內(nèi)存之后再嘗試分配內(nèi)存。如果還是失敗腊凶,調(diào)用System.gc()來(lái)觸發(fā)一次FullGC進(jìn)行回收(前提是沒(méi)有加-XX:-+DisableExplicitGC參數(shù))划咐。GC完之后再進(jìn)行內(nèi)存分配拴念,失敗的話就會(huì)進(jìn)行sleep,然后再進(jìn)行嘗試褐缠。每次sleep的時(shí)間是逐步增加的政鼠,規(guī)律是1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)。如果最終還沒(méi)有可分配的內(nèi)存送丰,那么就會(huì)拋出OOM異常缔俄。
? ? 為什么是通過(guò)調(diào)用tryHandlePendingReference來(lái)回收內(nèi)存呢?答案是JVM在判斷內(nèi)存不可達(dá)之后會(huì)把需要GC的不可達(dá)對(duì)象放在一個(gè)PendingList中器躏,然后應(yīng)用程序就可以看到這些對(duì)象。通過(guò)調(diào)用tryHandlePendingReference來(lái)訪問(wèn)這些不可達(dá)對(duì)象蟹略。如果不可達(dá)對(duì)象是Cleaner類(lèi)型登失,也就是說(shuō)關(guān)聯(lián)了堆外的DirectBuffer,那么該DirectBuffer就可以被回收了挖炬,通過(guò)調(diào)用Cleaner的clean方法來(lái)回收這部分堆外內(nèi)存揽浙。
這個(gè)邏輯就是進(jìn)行堆外內(nèi)存分配時(shí)觸發(fā)的回收內(nèi)存邏輯,也就是說(shuō)在分配的時(shí)候如果遇到堆外內(nèi)存不足意敛,可能會(huì)觸發(fā)FullGC馅巷,然后嘗試進(jìn)行分配。這也是為什么在一些用到堆外內(nèi)存的應(yīng)用中不建議加上-XX:-+DisableExplicitGC參數(shù)草姻。
Cleaner對(duì)象回收
? ? 另個(gè)觸發(fā)堆外內(nèi)存回收的時(shí)機(jī)是通過(guò)Cleaner對(duì)象的clean方法進(jìn)行回收钓猬。在每次新建一個(gè)DirectBuffer對(duì)象的時(shí)候,會(huì)同時(shí)創(chuàng)建一個(gè)Cleaner對(duì)象撩独,同一個(gè)進(jìn)程創(chuàng)建的所有的DirectBuffer對(duì)象跟Cleaner對(duì)象的個(gè)數(shù)是一樣的敞曹,并且所有的Cleaner對(duì)象會(huì)組成一個(gè)鏈表,前后相連综膀。
public static Cleaner create(Object ob, Runnable thunk) {
if (thunk == null)
return null;
return add(new Cleaner(ob, thunk));
}
? ? Cleaner對(duì)象的clean方法執(zhí)行時(shí)機(jī)是JVM在判斷該Cleaner對(duì)象關(guān)聯(lián)的DirectBuffer已經(jīng)不被任何對(duì)象引用了(也就是經(jīng)過(guò)可達(dá)性分析判定為不可達(dá)的時(shí)候)澳迫。此時(shí)Cleaner對(duì)象會(huì)被JVM掛到PendingList上。然后有一個(gè)固定的線程掃描這個(gè)List剧劝,如果遇到Cleaner對(duì)象橄登,那么就執(zhí)行clean方法。
? ? ? DirectBuffer在一些高性能的中間件上使用還是相當(dāng)廣泛的讥此。正確的使用可以提升程序的性能拢锹,降低GC的頻率。