今天聽了楊曉峯老師的java 36講,感受IO這塊是特別欠缺的,因此講義摘錄以下:html
歡迎你們去訂閱:java
本文章轉自:https://time.geekbang.org/column/article/8369linux
IO 一直是軟件開發中的核心部分之一,伴隨着海量數據增加和分佈式系統的發展,IO 擴展能力愈發重要。幸運的是,Java 平臺 IO 機制通過不斷完善,雖然在某些方面仍有不足,但已經在實踐中證實了其構建高擴展性應用的能力。面試
今天我要問你的問題是,Java 提供了哪些 IO 方式? NIO 如何實現多路複用?編程
Java IO 方式有不少種,基於不一樣的 IO 抽象模型和交互方式,能夠進行簡單區分。windows
首先,傳統的 java.io 包,它基於流模型實現,提供了咱們最熟知的一些 IO 功能,好比 File 抽象、輸入輸出流等。交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動做完成以前,線程會一直阻塞在那裏,它們之間的調用是可靠的線性順序。api
java.io 包的好處是代碼比較簡單、直觀,缺點則是 IO 效率和擴展性存在侷限性,容易成爲應用性能的瓶頸。數組
不少時候,人們也把 java.net 下面提供的部分網絡 API,好比 Socket、ServerSocket、HttpURLConnection 也歸類到同步阻塞 IO 類庫,由於網絡通訊一樣是 IO 行爲。緩存
第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,能夠構建多路複用的、同步非阻塞 IO 程序,同時提供了更接近操做系統底層的高性能數據操做方式。安全
第三,在 Java 7 中,NIO 有了進一步的改進,也就是 NIO 2,引入了異步非阻塞 IO 方式,也有不少人叫它 AIO(Asynchronous IO)。異步 IO 操做基於事件和回調機制,能夠簡單理解爲,應用操做直接返回,而不會阻塞在那裏,當後臺處理完成,操做系統會通知相應線程進行後續工做。
我上面列出的回答是基於一種常見分類方式,即所謂的 BIO、NIO、NIO 2(AIO)。
在實際面試中,從傳統 IO 到 NIO、NIO 2,其中有不少地方能夠擴展開來,考察點涉及方方面面,好比:
基礎 API 功能與設計, InputStream/OutputStream 和 Reader/Writer 的關係和區別。
NIO、NIO 2 的基本組成。
給定場景,分別用不一樣模型實現,分析 BIO、NIO 等模式的設計和實現原理。
NIO 提供的高性能數據操做方式是基於什麼原理,如何使用?
或者,從開發者的角度來看,你以爲 NIO 自身實現存在哪些問題?有什麼改進的想法嗎?
IO 的內容比較多,專欄一講很難可以說清楚。IO 不只僅是多路複用,NIO 2 也不只僅是異步 IO,尤爲是數據操做部分,會在專欄下一講詳細分析。
首先,須要澄清一些基本概念:
區分同步或異步(synchronous/asynchronous)。簡單來講,同步是一種可靠的有序運行機制,當咱們進行同步操做時,後續的任務是等待當前調用返回,纔會進行下一步;而異步則相反,其餘任務不須要等待當前調用返回,一般依靠事件、回調等機制來實現任務間次序關係。
區分阻塞與非阻塞(blocking/non-blocking)。在進行阻塞操做時,當前線程會處於阻塞狀態,沒法從事其餘任務,只有當條件就緒才能繼續,好比 ServerSocket 新鏈接創建完畢,或數據讀取、寫入操做完成;而非阻塞則是無論 IO 操做是否結束,直接返回,相應操做在後臺繼續處理。
不能一律而論認爲同步或阻塞就是低效,具體還要看應用和系統特徵。
對於 java.io,咱們都很是熟悉,我這裏就從整體上進行一下總結,若是須要學習更加具體的操做,你能夠經過教程等途徑完成。整體上,我認爲你至少須要理解:
IO 不只僅是對文件的操做,網絡編程中,好比 Socket 通訊,都是典型的 IO 操做目標。
輸入流、輸出流(InputStream/OutputStream)是用於讀取或寫入字節的,例如操做圖片文件。
而 Reader/Writer 則是用於操做字符,增長了字符編解碼等功能,適用於相似從文件中讀取或者寫入文本信息。本質上計算機操做的都是字節,無論是網絡通訊仍是文件讀取,Reader/Writer 至關於構建了應用邏輯和原始數據之間的橋樑。
BufferedOutputStream 等帶緩衝區的實現,能夠避免頻繁的磁盤讀寫,進而提升 IO 處理效率。這種設計利用了緩衝區,將批量數據進行一次操做,但在使用中千萬別忘了 flush。
參考下面這張類圖,不少 IO 工具類都實現了 Closeable 接口,由於須要進行資源的釋放。好比,打開 FileInputStream,它就會獲取相應的文件描述符(FileDescriptor),須要利用 try-with-resources、 try-finally 等機制保證 FileInputStream 被明確關閉,進而相應文件描述符也會失效,不然將致使資源沒法被釋放。利用專欄前面的內容提到的 Cleaner 或 finalize 機制做爲資源釋放的最後把關,也是必要的。
下面是我整理的一個簡化版的類圖,闡述了平常開發應用較多的類型和結構關係。
1.Java NIO 概覽
首先,熟悉一下 NIO 的主要組成部分:
Buffer,高效的數據容器,除了布爾類型,全部原始數據類型都有相應的 Buffer 實現。
Channel,相似在 Linux 之類操做系統上看到的文件描述符,是 NIO 中被用來支持批量式 IO 操做的一種抽象。
File 或者 Socket,一般被認爲是比較高層次的抽象,而 Channel 則是更加操做系統底層的一種抽象,這也使得 NIO 得以充分利用現代操做系統底層機制,得到特定場景的性能優化,例如,DMA(Direct Memory Access)等。不一樣層次的抽象是相互關聯的,咱們能夠經過 Socket 獲取 Channel,反之亦然。
Selector,是 NIO 實現多路複用的基礎,它提供了一種高效的機制,能夠檢測到註冊在 Selector 上的多個 Channel 中,是否有 Channel 處於就緒狀態,進而實現了單線程對多 Channel 的高效管理。
Selector 一樣是基於底層操做系統機制,不一樣模式、不一樣版本都存在區別,例如,在最新的代碼庫裏,相關實現以下:
Linux 上依賴於 epoll(http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java)。
Windows 上 NIO2(AIO)模式則是依賴於 iocp(http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/Iocp.java)。
Chartset,提供 Unicode 字符串定義,NIO 也提供了相應的編解碼器等,例如,經過下面的方式進行字符串到 ByteBuffer 的轉換:
Charset.defaultCharset().encode("Hello world!"));
2.NIO 能解決什麼問題?
下面我經過一個典型場景,來分析爲何須要 NIO,爲何須要多路複用。設想,咱們須要實現一個服務器應用,只簡單要求可以同時服務多個客戶端請求便可。
使用 java.io 和 java.net 中的同步、阻塞式 API,能夠簡單實現。
public class DemoServer extends Thread { private ServerSocket serverSocket; public int getPort() { return serverSocket.getLocalPort(); } public void run() { try { serverSocket = new ServerSocket(0); while (true) { Socket socket = serverSocket.accept(); RequestHandler requestHandler = new RequestHandler(socket); requestHandler.start(); } } catch (IOException e) { e.printStackTrace(); } finally { if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } ; } } } public static void main(String[] args) throws IOException { DemoServer server = new DemoServer(); server.start(); try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream())); bufferedReader.lines().forEach(s -> System.out.println(s)); } } } // 簡化實現,不作讀取,直接發送字符串 class RequestHandler extends Thread { private Socket socket; RequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { try (PrintWriter out = new PrintWriter(socket.getOutputStream());) { out.println("Hello world!"); out.flush(); } catch (Exception e) { e.printStackTrace(); } } }
其實現要點是:
服務器端啓動 ServerSocket,端口 0 表示自動綁定一個空閒端口。
調用 accept 方法,阻塞等待客戶端鏈接。
利用 Socket 模擬了一個簡單的客戶端,只進行鏈接、讀取、打印。
當鏈接創建後,啓動一個單獨線程負責回覆客戶端請求。
這樣,一個簡單的 Socket 服務器就被實現出來了。
思考一下,這個解決方案在擴展性方面,可能存在什麼潛在問題呢?
你們知道 Java 語言目前的線程實現是比較重量級的,啓動或者銷燬一個線程是有明顯開銷的,每一個線程都有單獨的線程棧等結構,須要佔用很是明顯的內存,因此,每個 Client 啓動一個線程彷佛都有些浪費。
那麼,稍微修正一下這個問題,咱們引入線程池機制來避免浪費。
serverSocket = new ServerSocket(0); executor = Executors.newFixedThreadPool(8); while (true) { Socket socket = serverSocket.accept(); RequestHandler requestHandler = new RequestHandler(socket); executor.execute(requestHandler); }
這樣作彷佛好了不少,經過一個固定大小的線程池,來負責管理工做線程,避免頻繁建立、銷燬線程的開銷,這是咱們構建併發服務的典型方式。這種工做方式,能夠參考下圖來理解。
若是鏈接數並非很是多,只有最多幾百個鏈接的普通應用,這種模式每每能夠工做的很好。可是,若是鏈接數量急劇上升,這種實現方式就沒法很好地工做了,由於線程上下文切換開銷會在高併發時變得很明顯,這是同步阻塞方式的低擴展性劣勢。
NIO 引入的多路複用機制,提供了另一種思路,請參考我下面提供的新的版本。
public class NIOServer extends Thread { public void run() { try (Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 建立 Selector 和 Channel serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888)); serverSocket.configureBlocking(false); // 註冊到 Selector,並說明關注點 serverSocket.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select();// 阻塞等待就緒的 Channel,這是關鍵點之一 Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); // 生產系統中通常會額外進行就緒狀態檢查 sayHelloWorld((ServerSocketChannel) key.channel()); iter.remove(); } } } catch (IOException e) { e.printStackTrace(); } } private void sayHelloWorld(ServerSocketChannel server) throws IOException { try (SocketChannel client = server.accept();) { client.write(Charset.defaultCharset().encode("Hello world!")); } } // 省略了與前面相似的 main }
這個很是精簡的樣例掀開了 NIO 多路複用的面紗,咱們能夠分析下主要步驟和元素:
首先,經過 Selector.open() 建立一個 Selector,做爲相似調度員的角色。
而後,建立一個 ServerSocketChannel,而且向 Selector 註冊,經過指定 SelectionKey.OP_ACCEPT,告訴調度員,它關注的是新的鏈接請求。
注意,爲何咱們要明確配置非阻塞模式呢?這是由於阻塞模式下,註冊操做是不容許的,會拋出 IllegalBlockingModeException 異常。
Selector 阻塞在 select 操做,當有 Channel 發生接入請求,就會被喚醒。
在 sayHelloWorld 方法中,經過 SocketChannel 和 Buffer 進行數據操做,在本例中是發送了一段字符串。
能夠看到,在前面兩個樣例中,IO 都是同步阻塞模式,因此須要多線程以實現多任務處理。而 NIO 則是利用了單線程輪詢事件的機制,經過高效地定位就緒的 Channel,來決定作什麼,僅僅 select 階段是阻塞的,能夠有效避免大量客戶端鏈接時,頻繁線程切換帶來的問題,應用的擴展能力有了很是大的提升。下面這張圖對這種實現思路進行了形象地說明。
在 Java 7 引入的 NIO 2 中,又增添了一種額外的異步 IO 模式,利用事件和回調,處理 Accept、Read 等操做。 AIO 實現看起來是相似這樣子:
AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(sockAddr); serverSock.accept(serverSock, new CompletionHandler<>() { // 爲異步操做指定 CompletionHandler 回調函數 @Override public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) { serverSock.accept(serverSock, this); // 另一個 write(sock,CompletionHandler{}) sayHelloWorld(sockChannel, Charset.defaultCharset().encode ("Hello World!")); } // 省略其餘路徑處理方法... });
鑑於其編程要素(如 Future、CompletionHandler 等),咱們尚未進行準備工做,爲避免理解困難,我會在專欄後面相關概念補充後的再進行介紹,尤爲是 Reactor、Proactor 模式等方面將在 Netty 主題一塊兒分析,這裏我先進行概念性的對比:
基本抽象很類似,AsynchronousServerSocketChannel 對應於上面例子中的 ServerSocketChannel;AsynchronousSocketChannel 則對應 SocketChannel。
業務邏輯的關鍵在於,經過指定 CompletionHandler 回調接口,在 accept/read/write 等關鍵節點,經過事件機制調用,這是很是不一樣的一種編程思路
今天我要問你的問題是,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。
當咱們使用輸入輸出流進行讀寫時,其實是進行了屢次上下文切換,好比應用讀取數據時,先在內核態將數據從磁盤讀取到內核緩存,再切換到用戶態將數據從內核緩存讀取到用戶緩存。
寫入操做也是相似,僅僅是步驟相反,你能夠參考下面這張圖。
因此,這種方式會帶來必定的額外開銷,可能會下降 IO 效率。
而基於 NIO transferTo 的實現方式,在 Linux 和 Unix 上,則會使用到零拷貝技術,數據傳輸並不須要用戶態參與,省去了上下文切換的開銷和沒必要要的內存拷貝,進而可能提升應用拷貝性能。注意,transferTo 不只僅是能夠用在文件拷貝中,與其相似的,例如讀取磁盤文件,而後進行 Socket 發送,一樣能夠享受這種機制帶來的性能和擴展性提升。
transferTo 的傳輸過程是:
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 實現,可以在方法實現裏直接看到使用的是 transferTo,你能夠直接看源碼;而對於第一種方法的分析過程要相對麻煩一些,能夠參考下面片斷。簡單起見,我只分析同類型文件系統拷貝過程。
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 只是個抽象類,閱讀它的源碼可以理解到,原來文件系統實際邏輯存在於 JDK 內部實現裏,公共 API 實際上是經過 ServiceLoader 機制加載一系列文件系統實現,而後提供服務。
咱們能夠在 JDK 源碼裏搜索 FileSystemProvider 和 nio,能夠定位到sun/nio/fs,咱們知道 NIO 底層是和操做系統緊密相關的,因此每一個平臺都有本身的部分特有文件系統邏輯。
省略掉一些細節,最後咱們一步步定位到 UnixFileSystemProvider → UnixCopyFile.Transfer,發現這是個本地方法。
最後,明肯定位到UnixCopyFile.c,其內部實現清楚說明居然只是簡單的用戶態空間拷貝!
因此,咱們明確這個最多見的 copy 方法其實不是利用 transferTo,而是本地技術實現的用戶態拷貝。
前面談了很多機制和源碼,我簡單從實踐角度總結一下,如何提升相似拷貝等 IO 操做的性能,有一些寬泛的原則:
在程序中,使用緩存等機制,合理減小 IO 次數(在網絡通訊中,如 TCP 傳輸,window 大小也能夠看做是相似思路)。
使用 transferTo 等機制,減小上下文切換和額外 IO 操做。
儘可能減小沒必要要的轉換過程,好比編解碼;對象序列化和反序列化,好比操做文本文件或者網絡通訊,若是不是過程當中須要使用文本信息,能夠考慮不要將二進制信息轉換成字符串,直接傳輸二進制信息。
3. 掌握 NIO Buffer
我在上一講提到 Buffer 是 NIO 操做數據的基本工具,Java 爲每種原始數據類型都提供了相應的 Buffer 實現(布爾除外),因此掌握和使用 Buffer 是十分必要的,尤爲是涉及 Direct Buffer 等使用,由於其在垃圾收集等方面的特殊性,更要重點掌握。
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。
更進一步的詳細使用,我建議參考相關教程。
4.Direct Buffer 和垃圾收集
我這裏重點介紹兩種特別的 Buffer。
Direct Buffer:若是咱們看 Buffer 的方法定義,你會發現它定義了 isDirect() 方法,返回當前 Buffer 是不是 Direct 類型。這是由於 Java 提供了堆內和堆外(Direct)Buffer,咱們能夠以它的 allocate 或者 allocateDirect 方法直接建立。
MappedByteBuffer:它將文件按照指定大小直接映射爲內存區域,當程序訪問這個內存區域時將直接操做這塊兒文件數據,省去了將數據從內核空間向用戶空間傳輸的損耗。咱們可使用FileChannel.map建立 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 的生命週期管理和診斷進行了較詳細的分析。