三種方式文件拷貝的方式
- 通過阻塞流實現
public static void copyFileByStream(File source, File dest) throws
IOException {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(dest);){
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
}
}
優點是實現簡單,而且在實際使用中,簡單的場景下可能是最快的。
- 通過 transferTo/From 實現
public static void copyFileByChannel(File source, File dest) throws IOException {
try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
FileChannel targetChannel = new FileOutputStream(dest).getChannel()){
for (long count = sourceChannel.size() ;count>0 ;) {
long transferred = sourceChannel.transferTo(
sourceChannel.position(), count, targetChannel);
sourceChannel.position(sourceChannel.position() + transferred);
count -= transferred;
}
}
}
缺點是寫起來比 stream 復雜。優點是利用直接在內核態和操作,避免了在用戶態傳輸數據的消耗。理論上是最快的拷貝方式。
- 使用 Files.copy()
優點是使用最為簡潔,而且不只是文件流的拷貝。
拷貝實現機制分析
前面提到的三種拷貝方式,實現流程都是一樣的:從一個地方,復制一段數據到內存,再從內存中把這段數據輸出到另一個地方。
唯一的細節不同處,就是數據在這個過程中需不需要經過用戶態空間。
- 當我們使用輸入輸出流時,實際上是進行了多次上下文切換,比如應用讀取數據時,現在內核態將數據從磁盤讀取到內核緩存,再切換到用戶態,將數據從內核緩存讀取到用戶緩存。流程圖如下:
顯然這種方式需要額外的開銷,會降低 IO 效率。
- 當我們使用 NIO transferTo 時,在 Linux 和 Unix 系統上,則會使用到零拷貝技術,即數據傳輸不需要經過用戶態,省去了上下文切換的開銷和不必要的內存拷貝,進而可能提高應用拷貝性能。而且,transferTo 還可以應用在 Socket 傳輸中,同樣可以享受這種機制帶來的性能和擴展性提高。
Files.copy() 源碼分析
前面提到,Java 標準庫直接給我們提供了文件拷貝的 API。他有三個重載版本:
從參數可以看出,這個方法不僅僅是只支持文件之間的操作,還可以在各種流中傳輸文件。
后兩種實現方式,從底層源碼可以看到,是直接利用阻塞 IO stream 配合一個 byte[] 數組作為緩沖區實現文件拷貝的。
private static long copy(InputStream source, OutputStream sink)
throws IOException
{
long nread = 0L;
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = source.read(buf)) > 0) {
sink.write(buf, 0, n);
nread += n;
}
return nread;
}
而第一種拷貝方式,則會先具體區分文件系統再進行處理:
public static Path copy(Path source, Path target, CopyOption... options)
throws IOException
{
FileSystemProvider provider = provider(source);
if (provider(target) == provider) {
// same provider
provider.copy(source, target, options);
} else {
// different providers
CopyMoveHelper.copyToForeignTarget(source, target, options);
}
return target;
}
追蹤同類型文件系統中的拷貝,發現內部實現和公共 API 之間不是直接關聯的,NIO 部分甚至是定義為模板而不是 Java 源文件,在 build 過程中生成源碼,下面介紹下部分 JDK 代碼機制和如何繞過隱藏障礙。
- 首先,直接跟蹤 FileSystemProvider,發現這是一個抽象類,根據注釋可以直接理解到,文件系統的實際邏輯存在于 JDK 的內部實現中,公共 API 其實是通過 ServiceLoader 機制加載一系列文件系統實現,然后提供服務。
- 在 JDK 源碼中搜索 FileSystemProvider 的具體實現,可以定位到 sun/nio/fs,這里存放著具體平臺的部分特有文件系統邏輯。
- 對于 Linux 下,省略掉一些細節,最后一步一步定位到 UnixFileSystemProvider -> UnixCopyFile.Transfer,可以看到這是一個本地方法。
- 最終明確定位到 UnixCopyFile.c,其內部實現清楚說明這只是簡單的用戶態空間拷貝。
總結下來,可以知道,這個 JDK 提供的接口,其實只是簡單的本地技術實現的用戶態拷貝。
如何提高類似拷貝 IO 的性能
- 利用緩沖區,減少 IO 次數
- 使用 transferTo/From 機制,減少上下文切換和額外的 IO 操作。
- 減少不必要的轉換過程。比如編解碼、對象序列化和反序列化,比如操作文本文件或者網絡通信,如果不是過程中需要使用到文本信息,可以考慮直接傳輸二進制信息而不用將二進制信息轉換成字符串。
Direct Buffer 和垃圾收集
這里重點介紹兩種特別的 buffer。
- DirectBuffer : 在 Buffer 的方法定義中,有一個 isDirect() 方法,返回當前方法是否是 Direct 類型。這是 Java 提供的堆外 Buffer。可以使用 allocateDirect 方法直接創建。
- MappedByteBuffer : 它將文件按照指定大小直接映射為內存區域,當程序訪問這個內存區域時,將直接操作這塊文件數據,省去了將數據從內核空間向用戶空間傳輸的損耗。我們可以使用 FileChannel.map 創建 MappedByteBuffer,它本質上也是種 Direct Buffer。
在實際使用中,Java 會盡量對 Direct Buffer 僅作本地 IO 操作,對于很大數據量的 IO 密集型操作,可能會帶來很大的性能優勢,因為:
- Direct Buffer 在生命周期內內存地址都不會再做改變,進而內核可以直接安全地對其訪問,很多 IO 操作會很高效。
- Direct Buffer 避免了堆內對象需要的額外的維護工作,提高了效率。
但是,高效背后也是高成本。Direct Buffer 在創建和銷毀過程中,都會比一般的 Buffer 增加部分開銷,所以通常應該用于長期使用、數據量較大的場景。
Direct Buffer 因為不在堆上,所以 Xmx 參數對它無效,可以使用下面的代碼設置堆外內存的大小:
-XX:MaxDirectMemorySize=512M
從參數設置和內存問題排查來看,我們在設置 JVM 需要的內存時,如果用到了堆外內存,還應考慮堆外內存的開銷。而出現了 OOM 問題時,也應該考慮是否是堆外內存不夠的可能性。
對于 Direct Buffer 的回收,可以考慮:
- 在應用程序中,顯式調用 System.gc() 來強制觸發。
- 另一種思路是,在大量使用 Direct Buffer 的部分框架中,框架會自己在程序中調用釋放方法,Netty 就是這么做的。
- 重復使用 Direct Buffer,而不是每次需要再創建,用完立刻銷毀。
跟蹤診斷 Direct Buffer 的內存占用的方法
在普通的垃圾收集日志中,并不包含 Direct Buffer 等信息,所以 Direct Buffer 的內存診斷是個比較頭疼的問題。在 java 8 以后,我們可以使用 Native Memory Tracking (NMT) 來診斷,在啟動程序時加上下面的參數可以激活 NMT,但是會導致 JVM 出現 5%~10% 的性能下降:
-XX:NativeMemoryTracking={summary|detail}
開啟 NMT 后,就可以通過下面的命令進行交互式對比:
// 打印 NMT 信息
jcmd <pid> VM.native_memory detail
// 進行 baseline,以對比分配內存變化
jcmd <pid> VM.native_memory baseline
// 進行 baseline,以對比分配內存變化
jcmd <pid> VM.native_memory detail.diff