從字面意思理解就是數據不須要來回的拷貝,大大提高了系統的性能;這個詞咱們也常常在java nio,netty,kafka,RocketMQ等框架中聽到,常常做爲其提高性能的一大亮點;下面從I/O的幾個概念開始,進而在分析零拷貝。java
緩衝區是全部I/O的基礎,I/O講的無非就是把數據移進或移出緩衝區;進程執行I/O操做,就是向操做系統發出請求,讓它要麼把緩衝區的數據排幹(寫),要麼填充緩衝區(讀);下面看一個java進程發起read請求加載數據大體的流程圖:
bash
全部現代操做系統都使用虛擬內存,使用虛擬的地址取代物理地址,這樣作的好處是:
1.一個以上的虛擬地址能夠指向同一個物理內存地址,
2.虛擬內存空間可大於實際可用的物理地址;
利用第一條特性能夠把內核空間地址和用戶空間的虛擬地址映射到同一個物理地址,這樣DMA就能夠填充對內核和用戶空間進程同時可見的緩衝區了,大體以下圖所示:網絡
使用mmap+write方式代替原來的read+write方式,mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係;這樣就能夠省掉原來內核read緩衝區copy數據到用戶緩衝區,可是仍是須要內核read緩衝區將數據copy到內核socket緩衝區,大體以下圖所示:
app
sendfile系統調用在內核版本2.1中被引入,目的是簡化經過網絡在兩個通道之間進行的數據傳輸過程。sendfile系統調用的引入,不只減小了數據複製,還減小了上下文切換的次數,大體以下圖所示:
框架
java nio提供的FileChannel提供了map()方法,該方法能夠在一個打開的文件和MappedByteBuffer之間創建一個虛擬內存映射,MappedByteBuffer繼承於ByteBuffer,相似於一個基於內存的緩衝區,只不過該對象的數據元素存儲在磁盤的一個文件中;調用get()方法會從磁盤中獲取數據,此數據反映該文件當前的內容,調用put()方法會更新磁盤上的文件,而且對文件作的修改對其餘閱讀者也是可見的;下面看一個簡單的讀取實例,而後在對MappedByteBuffer進行分析:jvm
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
File file = new File("D://db.txt");
long len = file.length();
byte[] ds = new byte[(int) len];
MappedByteBuffer mappedByteBuffer = new FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY, 0,
len);
for (int offset = 0; offset < len; offset++) {
byte b = mappedByteBuffer.get();
ds[offset] = b;
}
Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");
while (scan.hasNext()) {
System.out.print(scan.next() + " ");
}
}
}
複製代碼
主要經過FileChannel提供的map()來實現映射,map()方法以下:socket
public abstract MappedByteBuffer map(MapMode mode,
long position, long size)
throws IOException;
複製代碼
分別提供了三個參數,MapMode,Position和size;分別表示:
MapMode:映射的模式,可選項包括:READ_ONLY,READ_WRITE,PRIVATE;
Position:從哪一個位置開始映射,字節數的位置;
Size:從position開始向後多少個字節;性能
重點看一下MapMode,請兩個分別表示只讀和可讀可寫,固然請求的映射模式受到Filechannel對象的訪問權限限制,若是在一個沒有讀權限的文件上啓用READ_ONLY,將拋出NonReadableChannelException;PRIVATE模式表示寫時拷貝的映射,意味着經過put()方法所作的任何修改都會致使產生一個私有的數據拷貝而且該拷貝中的數據只有MappedByteBuffer實例能夠看到;該過程不會對底層文件作任何修改,並且一旦緩衝區被施以垃圾收集動做(garbage collected),那些修改都會丟失;大體瀏覽一下map()方法的源碼:ui
public MappedByteBuffer map(MapMode mode, long position, long size)
throws IOException
{
...省略...
int pagePosition = (int)(position % allocationGranularity);
long mapPosition = position - pagePosition;
long mapSize = size + pagePosition;
try {
// If no exception was thrown from map0, the address is valid
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError x) {
// An OutOfMemoryError may indicate that we've exhausted memory // so force gc and re-attempt map System.gc(); try { Thread.sleep(100); } catch (InterruptedException y) { Thread.currentThread().interrupt(); } try { addr = map0(imode, mapPosition, mapSize); } catch (OutOfMemoryError y) { // After a second OOME, fail throw new IOException("Map failed", y); } } // On Windows, and potentially other platforms, we need an open // file descriptor for some mapping operations. FileDescriptor mfd; try { mfd = nd.duplicateForMapping(fd); } catch (IOException ioe) { unmap0(addr, mapSize); throw ioe; } assert (IOStatus.checkAll(addr)); assert (addr % allocationGranularity == 0); int isize = (int)size; Unmapper um = new Unmapper(addr, mapSize, isize, mfd); if ((!writable) || (imode == MAP_RO)) { return Util.newMappedByteBufferR(isize, addr + pagePosition, mfd, um); } else { return Util.newMappedByteBuffer(isize, addr + pagePosition, mfd, um); } } 複製代碼
大體意思就是經過native方法獲取內存映射的地址,若是失敗,手動gc再次映射;最後經過內存映射的地址實例化出MappedByteBuffer,MappedByteBuffer自己是一個抽象類,其實這裏真正實例話出來的是DirectByteBuffer;spa
DirectByteBuffer繼承於MappedByteBuffer,從名字就能夠猜想出開闢了一段直接的內存,並不會佔用jvm的內存空間;上一節中經過Filechannel映射出的MappedByteBuffer其實際也是DirectByteBuffer,固然除了這種方式,也能夠手動開闢一段空間:
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);
複製代碼
如上開闢了100字節的直接內存空間;
常常須要從一個位置將文件傳輸到另一個位置,FileChannel提供了transferTo()方法用來提升傳輸的效率,首先看一個簡單的實例:
public class ChannelTransfer {
public static void main(String[] argv) throws Exception {
String files[]=new String[1];
files[0]="D://db.txt";
catFiles(Channels.newChannel(System.out), files);
}
private static void catFiles(WritableByteChannel target, String[] files)
throws Exception {
for (int i = 0; i < files.length; i++) {
FileInputStream fis = new FileInputStream(files[i]);
FileChannel channel = fis.getChannel();
channel.transferTo(0, channel.size(), target);
channel.close();
fis.close();
}
}
}
複製代碼
經過FileChannel的transferTo()方法將文件數據傳輸到System.out通道,接口定義以下:
public abstract long transferTo(long position, long count,
WritableByteChannel target)
throws IOException;
複製代碼
幾個參數也比較好理解,分別是開始傳輸的位置,傳輸的字節數,以及目標通道;transferTo()容許將一個通道交叉鏈接到另外一個通道,而不須要一箇中間緩衝區來傳遞數據;
注:這裏不須要中間緩衝區有兩層意思:第一層不須要用戶空間緩衝區來拷貝內核緩衝區,另一層兩個通道都有本身的內核緩衝區,兩個內核緩衝區也能夠作到無需拷貝數據;
netty提供了零拷貝的buffer,在傳輸數據時,最終處理的數據會須要對單個傳輸的報文,進行組合和拆分,Nio原生的ByteBuffer沒法作到,netty經過提供的Composite(組合)和Slice(拆分)兩種buffer來實現零拷貝;看下面一張圖會比較清晰:
public class CompositeChannelBuffer extends AbstractChannelBuffer {
private final ByteOrder order;
private ChannelBuffer[] components;
private int[] indices;
private int lastAccessedComponentId;
private final boolean gathering;
public byte getByte(int index) {
int componentId = componentId(index);
return components[componentId].getByte(index - indices[componentId]);
}
...省略...
複製代碼
components用來保存的就是全部接收到的buffer,indices記錄每一個buffer的起始位置,lastAccessedComponentId記錄上一次訪問的ComponentId;CompositeChannelBuffer並不會開闢新的內存並直接複製全部ChannelBuffer內容,而是直接保存了全部ChannelBuffer的引用,並在子ChannelBuffer裏進行讀寫,實現了零拷貝。
RocketMQ的消息採用順序寫到commitlog文件,而後利用consume queue文件做爲索引;RocketMQ採用零拷貝mmap+write的方式來回應Consumer的請求;
一樣kafka中存在大量的網絡數據持久化到磁盤和磁盤文件經過網絡發送的過程,kafka使用了sendfile零拷貝方式;
零拷貝若是簡單用java裏面對象的機率來理解的話,其實就是使用的都是對象的引用,每一個引用對象的地方對其改變就都能改變此對象,永遠只存在一份對象。
<<java_nio>>