摘 要:程序的I/O性能是影響程序性能的關鍵之一。有效地使用緩沖機制可以極大地提高程序的I/O性能。本文首先對java.io包做了一個簡要的介紹,指出導致I/O性能低下的根本原因,然后循序漸進地采取了三種策略,逐步將I/O性能提高到168倍,這三種策略分別是采用緩沖流,定制自己的緩沖區(qū),定制靜態(tài)的固定長度的緩沖區(qū)。
關鍵詞:I/O性能;流;緩沖區(qū);分配和回收
中圖分類號:TP311 文獻標識碼:A
Java應用程序都會經(jīng)常使用java.io包,例如磁盤文件讀寫和通過網(wǎng)絡傳輸數(shù)據(jù)。然而初學者由于對java.io包的理解的局限性導致編寫的程序I/O性能很差。所幸的是,只要對java.io包有了很好的理解,就可以杜絕這方面的問題,從而保證程序擁有較好的I/O性能。
本文首先對java.io包做一個簡要的介紹,然后指出導致I/O性能低下的根本原因是沒有采用緩沖機制,然后循序漸進地采取了三種策略,將I/O性能逐步提高到168倍,這三種策略分別是采用緩沖流,定制自己的緩沖區(qū),定制靜態(tài)的固定長度的緩沖區(qū)。
1 I/O操作的基本概念
在Java I/O中最基本的概念是流(Stream),流是一個連續(xù)的字節(jié)序列,包括輸入流和輸出流,輸入流用來讀取這個序列,而輸出流則用來寫這個序列,在默認情況下Java的流操作是基于字節(jié)的,即一次只讀或只寫一個字節(jié)。
java.io包提供了InputStream和OutputStream作為對I/O操作的抽象,這兩個接口決定了類層次結構的基本格局。InputStream和OutputStream的具體實現(xiàn)類提供了對不同數(shù)據(jù)源的訪問,如磁盤文件和網(wǎng)絡連接。
java.io包還提供了過濾流(Filter Stream),過濾流并不指向具體的數(shù)據(jù)源,而是在其他流之上進行了包裝,這些過濾流其實是java.io包的核心。從性能的角度來看,最重要的過濾流是緩沖流(Buffered Stream),圖1顯示了一個簡化的類層次結構圖。
2 緩沖流
導致I/O性能低下的主要原因是沒有對I/O操作進行緩沖。眾所周知,硬盤擅長于大塊數(shù)據(jù)的讀寫,但是在小量數(shù)據(jù)的讀寫上性能不好,所以,為了最大化I/O性能,我們應該選擇批量的數(shù)據(jù)操作,而緩沖流正是為這個目的設計的。緩沖流,包括BufferedInputStream和BufferedOutputStream, 它為I/O流增加了內存緩沖區(qū),使得Java程序一次可以向底層設備寫入或者讀取大量數(shù)據(jù),從而提高了程序的性能。
為了更好地理解緩沖流的效果,請閱讀程序清單一,這個例子采用了原始的文件流實現(xiàn)文件的拷貝。copy方法打開了一個FileInputStream 和一個FileOutputStream,并將數(shù)據(jù)從一個流直接拷貝到另一個流。由于read和write方法是基于字節(jié)的,所以實際的磁盤讀寫也是按字節(jié)發(fā)生的。實踐證明使用這段代碼拷貝一個8M的文本文件需要花費36750ms。
程序清單一中的copy方法只需稍作修改就能有效地改善性能,如程序清單二所示,它在原始的文件流之上采用了緩沖流BufferedInputStream和BufferedOutputStream,緩沖流將每一個小的讀寫請求積攢起來,然后一次性地批處理,通常是將幾千個讀寫請求合并成一個大的請求。改進后的copy方法拷貝一個8M的文本文件需要花費1187ms,性能大約提高了31倍。
3 建立自己的緩沖
緩沖流雖然在它的內部增加了內存緩沖區(qū),使得在緩沖區(qū)和底層設備之間寫入或讀取大量數(shù)據(jù)成為可能[2,3],但是在這之上Java程序仍舊使用while循環(huán)從該流(實際上是緩沖數(shù)組)按字節(jié)讀寫數(shù)據(jù)。由于緩沖數(shù)組的大小是一定的,JVM需要針對數(shù)組做越界檢查,該操作會導致額外的系統(tǒng)開銷。另一方面,為了支持多線程環(huán)境,將數(shù)據(jù)從BufferedInputStream的緩沖數(shù)組拷貝到BufferedOutputStream的緩沖數(shù)組,其中的方法調用很多都是synchronized, 這也會導致額外的系統(tǒng)開銷。所以盡管程序清單二所示代碼可以有效地改善性能,但是改善的程度仍然不盡人意。
還是利用硬盤擅長讀寫大塊數(shù)據(jù)這一特性,我們可以自己建立緩沖,以避免上述額外的系統(tǒng)開銷。這需要使用InputStream和OutputStream提供的兩個重載方法,它們允許按字節(jié)讀寫,也可以按字節(jié)數(shù)組讀寫,如下所示:
public int read();
public int read(byte[] bytes);
public int write();
public int write(byte[] bytes);
程序清單三創(chuàng)建了自己的緩沖區(qū),即字節(jié)數(shù)組byte[] buffer, 其大小是整個文件的字節(jié)長度,然后將整個文件一次性讀入內存,再一次性寫到另一個文件。這個代碼非常快,拷貝一個8M的文本文件所花費時間降低到750ms。
注意,使用這個策略需要權衡兩個因素。首先是緩沖區(qū)的大小,它所創(chuàng)建的緩沖區(qū)的大小等于被拷貝的文件大小,當文件很大時,該緩沖區(qū)也會很大。第二,它為每次文件拷貝操作都要創(chuàng)建一個新的緩沖區(qū),當有大量文件需要拷貝時,JVM不得不分配和回收這些大緩沖區(qū)內存,這對程序性能是極大的傷害。
如何在保持速度甚至速度更快的前提下避免這兩個缺陷呢?方法是這樣的:創(chuàng)建一個靜態(tài)的固定長度的字節(jié)數(shù)組,如1024*1024字節(jié)即1M,每次只讀寫1M的數(shù)據(jù)。雖然對于大于1M的文件會需要多次讀寫才能完成整個文件的拷貝,但是這樣避免了內存的反復分配和回收。緩沖區(qū)大小是可以調整的,針對某個具體的應用場景,我們可以在速度和內存之間取得一個最佳的平衡。
程序清單四的copy方法使用了這樣的1M字節(jié)的數(shù)組,這個版本表現(xiàn)更為出色,它拷貝一個8M的文本文件所花費時間只有218ms,性能提高了168倍。
值得注意的是代碼中的同步塊,在單線程環(huán)境下沒有同步塊是可以的,但是在多線程環(huán)境下需要同步塊防止多個線程同時對緩沖區(qū)實施寫操作。盡管同步會帶來系統(tǒng)開銷,但是由于while循環(huán)的次數(shù)很?。?M文件只要循環(huán)8次),所以由此帶來的性能損失是可以忽略不計的。實驗證明,有同步塊的版本和去掉同步塊的版本,性能是一樣的。
下表顯示了采取四種不同的策略拷貝一個8M文件所花費的時間。它充分說明,有效地改進緩沖機制可以大大提高程序的I/O性能。
4 結束語
一般情況下,我們總可以為具體的應用程序找到改善I/O性能的方法,這需要具體分析該應用程序的目的和操作特性。例如,考慮FTP和Http服務器,這些服務器的主要工作就是將文件從磁盤拷貝到網(wǎng)絡的Socket,一個網(wǎng)站的主頁通常比其他網(wǎng)頁訪問得更多。為了提高性能,我們可以建立快速緩沖貯存區(qū),將那些經(jīng)常被訪問的文件做緩存, 這樣這些文件就不必每次從磁盤讀寫,而是直接從內存拷貝到網(wǎng)絡。
參考文獻
[1] What is Java I/O? [EB/OL],http://www.roseindia.net/java/example/java/io/Java_io.shtml
[2] BufferedInputStream,JavaTM Platform Standard Ed. 7 [EB/OL],http://docs.oracle.com/javase/7/docs/api/java/io/BufferedInputStream.html
[3] BufferedOutputStream,JavaTM Platform Standard Ed. 7 [EB/OL],http://docs.oracle.com/javase/7/docs/api/java/io/BufferedOutputStream.html
作者簡介:
錢宇虹(1967-),女,碩士,副教授.研究領域:軟件開發(fā)與應用、軟件工程、軟件測試技術方面的教學、科研和開發(fā).