<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                我在專欄上一講提到,NIO 不止是多路復用,NIO 2 也不只是異步 IO,今天我們來看看 Java IO 體系中,其他不可忽略的部分。 今天我要問你的問題是,Java 有幾種文件拷貝方式?哪一種最高效? ## 典型回答 Java 有多種比較典型的文件拷貝實現方式,比如: 利用 java.io 類庫,直接為源文件構建一個 FileInputStream 讀取,然后再為目標文件構建一個 FileOutputStream,完成寫入工作。 ~~~ 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 類庫提供的 transferTo 或 transferFrom 方法實現。 ~~~ 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; } } } ~~~ 當然,Java 標準類庫本身已經提供了幾種 Files.copy 的實現。 對于 Copy 的效率,這個其實與操作系統和配置等情況相關,總體上來說,NIO transferTo/From 的方式**可能更快**,因為它更能利用現代操作系統底層機制,避免不必要拷貝和上下文切換。 ## 考點分析 今天這個問題,從面試的角度來看,確實是一個面試考察的點,針對我上面的典型回答,面試官還可能會從實踐角度,或者 IO 底層實現機制等方面進一步提問。這一講的內容從面試題出發,主要還是為了讓你進一步加深對 Java IO 類庫設計和實現的了解。 從實踐角度,我前面并沒有明確說 NIO transfer 的方案一定最快,真實情況也確實未必如此。我們可以根據理論分析給出可行的推斷,保持合理的懷疑,給出驗證結論的思路,有時候面試官考察的就是如何將猜測變成可驗證的結論,思考方式遠比記住結論重要。 從技術角度展開,下面這些方面值得注意: * 不同的 copy 方式,底層機制有什么區別? * 為什么零拷貝(zero-copy)可能有性能優勢? * Buffer 分類與使用。 * Direct Buffer 對垃圾收集等方面的影響與實踐選擇。 接下來,我們一起來分析一下吧。 ## 知識擴展 1\. 拷貝實現機制分析 先來理解一下,前面實現的不同拷貝方法,本質上有什么明顯的區別。 首先,你需要理解用戶態空間(User Space)和內核態空間(Kernel Space),這是操作系統層面的基本概念,操作系統內核、硬件驅動等運行在內核態空間,具有相對高的特權;而用戶態空間,則是給普通應用和服務使用。你可以參考:[https://en.wikipedia.org/wiki/User\_space](https://en.wikipedia.org/wiki/User_space)。 當我們使用輸入輸出流進行讀寫時,實際上是進行了多次上下文切換,比如應用讀取數據時,先在內核態將數據從磁盤讀取到內核緩存,再切換到用戶態將數據從內核緩存讀取到用戶緩存。 寫入操作也是類似,僅僅是步驟相反,你可以參考下面這張圖。 ![](https://img.kancloud.cn/6d/23/6d2368424431f1b0d2b935386324b585_672x497.png) 所以,這種方式會帶來一定的額外開銷,可能會降低 IO 效率。 而基于 NIO transferTo 的實現方式,在 Linux 和 Unix 上,則會使用到零拷貝技術,數據傳輸并不需要用戶態參與,省去了上下文切換的開銷和不必要的內存拷貝,進而可能提高應用拷貝性能。注意,transferTo 不僅僅是可以用在文件拷貝中,與其類似的,例如讀取磁盤文件,然后進行 Socket 發送,同樣可以享受這種機制帶來的性能和擴展性提高。 transferTo 的傳輸過程是: ![](https://img.kancloud.cn/b0/c8/b0c8226992bb97adda5ad84fe25372ea_630x518.png) 2.Java IO/NIO 源碼結構 前面我在典型回答中提了第三種方式,即 Java 標準庫也提供了文件拷貝方法(java.nio.file.Files.copy)。如果你這樣回答,就一定要小心了,因為很少有問題的答案是僅僅調用某個方法。從面試的角度,面試官往往會追問:既然你提到了標準庫,那么它是怎么實現的呢?有的公司面試官以喜歡追問而出名,直到追問到你說不知道。 其實,這個問題的答案還真不是那么直觀,因為實際上有幾個不同的 copy 方法。 ~~~ 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 不僅僅是支持文件之間操作,沒有人限定輸入輸出流一定是針對文件的,這是兩個很實用的工具方法。 后面兩種 copy 實現,能夠在方法實現里直接看到使用的是 InputStream.transferTo(),你可以直接看源碼,其內部實現其實是 stream 在用戶態的讀寫;而對于第一種方法的分析過程要相對麻煩一些,可以參考下面片段。簡單起見,我只分析同類型文件系統拷貝過程。 ~~~ 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; } ~~~ 我把源碼分析過程簡單記錄如下,JDK 的源代碼中,內部實現和公共 API 定義也不是可以能夠簡單關聯上的,NIO 部分代碼甚至是定義為模板而不是 Java 源文件,在 build 過程自動生成源碼,下面順便介紹一下部分 JDK 代碼機制和如何繞過隱藏障礙。 * 首先,直接跟蹤,發現 FileSystemProvider 只是個抽象類,閱讀它的[源碼](http://hg.openjdk.java.net/jdk/jdk/file/f84ae8aa5d88/src/java.base/share/classes/java/nio/file/spi/FileSystemProvider.java)能夠理解到,原來文件系統實際邏輯存在于 JDK 內部實現里,公共 API 其實是通過 ServiceLoader 機制加載一系列文件系統實現,然后提供服務。 * 我們可以在 JDK 源碼里搜索 FileSystemProvider 和 nio,可以定位到[sun/nio/fs](http://hg.openjdk.java.net/jdk/jdk/file/f84ae8aa5d88/src/java.base/share/classes/sun/nio/fs),我們知道 NIO 底層是和操作系統緊密相關的,所以每個平臺都有自己的部分特有文件系統邏輯。 ![](https://img.kancloud.cn/5e/0b/5e0bf3130dffa8e56f398f0856eb76f7_574x411.png) * 省略掉一些細節,最后我們一步步定位到 UnixFileSystemProvider → UnixCopyFile.Transfer,發現這是個本地方法。 * 最后,明確定位到[UnixCopyFile.c](http://hg.openjdk.java.net/jdk/jdk/file/f84ae8aa5d88/src/java.base/unix/native/libnio/fs/UnixCopyFile.c),其內部實現清楚說明竟然只是簡單的用戶態空間拷貝! 所以,我們明確這個最常見的 copy 方法其實不是利用 transferTo,而是本地技術實現的用戶態拷貝。 前面談了不少機制和源碼,我簡單從實踐角度總結一下,如何提高類似拷貝等 IO 操作的性能,有一些寬泛的原則: * 在程序中,使用緩存等機制,合理減少 IO 次數(在網絡通信中,如 TCP 傳輸,window 大小也可以看作是類似思路)。 * 使用 transferTo 等機制,減少上下文切換和額外 IO 操作。 * 盡量減少不必要的轉換過程,比如編解碼;對象序列化和反序列化,比如操作文本文件或者網絡通信,如果不是過程中需要使用文本信息,可以考慮不要將二進制信息轉換成字符串,直接傳輸二進制信息。 3\. 掌握 NIO Buffer 我在上一講提到 Buffer 是 NIO 操作數據的基本工具,Java 為每種原始數據類型都提供了相應的 Buffer 實現(布爾除外),所以掌握和使用 Buffer 是十分必要的,尤其是涉及 Direct Buffer 等使用,因為其在垃圾收集等方面的特殊性,更要重點掌握。 ![](https://img.kancloud.cn/52/20/5220029e92bc21e99920937a8210276e_594x470.png) Buffer 有幾個基本屬性: * capcity,它反映這個 Buffer 到底有多大,也就是數組的長度。 * position,要操作的數據起始位置。 * limit,相當于操作的限額。在讀取或者寫入時,limit 的意義很明顯是不一樣的。比如,讀取操作時,很可能將 limit 設置到所容納數據的上限;而在寫入時,則會設置容量或容量以下的可寫限度。 * mark,記錄上一次 postion 的位置,默認是 0,算是一個便利性的考慮,往往不是必須的。 前面三個是我們日常使用最頻繁的,我簡單梳理下 Buffer 的基本操作: * 我們創建了一個 ByteBuffer,準備放入數據,capcity 當然就是緩沖區大小,而 position 就是 0,limit 默認就是 capcity 的大小。 * 當我們寫入幾個字節的數據時,position 就會跟著水漲船高,但是它不可能超過 limit 的大小。 * 如果我們想把前面寫入的數據讀出來,需要調用 flip 方法,將 position 設置為 0,limit 設置為以前的 position 那里。 * 如果還想從頭再讀一遍,可以調用 rewind,讓 limit 不變,position 再次設置為 0。 更進一步的詳細使用,我建議參考相關[教程](http://tutorials.jenkov.com/java-nio/buffers.html)。 4.Direct Buffer 和垃圾收集 我這里重點介紹兩種特別的 Buffer。 * Direct Buffer:如果我們看 Buffer 的方法定義,你會發現它定義了 isDirect() 方法,返回當前 Buffer 是否是 Direct 類型。這是因為 Java 提供了堆內和堆外(Direct)Buffer,我們可以以它的 allocate 或者 allocateDirect 方法直接創建。 * MappedByteBuffer:它將文件按照指定大小直接映射為內存區域,當程序訪問這個內存區域時將直接操作這塊兒文件數據,省去了將數據從內核空間向用戶空間傳輸的損耗。我們可以使用[FileChannel.map](https://docs.oracle.com/javase/9/docs/api/java/nio/channels/FileChannel.html#map-java.nio.channels.FileChannel.MapMode-long-long-)創建 MappedByteBuffer,它本質上也是種 Direct Buffer。 在實際使用中,Java 會盡量對 Direct Buffer 僅做本地 IO 操作,對于很多大數據量的 IO 密集操作,可能會帶來非常大的性能優勢,因為: * Direct Buffer 生命周期內內存地址都不會再發生更改,進而內核可以安全地對其進行訪問,很多 IO 操作會很高效。 * 減少了堆內對象存儲的可能額外維護工作,所以訪問效率可能有所提高。 但是請注意,Direct Buffer 創建和銷毀過程中,都會比一般的堆內 Buffer 增加部分開銷,所以通常都建議用于長期使用、數據較大的場景。 使用 Direct Buffer,我們需要清楚它對內存和 JVM 參數的影響。首先,因為它不在堆上,所以 Xmx 之類參數,其實并不能影響 Direct Buffer 等堆外成員所使用的內存額度,我們可以使用下面參數設置大小: ~~~ -XX:MaxDirectMemorySize=512M ~~~ 從參數設置和內存問題排查角度來看,這意味著我們在計算 Java 可以使用的內存大小的時候,不能只考慮堆的需要,還有 Direct Buffer 等一系列堆外因素。如果出現內存不足,堆外內存占用也是一種可能性。 另外,大多數垃圾收集過程中,都不會主動收集 Direct Buffer,它的垃圾收集過程,就是基于我在專欄前面所介紹的 Cleaner(一個內部實現)和幻象引用(PhantomReference)機制,其本身不是 public 類型,內部實現了一個 Deallocator 負責銷毀的邏輯。對它的銷毀往往要拖到 full GC 的時候,所以使用不當很容易導致 OutOfMemoryError。 對于 Direct Buffer 的回收,我有幾個建議: * 在應用程序中,顯式地調用 System.gc() 來強制觸發。 * 另外一種思路是,在大量使用 Direct Buffer 的部分框架中,框架會自己在程序中調用釋放方法,Netty 就是這么做的,有興趣可以參考其實現(PlatformDependent0)。 * 重復使用 Direct Buffer。 5\. 跟蹤和診斷 Direct Buffer 內存占用? 因為通常的垃圾收集日志等記錄,并不包含 Direct Buffer 等信息,所以 Direct Buffer 內存診斷也是個比較頭疼的事情。幸好,在 JDK 8 之后的版本,我們可以方便地使用 Native Memory Tracking(NMT)特性來進行診斷,你可以在程序啟動時加上下面參數: ~~~ -XX:NativeMemoryTracking={summary|detail} ~~~ 注意,激活 NMT 通常都會導致 JVM 出現 5%~10% 的性能下降,請謹慎考慮。 運行時,可以采用下面命令進行交互式對比: ~~~ // 打印 NMT 信息 jcmd <pid> VM.native_memory detail // 進行 baseline,以對比分配內存變化 jcmd <pid> VM.native_memory baseline // 進行 baseline,以對比分配內存變化 jcmd <pid> VM.native_memory detail.diff ~~~ 我們可以在 Internal 部分發現 Direct Buffer 內存使用的信息,這是因為其底層實際是利用 unsafe\_allocatememory。嚴格說,這不是 JVM 內部使用的內存,所以在 JDK 11 以后,其實它是歸類在 other 部分里。 JDK 9 的輸出片段如下,“+”表示的就是 diff 命令發現的分配變化: ~~~ -Internal (reserved=679KB +4KB, committed=679KB +4KB) (malloc=615KB +4KB #1571 +4) (mmap: reserved=64KB, committed=64KB) ~~~ **注意**:JVM 的堆外內存遠不止 Direct Buffer,NMT 輸出的信息當然也遠不止這些,我在專欄后面有綜合分析更加具體的內存結構的主題。 今天我分析了 Java IO/NIO 底層文件操作數據的機制,以及如何實現零拷貝的高性能操作,梳理了 Buffer 的使用和類型,并針對 Direct Buffer 的生命周期管理和診斷進行了較詳細的分析。 ## 一課一練 關于今天我們討論的題目你做到心中有數了嗎?你可以思考下,如果我們需要在 channel 讀取的過程中,將不同片段寫入到相應的 Buffer 里面(類似二進制消息分拆成消息頭、消息體等),可以采用 NIO 的什么機制做到呢?
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看