在傳統的文件IO操作中,我們都是調用操作系統提供的底層標準IO系統調用函數read()、write() ,此時調用此函數的進程(在JAVA中即java進程)由當前的用戶態切換到內核態,然後OS的內核代碼負責將相應的文件數據讀取到內核的IO緩衝區,然後再把數據從內核IO緩衝區拷貝到進程的私有地址空間中去,這樣便完成了一次IO操作。這麼做是爲了減少磁盤的IO操作,爲了提高性能而考慮的,因爲我們的程序訪問一般都帶有局部性,也就是所謂的局部性原理,在這裏主要是指的空間局部性,即我們訪問了文件的某一段數據,那麼接下去很可能還會訪問接下去的一段數據,由於磁盤IO操作的速度比直接訪問內存慢了好幾個數量級,所以OS根據局部性原理會在一次read()系統調用過程中預讀更多的文件數據緩存在內核IO緩衝區中,當繼續訪問的文件數據在緩衝區中時便直接拷貝數據到進程私有空間,避免了再次的低效率磁盤IO操作。
內存映射文件 MappedByteBuffer
JAVA處理大文件,一般用BufferedReader,BufferedInputStream這類帶緩衝的IO類,不過如果文件超大的話,更快的方式是採用MappedByteBuffer。
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, len);
FileChannel提供了map方法來把文件映射爲內存映像文件:可以把文件的從position開始的size大小的區域映射爲內存映像文件,MapMode表示了可訪問該內存映像文件的方式:
READ_ONLY,(只讀): 試圖修改得到的緩衝區將導致拋出ReadOnlyBufferException。(MapMode.READ_ONLY)
READ_WRITE(讀/寫): 對得到的緩衝區的更改最終將傳播到文件;該更改對映射到同一文件的其他程序不一定是可見的。(MapMode.READ_WRITE)
PRIVATE(專用): 對得到的緩衝區的更改不會傳播到文件,並且該更改對映射到同一文件的其他程序也不是可見的;相反,會創建緩衝區已修改部分的專用副本。(MapMode.PRIVATE)
MappedByteBuffer是ByteBuffer的子類,其擴充了三個方法:
force():緩衝區是READ_WRITE模式下,此方法對緩衝區內容的修改強行寫入文件;
load():將緩衝區的內容載入內存,並返回該緩衝區的引用;
isLoaded():如果緩衝區的內容在物理內存中,則返回真,否則返回假;
通過下面的例子測試普通文件通道IO和內存映射IO的速度:
public class FileChannelTest {
public static void main(String[] args) throws IOException {
testFileChannel();
testMappedByteBuffer();
}
public static void testFileChannel() throws IOException {
RandomAccessFile file = null;
try {
file = new RandomAccessFile("/Users/xin/Downloads/b.txt", "rw");
FileChannel channel = file.getChannel();
ByteBuffer buff = ByteBuffer.allocate(1024);
long timeBegin = System.currentTimeMillis();
while (channel.read(buff) != -1) {
buff.flip();
buff.clear();
}
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (file != null) {
file.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void testMappedByteBuffer() throws IOException {
RandomAccessFile file = null;
try {
file = new RandomAccessFile("/Users/xin/Downloads/b.txt", "rw");
FileChannel fc = file.getChannel();
int len = (int) file.length();
MappedByteBuffer buffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, len);
byte[] b = new byte[1024];
long timeBegin = System.currentTimeMillis();
for (int offset = 0; offset < len; offset += 1024) {
if (len - offset > 1024) {
buffer.get(b);
} else {
buffer.get(new byte[len - offset]);
}
}
long timeEnd = System.currentTimeMillis();
System.out.println("Read time: " + (timeEnd - timeBegin) + "ms");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (file != null) {
file.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
控制檯輸出結果如下:
Read time: 302ms
Read time: 61ms
根據測試結果證明了內存映射文件比文件通道速度快很多。
內存映射文件和之前說的標準IO操作最大的不同之處就在於它雖然最終也是要從磁盤讀取數據,但是它並不需要將數據讀取到OS內核緩衝區,而是直接將進程的用戶私有地址空間中的一部分區域與文件對象建立起映射關係,就好像直接從內存中讀、寫文件一樣,速度當然快了。內存映射文件的效率比標準IO高的重要原因就是因爲少了把數據拷貝到OS內核緩衝區這一步。
zerocopy技術
zerocopy技術的目標就是提高IO密集型JAVA應用程序的性能。IO操作需要數據頻繁地在內核緩衝區和用戶緩衝區之間拷貝,而zerocopy技術可以減少這種拷貝的次數,同時也降低了上下文切換(用戶態與內核態之間的切換)的次數。在Java中的應用就是java.nio.channels.FileChannel類的transferTo()方法可以直接將字節傳送到可寫的通道中,並不需要將字節轉入用戶緩衝區。
package java.nio.channels;
public abstract class FileChannel
extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
{
... ...
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
... ...
}
FileChannel fileChannel = fis.getChannel();
fileChannel.transferTo(0, fileChannel.size(), targetChannel);
正常讀取文件再發送出去需要經歷一下幾個步驟:
從本地磁盤或者網絡讀取數據--->數據進入內核緩衝區--->用戶緩衝區--->內核緩衝區--->通過socket發送
數據每次在內核緩衝區與用戶緩衝區之間的拷貝會消耗CPU以及內存的帶寬。而zerocopy有效減少了這種拷貝次數,用戶程序執行transferTo()方法,導致一次系統調用,從用戶態切換到內核態,完成的動作是:
從本地磁盤或者網絡讀取數據--->數據進入內核緩衝區--->通過socket發送
zerocopy好處就是減少了將數據從內核緩衝區拷貝到用戶緩衝區,再拷貝回內核緩衝區,減少了拷貝次數和上下文切換次數。