Zero-Copy、MMap

有一個很常見的場景,好比須要將文件從磁盤上原封不動地發送到網絡的另外一端。這經過代碼實現起來很簡單:對於Java而言,你可使用InputStream的某個實現類將文件一塊塊地讀取到小的緩衝區(一般咱們都將緩衝區大小設置爲8KB),而後再將緩衝區數據輸出到OutputStream中。更好的作法是你能夠建立一個PipedInputStream實例,讓它來管理緩衝區。可是,若是你的應用對性能有要求,那麼經過這種方式去讀取文件再發送在操做系統層面來看就顯得有些太耗資源了。
爲何這樣說呢?結合下圖我來解釋下緣由html

  1. JVM執行read()系統調用;
  2. 操做系統從用戶態切換到內核態,而後把數據讀到內核緩衝區;
  3. 內核將數據拷貝到應用緩衝區,並切換回用戶態,read()調用返回;
  4. JVM處理代碼邏輯,而後執行write()系統調用;
  5. 操做系統切換到內核態,將數據從應用緩衝區拷貝到socket內核緩衝區;
  6. 操做系統返回到用戶態,JVM繼續執行後面的業務邏輯。

若是你的應用不關心延時和吞吐量等性能指標,那麼以上作法是沒問題的,可是若是你的應用有這方面要求,好比靜態資源服務器,那麼這樣作將會沒法知足性能要求。上圖中有4次上下文切換以及2次沒必要要的內存拷貝。java

系統級別的Zero-Copy(零拷貝)

從上面的方式中能夠很清楚的看到,將數據從內核緩衝區拷貝到應用緩衝區,以及從應用緩衝區拷貝到socket內核緩衝區是徹底不必的,由於咱們沒有對數據做任何處理,僅僅只是將數據從一個socket倒騰到另外一個socket。零拷貝技術就能消除這兩次額外的內存拷貝。零拷貝技術的實現方式沒有一個統一的標準,它取決於不一樣的操做系統。典型地,那些UNIX LIKE系統用sendfile()來實現零拷貝功能。
使用零拷貝方式實現上面場景的圖示以下linux

你可能會說,操做系統仍是要在內核內存空間作一次拷貝呀!是的。可是從操做系統的角度來講,它已是零拷貝了,由於已經沒有數據從內核空間拷貝到用戶空間了。內核須要作一次拷貝的緣由是通常的硬件DMA方式只能存取連續的內存空間(因此纔有了緩衝區)。可是若是硬件支持scatter-n-gather特性,此次的拷貝就能夠避免。
支持scatter-n-gather特性時的圖示以下緩存

不少WEB服務器都支持零拷貝,好比Tomcat和Apache。默認狀況下Apache的這個特性是關閉的。
注意: Java的NIO經過transferTo方法提供零拷貝。服務器

MMap

上面的零拷貝方案有個問題,由於沒有涉及到用戶態,因此除了打通流管道,咱們沒法經過代碼來修改流管道里的數據。不過如今有個比零拷貝昂貴但優於傳統I/O的方案——內存映射,簡稱MMap網絡

MMap容許代碼將文件映射到內核內存,應用能夠直接訪問這個內核內存,就像訪問用戶態的內存空間同樣,這樣就不會產生內核空間到用戶空間的內存拷貝。不過這種方式仍然須要4次上下文切換以及3次數據拷貝(其中有一次是CPU參與的內核內存拷貝)。操做系統將文件的某塊數據映射到內存,受益於操做系統的虛擬內存管理,熱點數據能被提早載入到內存,全部的數據是頁對齊的,所以不須要緩衝區拷貝就能將數據倒騰到目標socket。app

雖然,MMap避免了額外的內存拷貝,可是使用了MMap不必定會比普通的方式快,這取決於不一樣的操做系統。由於這涉及到MMap的建立和銷燬所須要的性能開銷以及頁缺失時的負面影響。socket

Java中實現MMap方式的類是MappedByteBuffer,它其實也是一種DirectByteBufferDirectByteBufferMappedByteBuffer的子類),不過這兩個類並無直接的關係。咱們一般所說的直接內存並無MMap的特性。性能

DirectByteBuffer

Java NIO中有三種ByteBuffer優化

  1. HeapByteBufferByteBuffer.allocate()使用的就是這種緩衝區,叫堆緩衝區,由於它是在JVM堆內存的,支持GC和緩存優化。可是它不是頁對齊的,也就是說若是要使用JNI的方式調用native代碼時,JVM會先將它拷貝到頁對齊的緩衝空間。
  2. DirectByteBufferByteBuffer.allocateDirect()方法被調用時,JVM使用C語言的malloc()方法分配堆外內存。因爲不受JVM管理,這個內存空間是頁對齊的且不支持GC,和native代碼交互頻繁時使用這種緩衝區能提升性能。不過內存分配和銷燬的事就要靠你本身了。
  3. MappedByteBufferFileChannel.map()調用返回的就是這種緩衝區,這種緩衝區用的也是堆外內存,本質上其實就是對系統調用mmap()的封裝,以便經過代碼直接操縱映射物理內存數據。

參考資料

相關文章
相關標籤/搜索