jvm 堆外内存_NIO效率高的原理之零拷贝与直接内存映射
更多內容,歡迎關注微信公眾號:全菜工程師小輝~
前言
在筆者上一篇博客,詳解了NIO,并總結NIO相比BIO的效率要高的三個原因,徹底搞懂NIO效率高的原理。
這篇博客將針對第三個原因,進行更詳細的講解。
首先澄清,零拷貝與內存直接映射并不是Java中獨有的概念,并且這兩個技術并不是等價的。
零拷貝
零拷貝是指避免在用戶態(User-space) 與內核態(Kernel-space) 之間來回拷貝數據的技術。
傳統IO
傳統IO讀取數據并通過網絡發送的流程,如下圖
傳統IO
- read()調用導致上下文從用戶態切換到內核態。內核通過sys_read()(或等價的方法)從文件讀取數據。DMA引擎執行第一次拷貝:從文件讀取數據并存儲到內核空間的緩沖區。
- 請求的數據從內核的讀緩沖區拷貝到用戶緩沖區,然后read()方法返回。read()方法返回導致上下文從內核態切換到用戶態。現在待讀取的數據已經存儲在用戶空間內的緩沖區。至此,完成了一次IO的讀取過程。
- send()調用導致上下文從用戶態切換到內核態。第三次拷貝數據從用戶空間重新拷貝到內核空間緩沖區。但是,這一次,數據被寫入一個不同的緩沖區,一個與目標套接字相關聯的緩沖區。
- send()系統調用返回導致第四次上下文切換。當DMA引擎將數據從內核緩沖區傳輸到協議引擎緩沖區時,第四次拷貝是獨立且異步的。
內存緩沖數據(上圖中的read buffer和socket buffer ),主要是為了提高性能,內核可以預讀部分數據,當所需數據小于內存緩沖區大小時,將極大的提高性能。
IO的內核切換
磁盤到內核空間屬于DMA拷貝,用戶空間與內核空間之間的數據傳輸并沒有類似DMA這種可以不需要CPU參與的傳輸方式,因此用戶空間與內核空間之間的數據傳輸是需要CPU全程參與的(如上圖所示)。
DMA拷貝即直接內存存取,原理是外部設備不通過CPU而直接與系統內存交換數據
所以也就有了使用零拷貝技術,避免不必要的CPU數據拷貝過程。
NIO的零拷貝
NIO的零拷貝由transferTo方法實現。transferTo方法將數據從FileChannel對象傳送到可寫的字節通道(如Socket Channel等)。在transferTo方法內部實現中,由native方法transferTo0來實現,它依賴底層操作系統的支持。在UNIX和Linux系統中,調用這個方法會引起sendfile()系統調用,實現了數據直接從內核的讀緩沖區傳輸到套接字緩沖區,避免了用戶態(User-space) 與內核態(Kernel-space) 之間的數據拷貝。
NIO零拷貝
使用NIO零拷貝,流程簡化為兩步:
- transferTo方法調用觸發DMA引擎將文件上下文信息拷貝到內核讀緩沖區,接著內核將數據從內核緩沖區拷貝到與套接字相關聯的緩沖區。
- DMA引擎將數據從內核套接字緩沖區傳輸到協議引擎(第三次數據拷貝)。
內核態與用戶態切換如下圖:
NIO零拷貝的內核切換
相比傳統IO,使用NIO零拷貝后改進的地方:
- 我們已經將上下文切換次數從4次減少到了2次;
- 將數據拷貝次數從4次減少到了3次(其中只有1次涉及了CPU,另外2次是DMA直接存取)。
如果底層NIC(網絡接口卡)支持gather操作,可以進一步減少內核中的數據拷貝。在Linux 2.4以及更高版本的內核中,socket緩沖區描述符已被修改用來適應這個需求。這種方式不但減少上下文切換,同時消除了需要CPU參與的重復的數據拷貝。
NIO
用戶這邊的使用方式不變,依舊通過transferTo方法,但是方法的內部實現發生了變化:
- transferTo方法調用觸發DMA引擎將文件上下文信息拷貝到內核緩沖區。
- 數據不會被拷貝到套接字緩沖區,只有數據的描述符(包括數據位置和長度)被拷貝到套接字緩沖區。DMA 引擎直接將數據從內核緩沖區拷貝到協議引擎,這樣減少了最后一次需要消耗CPU的拷貝操作。
NIO零拷貝適用于以下場景:
- 文件較大,讀寫較慢,追求速度
- JVM內存不足,不能加載太大數據
- 內存帶寬不夠,即存在其他程序或線程存在大量的IO操作,導致帶寬本來就小
NIO的零拷貝代碼示例
/** * filechannel進行文件復制(零拷貝) * * @param fromFile 源文件 * @param toFile 目標文件 */public static void fileCopyWithFileChannel(File fromFile, File toFile) { try (// 得到fileInputStream的文件通道 FileChannel fileChannelInput = new FileInputStream(fromFile).getChannel(); // 得到fileOutputStream的文件通道 FileChannel fileChannelOutput = new FileOutputStream(toFile).getChannel()) { //將fileChannelInput通道的數據,寫入到fileChannelOutput通道 fileChannelInput.transferTo(0, fileChannelInput.size(), fileChannelOutput); } catch (IOException e) { e.printStackTrace(); }}static final int BUFFER_SIZE = 1024;/** * BufferedInputStream進行文件復制(用作對比實驗) * * @param fromFile 源文件 * @param toFile 目標文件 */public static void bufferedCopy(File fromFile,File toFile) throws IOException { try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fromFile)); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(toFile))){ byte[] buf = new byte[BUFFER_SIZE]; while ((bis.read(buf)) != -1) { bos.write(buf); } }}在不需要進行數據文件操作時,可以使用NIO的零拷貝。但如果既需要IO速度,又需要進行數據操作,則需要使用NIO的直接內存映射。
直接內存映射
Linux提供的mmap系統調用, 它可以將一段用戶空間內存映射到內核空間, 當映射成功后, 用戶對這段內存區域的修改可以直接反映到內核空間;同樣地, 內核空間對這段區域的修改也直接反映用戶空間。正因為有這樣的映射關系, 就不需要在用戶態(User-space)與內核態(Kernel-space) 之間拷貝數據, 提高了數據傳輸的效率,這就是內存直接映射技術。
NIO的直接內存映射
JDK1.4加入了NIO機制和直接內存,目的是防止Java堆和Native堆之間數據復制帶來的性能損耗,此后NIO可以使用Native的方式直接在 Native堆分配內存。
背景:堆內數據在flush到遠程時,會先復制到Native 堆,然后再發送;直接移到堆外就更快了。
在JDK8,Native Memory包括元空間和Native 堆。更多有關JVM的知識,點擊查看JVM內存模型和垃圾回收機制
直接內存
直接內存的創建
在ByteBuffer有兩個子類,HeapByteBuffer和DirectByteBuffer。前者是存在于JVM堆中的,后者是存在于Native堆中的。
UML
申請堆內存
public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity);}申請直接內存
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity);}使用直接內存的原因
- 對垃圾回收停頓的改善。因為full gc時,垃圾收集器會對所有分配的堆內內存進行掃描,垃圾收集對Java應用造成的影響,跟堆的大小是成正比的。過大的堆會影響Java應用的性能。如果使用堆外內存的話,堆外內存是直接受操作系統管理。這樣做的結果就是能保持一個較小的JVM堆內存,以減少垃圾收集對應用的影響。(full gc時會觸發堆外空閑內存的回收。)
- 減少了數據從JVM拷貝到native堆的次數,在某些場景下可以提升程序I/O的性能。
- 可以突破JVM內存限制,操作更多的物理內存。
當直接內存不足時會觸發full gc,排查full gc的時候,一定要考慮。
有關JVM和GC的相關知識,請點擊查看JVM內存模型和垃圾回收機制
使用直接內存的問題
- 堆外內存難以控制,如果內存泄漏,那么很難排查(VisualVM可以通過安裝插件來監控堆外內存)。
- 堆外內存只能通過序列化和反序列化來存儲,保存對象速度比堆內存慢,不適合存儲很復雜的對象。一般簡單的對象或者扁平化的比較適合。
- 直接內存的訪問速度(讀寫方面)會快于堆內存。在申請內存空間時,堆內存速度高于直接內存。
直接內存適合申請次數少,訪問頻繁的場合。如果內存空間需要頻繁申請,則不適合直接內存。
NIO的直接內存映射
NIO中一個重要的類:MappedByteBuffer——java nio引入的文件內存映射方案,讀寫性能極高。MappedByteBuffer將文件直接映射到內存??梢杂成湔麄€文件,如果文件比較大的話可以考慮分段進行映射,只要指定文件的感興趣部分就可以。
由于MappedByteBuffer申請的是直接內存,因此不受Minor GC控制,只能在發生Full GC時才能被回收,因此Java提供了DirectByteBuffer類來改善這一情況。它是MappedByteBuffer類的子類,同時它實現了DirectBuffer接口,維護一個Cleaner對象來完成內存回收。因此它既可以通過Full GC來回收內存,也可以調用clean()方法來進行回收
NIO的直接內存映射的函數調用
FileChannel提供了map方法來把文件映射為內存對象:
MappedByteBuffer map(int mode,long position,long size);
可以把文件的從position開始的size大小的區域映射為內存對象,mode指出了 可訪問該內存映像文件的方式
- READ_ONLY,(只讀): 試圖修改得到的緩沖區將導致拋出 ReadOnlyBufferException.(MapMode.READ_ONLY)
- READ_WRITE(讀/寫): 對得到的緩沖區的更改最終將傳播到文件;該更改對映射到同一文件的其他程序不一定是可見的。 (MapMode.READ_WRITE)
- PRIVATE(專用): 對得到的緩沖區的更改不會傳播到文件,并且該更改對映射到同一文件的其他程序也不是可見的;相反,會創建緩沖區已修改部分的專用副本。 (MapMode.PRIVATE)
使用參數-XX:MaxDirectMemorySize=10M,可以指定DirectByteBuffer的大小最多是10M。
直接內存映射代碼示例
static final int BUFFER_SIZE = 1024;/** * 使用直接內存映射讀取文件 * @param file */public static void fileReadWithMmap(File file) { long begin = System.currentTimeMillis(); byte[] b = new byte[BUFFER_SIZE]; int len = (int) file.length(); MappedByteBuffer buff; try (FileChannel channel = new FileInputStream(file).getChannel()) { // 將文件所有字節映射到內存中。返回MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); for (int offset = 0; offset < len; offset += BUFFER_SIZE) { if (len - offset > BUFFER_SIZE) { buff.get(b); } else { buff.get(new byte[len - offset]); } } } catch (IOException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("time is:" + (end - begin));}/** * HeapByteBuffer讀取文件 * @param file */public static void fileReadWithByteBuffer(File file) { long begin = System.currentTimeMillis(); try(FileChannel channel = new FileInputStream(file).getChannel();) { // 申請HeapByteBuffer ByteBuffer buff = ByteBuffer.allocate(BUFFER_SIZE); while (channel.read(buff) != -1) { buff.flip(); buff.clear(); } } catch (IOException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); System.out.println("time is:" + (end - begin));}更多內容,歡迎關注微信公眾號:全菜工程師小輝~
總結
以上是生活随笔為你收集整理的jvm 堆外内存_NIO效率高的原理之零拷贝与直接内存映射的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: jpa分页查询_spring data
- 下一篇: python输入多个数字后续操作_有效地