Java NIO記憶體對映---上G大檔案處理
摘要:本文主要講了java中記憶體對映的原理及過程,與傳統IO進行了對比,最後,用例項說明了結果。
一、java中的記憶體對映IO和記憶體對映檔案是什麼?
記憶體對映檔案非常特別,它允許Java程式直接從記憶體中讀取檔案內容,通過將整個或部分檔案對映到記憶體,由作業系統來處理載入請求和寫入檔案,應用只需要和記憶體打交道,這使得IO操作非常快。載入記憶體對映檔案所使用的記憶體在Java堆區之外。Java程式語言支援記憶體對映檔案,通過java.nio包和MappedByteBuffer 可以從記憶體直接讀寫檔案。
記憶體對映檔案
記憶體對映檔案,是由一個檔案到一塊記憶體的對映。Win32提供了允許應用程式把檔案對映到一個程序的函式 (CreateFileMapping)。記憶體對映檔案與虛擬記憶體有些類似,通過記憶體對映檔案可以保留一個地址空間的區域,同時將物理儲存器提交給此區域,記憶體檔案對映的物理儲存器來自一個已經存在於磁碟上的檔案,而且在對該檔案進行操作之前必須首先對檔案進行對映。使用記憶體對映檔案處理儲存於磁碟上的檔案時,將不必再對檔案執行I/O操作,使得記憶體對映檔案在處理大資料量的檔案時能起到相當重要的作用。
記憶體對映IO
在傳統的檔案IO操作中,我們都是呼叫作業系統提供的底層標準IO系統呼叫函式 read()、write() ,此時呼叫此函式的程序(在JAVA中即java程序)由當前的使用者態切換到核心態,然後OS的核心程式碼負責將相應的檔案資料讀取到核心的IO緩衝區,然 後再把資料從核心IO緩衝區拷貝到程序的私有地址空間中去,這樣便完成了一次IO操作。這麼做是為了減少磁碟的IO操作,為了提高效能而考慮的,因為我們的程式訪問一般都帶有區域性性,也就是所 謂的區域性性原理,在這裡主要是指的空間區域性性,即我們訪問了檔案的某一段資料,那麼接下去很可能還會訪問接下去的一段資料,由於磁碟IO操作的速度比直接 訪問記憶體慢了好幾個數量級,所以OS根據區域性性原理會在一次 read()系統呼叫過程中預讀更多的檔案資料快取在核心IO緩衝區中,當繼續訪問的檔案資料在緩衝區中時便直接拷貝資料到程序私有空間,避免了再次的低 效率磁碟IO操作。其過程如下
記憶體對映檔案和之前說的 標準IO操作最大的不同之處就在於它雖然最終也是要從磁碟讀取資料,但是它並不需要將資料讀取到OS核心緩衝區,而是直接將程序的使用者私有地址空間中的一 部分割槽域與檔案物件建立起對映關係,就好像直接從記憶體中讀、寫檔案一樣,速度當然快了。
記憶體對映的優缺點
記憶體對映IO最大的優點可能在於效能,這對於建立高頻電子交易系統尤其重要。記憶體對映檔案通常比標準通過正常IO訪問檔案要快。另一個巨大的優勢是記憶體映 射IO允許載入不能直接訪問的潛在巨大檔案 。經驗表明,記憶體對映IO在大檔案處理方面效能更加優異。儘管它也有不足——增加了頁面錯誤的數目。由於作業系統只將一部分檔案載入到記憶體,如果一個請求 頁面沒有在記憶體中,它將導致頁面錯誤。同樣它可以被用來在兩個程序中共享資料。
支援記憶體對映IO的作業系統
大多數主流作業系統比如Windows平臺,UNIX,Solaris和其他類UNIX作業系統都支援記憶體對映IO和64位架構,你幾乎可以將所有檔案對映到記憶體並通過JAVA程式語言直接訪問。
Java的記憶體對映IO的要點
如下為一些你需要了解的java記憶體對映要點:
java通過java.nio包來支援記憶體對映IO。
記憶體對映檔案主要用於效能敏感的應用,例如高頻電子交易平臺。
通過使用記憶體對映IO,你可以將大檔案載入到記憶體。
記憶體對映檔案可能導致頁面請求錯誤,如果請求頁面不在記憶體中的話。
對映檔案區域的能力取決於於記憶體定址的大小。在32位機器中,你不能訪問超過4GB或2 ^ 32(以上的檔案)。
記憶體對映IO比起Java中的IO流要快的多。
載入檔案所使用的記憶體是Java堆區之外,並駐留共享記憶體,允許兩個不同程序共享檔案。
記憶體對映檔案讀寫由作業系統完成,所以即使在將內容寫入記憶體後java程式崩潰了,它將仍然會將它寫入檔案直到作業系統恢復。
出於效能考慮,推薦使用直接位元組緩衝而不是非直接緩衝。
不要頻繁呼叫MappedByteBuffer.force()方法,這個方法意味著強制作業系統將記憶體中的內容寫入磁碟,所以如果你每次寫入記憶體對映檔案都呼叫force()方法,你將不會體會到使用對映位元組緩衝的好處,相反,它(的效能)將類似於磁碟IO的效能。
萬一發生了電源故障或主機故障,將會有很小的機率發生記憶體對映檔案沒有寫入到磁碟,這意味著你可能會丟失關鍵資料。
二、例項程式碼
1、傳統IO讀取資料,不指定緩衝區大小
/**
* 傳統IO讀取資料,不指定緩衝區大小
* @author linbingwen
* @since 2015年9月5日
* @param path
* @return
*/
public static void readFile1(String path) {
long start = System.currentTimeMillis();//開始時間
File file = new File(path);
if (file.isFile()) {
BufferedReader bufferedReader = null;
FileReader fileReader = null;
try {
fileReader = new FileReader(file);
bufferedReader = new BufferedReader(fileReader);
String line = bufferedReader.readLine();
System.out.println("========================== 傳統IO讀取資料,使用虛擬機器堆記憶體 ==========================");
while (line != null) { //按行讀資料
System.out.println(line);
line = bufferedReader.readLine();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//最後一定要關閉
try {
fileReader.close();
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();//結束時間
System.out.println("傳統IO讀取資料,不指定緩衝區大小,總共耗時:"+(end - start)+"ms");
}
}
}
2、傳統IO讀取資料,指定緩衝區大小
/**
* 傳統IO讀取資料,指定緩衝區大小
* @author linbingwen
* @since 2015年9月5日
* @param path
* @return
* @throws FileNotFoundException
*/
public static void readFile2(String path) throws FileNotFoundException {
long start = System.currentTimeMillis();//開始時間
int bufSize = 1024 * 1024 * 5;//5M緩衝區
File fin = new File(path); // 檔案大小200M
FileChannel fcin = new RandomAccessFile(fin, "r").getChannel();
ByteBuffer rBuffer = ByteBuffer.allocate(bufSize);
String enterStr = "\n";
long len = 0L;
try {
byte[] bs = new byte[bufSize];
String tempString = null;
while (fcin.read(rBuffer) != -1) {//每次讀5M到緩衝區
int rSize = rBuffer.position();
rBuffer.rewind();
rBuffer.get(bs);//將緩衝區資料讀到陣列中
rBuffer.clear();//清除緩衝
tempString = new String(bs, 0, rSize);
int fromIndex = 0;//緩衝區起始
int endIndex = 0;//緩衝區結束
//按行讀緩衝區資料
while ((endIndex = tempString.indexOf(enterStr, fromIndex)) != -1) {
String line = tempString.substring(fromIndex, endIndex);//轉換一行
System.out.print(line);
fromIndex = endIndex + 1;
}
}
long end = System.currentTimeMillis();//結束時間
System.out.println("傳統IO讀取資料,指定緩衝區大小,總共耗時:"+(end - start)+"ms");
} catch (IOException e) {
e.printStackTrace();
}
}
3、記憶體對映讀檔案
/**
* NIO 記憶體對映讀大檔案
* @author linbingwen
* @since 2015年9月15日
* @param path
*/
public static void readFile3(String path) {
long start = System.currentTimeMillis();//開始時間
long fileLength = 0;
final int BUFFER_SIZE = 0x300000;// 3M的緩衝
File file = new File(path);
fileLength = file.length();
try {
MappedByteBuffer inputBuffer = new RandomAccessFile(file, "r").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, fileLength);// 讀取大檔案
byte[] dst = new byte[BUFFER_SIZE];// 每次讀出3M的內容
for (int offset = 0; offset < fileLength; offset += BUFFER_SIZE) {
if (fileLength - offset >= BUFFER_SIZE) {
for (int i = 0; i < BUFFER_SIZE; i++)
dst[i] = inputBuffer.get(offset + i);
} else {
for (int i = 0; i < fileLength - offset; i++)
dst[i] = inputBuffer.get(offset + i);
}
// 將得到的3M內容給Scanner,這裡的XXX是指Scanner解析的分隔符
Scanner scan = new Scanner(new ByteArrayInputStream(dst)).useDelimiter(" ");
while (scan.hasNext()) {
// 這裡為對讀取文字解析的方法
System.out.print(scan.next() + " ");
}
scan.close();
}
System.out.println();
long end = System.currentTimeMillis();//結束時間
System.out.println("NIO 記憶體對映讀大檔案,總共耗時:"+(end - start)+"ms");
} catch (Exception e) {
e.printStackTrace();
}
}
三、測試對比
1、100M檔案
檔案大小如下:
呼叫如下:
public static void main(String args[]) {
String path = "D:" + File.separator + "CES_T_MSM_LIQ-TRANS-ESP_20150702_01.DAT";
readFile1(path);
//readFile2(path);
//readFile3(path);
}
(1)傳統IO讀取資料,不指定緩衝區大小,總共耗時:80264ms
其記憶體使用如下:
(2)傳統IO讀取資料,指定緩衝區大小,總共耗時:80612ms其記憶體使用如下:
(3)NIO 記憶體對映讀大檔案,總共耗時:90955ms其記憶體使用如下:
分析發現記憶體對映並沒有比傳統IO快多少,甚至還更加慢了,有可能是因為磁碟IO操作多了,反而降低了其效率,記憶體對映看來還是對大檔案比較有好的效果。小檔案基本上是沒有多大的差別的。
2、1.2G檔案
傳統IO讀取資料,不指定緩衝區大小,總共耗時:1245111ms
NIO 記憶體對映讀大檔案,總共耗時:1223877ms(大概20分鐘多點)