第12講 | Java有幾種文件拷貝方式?哪一種最高效?

典型回答

Java 有多種比較典型的文件拷貝實(shí)現(xiàn)方式,例如
利用 java.io 類庫(kù),直接為源文件構(gòu)建一個(gè) FileInputStream 讀取,然后再為目標(biāo)文件構(gòu)建一個(gè) FileOutputStream,完成寫(xiě)入工作。

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);
        }
    }
 }

或者,利用 java.nio 類庫(kù)提供的 transferTo 或 transferFrom 方法實(shí)現(xiàn)。

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;
        }
    }
 }

當(dāng)然,Java 標(biāo)準(zhǔn)類庫(kù)本身已經(jīng)提供了幾種 Files.copy的實(shí)現(xiàn)。

對(duì)于 Copy 的效率,這個(gè)其實(shí)與操作系統(tǒng)和配置等情況相關(guān),,總體上來(lái)說(shuō),NIO transferTo/From 的方式可能更快,因?yàn)樗芾矛F(xiàn)代操作系統(tǒng)底層機(jī)制,避免不必要拷貝和上下文切換。

拷貝實(shí)現(xiàn)機(jī)制分析

用戶態(tài)空間(User Space)內(nèi)核態(tài)空間(Kernel Space),這是操作系統(tǒng)層面的基本概念,操作系統(tǒng)內(nèi)核、硬件驅(qū)動(dòng)等運(yùn)行在內(nèi)核態(tài)空間,具有相對(duì)高的特權(quán);而用戶態(tài)空間,則是給普通應(yīng)用和服務(wù)使用。

當(dāng)我們使用輸入輸出流進(jìn)行讀寫(xiě)時(shí),實(shí)際上是進(jìn)行了多次上下文切換,比如應(yīng)用讀取數(shù)據(jù)時(shí),先在內(nèi)核態(tài)將數(shù)據(jù)從磁盤(pán)讀取到內(nèi)核緩存,再切換到用戶態(tài)將數(shù)據(jù)從內(nèi)核緩存讀取到用戶緩存。寫(xiě)入操作也是類似,僅僅是步驟相反。


image.png

這種方式會(huì)帶來(lái)一定的額外開(kāi)銷,可能會(huì)降低 IO 效率。

而基于 NIO transferTo 的實(shí)現(xiàn)方式,在 Linux 和 Unix 上,則會(huì)使用到零拷貝技術(shù),數(shù)據(jù)傳輸并不需要用戶態(tài)參與,省去了上下文切換的開(kāi)銷和不必要的內(nèi)存拷貝,進(jìn)而可能提高應(yīng)用拷貝性能。注意,transferTo 不僅僅是可以用在文件拷貝中,與其類似的,例如讀取磁盤(pán)文件,然后進(jìn)行 Socket 發(fā)送,同樣可以享受這種機(jī)制帶來(lái)的性能和擴(kuò)展性提高。

image.png

Java IO/NIO 源碼結(jié)構(gòu)

Java 標(biāo)準(zhǔn)庫(kù)也提供了文件拷貝方法(java.nio.file.Files.copy)

private static long copy(InputStream source, OutputStream sink)
      throws IOException

public static Path copy(Path source, Path target, CopyOption... options)
  throws IOException

public static long copy(InputStream in, Path target, CopyOption... options)
  throws IOException

public static long copy(Path source, OutputStream out) 
throws IOException

copy 不僅僅是支持文件之間操作,沒(méi)有人限定輸入輸出流一定是針對(duì)文件。

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;
}

copy 方法其實(shí)不是利用 transferTo,而是本地技術(shù)實(shí)現(xiàn)的用戶態(tài)拷貝。

如何提高類似拷貝等 IO 操作的性能,有一些寬泛的原則:

  • 使用緩存等機(jī)制,合理減少 IO 次數(shù)
  • 使用 transferTo 等機(jī)制,減少上下文切換和額外io
  • 盡量減少不必要的轉(zhuǎn)換過(guò)程,比如編解碼;對(duì)象序列化和反序列化

NIO Buffer

Buffer 是 NIO 操作數(shù)據(jù)的基本工具,Java 為每種原始數(shù)據(jù)類型都提供了相應(yīng)的 Buffer 實(shí)現(xiàn)(布爾除外)。

image.png

Buffer 有幾個(gè)基本屬性:

  • capcity,它反映這個(gè) Buffer 到底有多大,也就是數(shù)組的長(zhǎng)度
  • position,要操作的數(shù)據(jù)起始位置。
  • limit,相當(dāng)于操作的限額。在讀取或者寫(xiě)入時(shí),limit 的意義不一樣,
    -mark,記錄上一次 postion 的位置,默認(rèn)是 0,算是一個(gè)便利性的考慮,往往不是必須的。

Buffer 的基本操作:

  • 我們創(chuàng)建了一個(gè) ByteBuffer,準(zhǔn)備放入數(shù)據(jù),capcity 當(dāng)然就是緩沖區(qū)大小,而 position 是 0,limit 默認(rèn)就是 capcity 的大小。
  • 當(dāng)我們寫(xiě)入幾個(gè)字節(jié)的數(shù)據(jù)時(shí),position 就會(huì)跟著水漲船高,但是它不可能超過(guò) limit 的大小。
  • 如果我們想把前面寫(xiě)入的數(shù)據(jù)讀出來(lái),需要調(diào)用 flip 方法,將 position 設(shè)置為 0,limit 設(shè)置為以前的 position 那里。
  • 如果還想從頭再讀一遍,可以調(diào)用 rewind,讓 limit 不變,position 再次設(shè)置為 0。

Direct Buffer 和垃圾收集

Direct Buffer:如果我們看 Buffer 的方法定義,你會(huì)發(fā)現(xiàn)它定義了 isDirect() 方法,返回當(dāng)前 Buffer 是否是 Direct 類型。這是因?yàn)镴ava 提供了堆內(nèi)和堆外(Direct)Buffer,通過(guò)allocate 或者 allocateDirect 方法直接創(chuàng)建。

MappedByteBuffer:它將文件按照指定大小直接映為內(nèi)存區(qū)域,當(dāng)程序訪問(wèn)這個(gè)內(nèi)存區(qū)域時(shí)將直接操作這塊兒文件數(shù)據(jù),省去了將數(shù)據(jù)從內(nèi)核空間向用戶空間傳輸?shù)膿p耗。我們可以使用FileChannel.map創(chuàng)建 MappedByteBuffer,它本質(zhì)上也是種 Direct Buffer。

Java 會(huì)盡量對(duì) Direct Buffer 僅做本地 IO 操作,對(duì)于很多大數(shù)據(jù)量的 IO 密集操作,可...可能會(huì)帶來(lái)非常大的性能優(yōu)勢(shì),因?yàn)椋?/p>

  • Direct Buffer 生命周期內(nèi)內(nèi)存地址都不會(huì)再發(fā)生更改,進(jìn)而內(nèi)核可以安全地對(duì)其進(jìn)行訪問(wèn),很多 IO 操作會(huì)很高效。
  • 減少了堆內(nèi)對(duì)象存儲(chǔ)的可能額外維護(hù)工作,所以訪問(wèn)效率可能有所提高。
  • Direct Buffer 創(chuàng)建和銷毀過(guò)程中,都會(huì)比一般的堆內(nèi) Buffer 增加部分開(kāi)銷,所以通常都建議用于長(zhǎng)期使用,數(shù)據(jù)較大的場(chǎng)景。

DirectBuffer 不在堆上,所以 Xmx 之類參數(shù),其實(shí)并不能影響 Direct Buffer 等堆外成員所使用的內(nèi)存額度,我們可以這樣設(shè)置:

-XX:MaxDirectMemorySize=512M

從參數(shù)設(shè)置和內(nèi)存問(wèn)題排查角度來(lái)看,這意味著我們?cè)谟?jì)算 Java 可以使用的內(nèi)存大小的時(shí)候,不能只考慮堆的需要,還有 Direct Buffer 等一系列堆外因素。如果出現(xiàn)內(nèi)存不足,堆外內(nèi)存占用也是一種可能性。

大多數(shù)垃圾收集過(guò)程中,都不會(huì)主動(dòng)收集 Direct Buffer,它的回收是通過(guò)Cleaner(一個(gè)內(nèi)部實(shí)現(xiàn))和幻象引用(PhantomReference)機(jī)制,對(duì)它的銷毀往往要拖到 full GC 的時(shí)候,所以使用不當(dāng)很容易導(dǎo)致OOM。

對(duì)于 Direct Buffer 的回收的幾個(gè)建議:

  • 在應(yīng)用程序中,顯式地調(diào)用 System.gc() 來(lái)強(qiáng)制觸發(fā).
  • 在大量使用 Direct Buffer 的部分框架中,框架會(huì)自己在程序中調(diào)用釋放方法,Netty 就是這么做的。
  • 重復(fù)使用 Direct Buffer。

極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393
極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393
rticle/8393
極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393
極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393
極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393
極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393...
極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393
極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393

極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393
極客時(shí)間版權(quán)所有: https://time.geekbang.org/column/article/8393

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容