傳統(tǒng)流式IO
傳統(tǒng)的Java IO是流式的IO实檀,從諸如類名InputStream
和OutputStream
中就可以看出读规。流式IO是單向的,分為輸入和輸出流杉允。在使用輸入流或者輸出流讀寫文件時(shí)邑贴,每次讀寫操作是以字節(jié)為單位席里,我們需要指定讀出或者寫入的大小,中間沒有任何用戶空間的緩存拢驾。例如從文件中讀取4字節(jié)長(zhǎng)度的數(shù)據(jù)奖磁,Java會(huì)創(chuàng)建一個(gè)4字節(jié)長(zhǎng)度的byte數(shù)組,然后通過JNI層經(jīng)由系統(tǒng)調(diào)用read讀文件繁疤,每次讀入一個(gè)字節(jié)的數(shù)據(jù)咖为,將數(shù)據(jù)寫入對(duì)應(yīng)的byte數(shù)組的正確位置。一共需要進(jìn)行4次系統(tǒng)調(diào)用稠腊,因?yàn)槊看挝覀冎荒茏x入一個(gè)字節(jié)躁染。隨著文件讀入的進(jìn)行,我們沒有辦法重新訪問我們已經(jīng)讀入的數(shù)據(jù)架忌,因?yàn)榱魇菃蜗虻耐掏覀儾荒躶eek某個(gè)位置,除非我們自己將這些已經(jīng)讀入的數(shù)據(jù)進(jìn)行了緩存叹放,才能在以后需要時(shí)進(jìn)行訪問备畦。當(dāng)我們寫數(shù)據(jù)的時(shí)候也是如此,我們每次只能寫入一個(gè)字節(jié)的數(shù)據(jù)许昨,寫4個(gè)字節(jié)的數(shù)據(jù)就需要4次系統(tǒng)調(diào)用。系統(tǒng)調(diào)用需要從用戶態(tài)切換到內(nèi)核態(tài)褥赊,然后再切換回來糕档,可想而知,流式的IO的讀寫性能開銷是很大的拌喉。如下是Java中流式IO讀寫的實(shí)現(xiàn)速那,從代碼中我們就可以印證上面的事實(shí)。
//java.io.InputStream#read(byte[], int, int)
public int read(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int c = read();
if (c == -1) {
return -1;
}
b[off] = (byte)c;
int i = 1;
try {
for (; i < len ; i++) {
c = read(); //每次只讀一個(gè)字節(jié)
if (c == -1) {
break;
}
b[off + i] = (byte)c;
}
} catch (IOException ee) {
}
return i;
}
//java.io.OutputStream#write(byte[], int, int)
public void write(byte b[], int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException();
} else if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
for (int i = 0 ; i < len ; i++) {
write(b[off + i]); //每次只寫一個(gè)字節(jié)
}
}
NIO
JDK 1.4之后尿背,java引入了NIO端仰。NIO以塊為單位進(jìn)行讀寫,而不是以單字節(jié)為單位田藐。Channel在NIO中代表一個(gè)通道荔烧,我們可以操作通道進(jìn)行讀寫,換句話說汽久,通道是雙向的鹤竭。通過NIO讀寫文件,我們并不是直接操作通道景醇,而是通過Buffer來中轉(zhuǎn)臀稚。Buffer代表一塊緩沖區(qū),其實(shí)就是一個(gè)字節(jié)數(shù)組三痰。當(dāng)我們要寫文件時(shí)吧寺,首先將數(shù)據(jù)寫入對(duì)應(yīng)的buffer中窜管,然后通過channel將buffer中的數(shù)據(jù)寫入文件。而當(dāng)我們需要讀入數(shù)據(jù)時(shí)稚机,也是首先將數(shù)據(jù)讀入一個(gè)Buffer中幕帆,然后從buffer中訪問。因?yàn)槊看尾僮魇且詨K為單位的抒钱,因此我們能大大減少系統(tǒng)調(diào)用的次數(shù)蜓肆,極大的提高IO性能。同時(shí)Buffer作為一個(gè)緩沖區(qū)也允許我們?cè)谥蟮哪扯螘r(shí)間內(nèi)重新訪問之前的數(shù)據(jù)谋币,Buffer內(nèi)部會(huì)自己維護(hù)數(shù)據(jù)的位置信息仗扬,如position
、limit
和capacity
等蕾额。
DirectByteBuffer vs HeapByteBuffer
ByteBuffer代表一個(gè)字節(jié)數(shù)組的緩沖區(qū)早芭。Java提供了direct和non-direct buffer。java.nio.ByteBuffer#allocate會(huì)創(chuàng)建一個(gè)HeapByteBuffer诅蝶,即分配在jvm heap上的一個(gè)字節(jié)數(shù)組退个。而通過java.nio.ByteBuffer#allocateDirect方法返回一個(gè)DirectByteBuffer對(duì)象,它也是封裝了一個(gè)字節(jié)數(shù)組调炬,但是這個(gè)字節(jié)數(shù)組并不是直接分配在通用的jvm heap上的语盈,而是另外一塊單獨(dú)的內(nèi)存區(qū)域中(人們喜歡將之稱為堆外內(nèi)存),在不同的虛擬機(jī)版本可能有不同的實(shí)現(xiàn)缰泡。例如ART運(yùn)行時(shí)刀荒,會(huì)有一個(gè)heap之外的區(qū)域,我理解為大對(duì)象區(qū)域
棘钞,這個(gè)區(qū)域主要用來分配一些大對(duì)象缠借,如Bitmap,DirectByteeBuffer等宜猜。我們都知道大對(duì)象對(duì)jvm的GC會(huì)造成一些影響泼返,所以單獨(dú)開辟這些區(qū)域用來存儲(chǔ)一些生命周期長(zhǎng)的大對(duì)象是有道理,可以減少正常GC的次數(shù)姨拥,提高內(nèi)存效率绅喉。
在進(jìn)行NIO時(shí),我們可以通過DirectByteBuffer提高IO性能垫毙。官方的原話是:
A byte buffer is either <i>direct</i> or <i>non-direct</i>. Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations.
到底是怎么個(gè)性能提高法呢霹疫?還是看代碼更清晰。
//IOUtil.java
static int write(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
if(var1 instanceof DirectBuffer) {
return writeFromNativeBuffer(var0, var1, var2, var4); //如果是directbytebuffer综芥,直接寫
} else {
int var5 = var1.position();
int var6 = var1.limit();
assert var5 <= var6;
int var7 = var5 <= var6?var6 - var5:0;
ByteBuffer var8 = Util.getTemporaryDirectBuffer(var7); //獲取一個(gè)臨時(shí)的directbytebuffer
int var10;
try {
var8.put(var1); //復(fù)制數(shù)據(jù)到directbytebuffer之后再寫
var8.flip();
var1.position(var5);
int var9 = writeFromNativeBuffer(var0, var8, var2, var4);
if(var9 > 0) {
var1.position(var5 + var9);
}
var10 = var9;
} finally {
Util.offerFirstTemporaryDirectBuffer(var8);
}
return var10;
}
}
static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
if(var1.isReadOnly()) {
throw new IllegalArgumentException("Read-only buffer");
} else if(var1 instanceof DirectBuffer) { //是directbytebuffer 直接讀入
return readIntoNativeBuffer(var0, var1, var2, var4);
} else {
ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining()); //獲取一個(gè)臨時(shí)的directbytebuffer
int var7;
try {
int var6 = readIntoNativeBuffer(var0, var5, var2, var4); //讀入數(shù)據(jù)到directbytebuffer中
var5.flip();
if(var6 > 0) {
var1.put(var5); //拷貝數(shù)據(jù)到目標(biāo)buffer中
}
var7 = var6;
} finally {
Util.offerFirstTemporaryDirectBuffer(var5);
}
return var7;
}
}
在使用NIO進(jìn)行讀寫的時(shí)候丽蝎,最終會(huì)調(diào)用IOUtil中的相關(guān)read、write方法⊥雷瑁可以看到如果是DirectByteBuffer红省,在IO時(shí)直接在該buffer上進(jìn)行讀寫。如果不是国觉,則需要獲取一個(gè)臨時(shí)的DirectByteBuffer(jvm從directbytebuffer cache中獲取)吧恃,將數(shù)據(jù)拷貝到directbytebuffer中再寫入或者讀入directbuffer中在拷貝到目標(biāo)Buffer中÷榫鳎可以看到痕寓,如果是DirectByteBuffer,那么可以省去了很多拷貝的開銷蝇闭。那么jvm為什么需要一個(gè)中間的DirectByteBuffer緩沖區(qū)呢呻率?我的猜想是普通的buffer是分配在heap上的,可能是內(nèi)存空間不連續(xù)的字節(jié)數(shù)組呻引,而且隨著程序的運(yùn)行 GC可能會(huì)移動(dòng)對(duì)應(yīng)的字節(jié)數(shù)組礼仗,這就給IO帶來了挑戰(zhàn)。反觀DirectByteBuffer逻悠,它是連續(xù)的字節(jié)數(shù)組元践,不是分配在堆上的,受GC影響小童谒,而且一般而言DirectByteBuffer分配內(nèi)存都是指定non-movale的
单旁。但是DirectByteBuffer也不是沒有任何缺點(diǎn),因?yàn)樗皇窃诙焉系募⒁粒钥赡茉斐稍L問速度慢慎恒,并且DirectByteBuffer的分配和釋放開銷比HeapByteBuffer要大。