正文如下:
前言
從字面意思理解就是資料不需要來回的拷貝,大大提升了系統的性能;這個詞我們也經常在java nio,netty,kafka,RocketMQ等框架中聽到,經常作為其提升性能的一大亮點;下面從I/O的幾個概念開始,進而在分析零拷貝,
I/O概念
1.緩沖區
緩沖區是所有I/O的基礎,I/O講的無非就是把資料移進或移出緩沖區;行程執行I/O操作,就是向作業系統發出請求,讓它要么把緩沖區的資料排干(寫),要么填充緩沖區(讀);下面看一個java行程發起read請求加載資料大致的流程圖:

行程發起read請求之后,內核接收到read請求之后,會先檢查內核空間中是否已經存在行程所需要的資料,如果已經存在,則直接把資料copy給行程的緩沖區;如果沒有內核隨即向磁盤控制器發出命令,要求從磁盤讀取資料,磁盤控制器把資料直接寫入內核read緩沖區,這一步通過DMA完成;接下來就是內核將資料copy到行程的緩沖區;如果行程發起write請求,同樣需要把用戶緩沖區里面的資料copy到內核的socket緩沖區里面,然后再通過DMA把資料copy到網卡中,發送出去;你可能覺得這樣挺浪費空間的,每次都需要把內核空間的資料拷貝到用戶空間中,所以零拷貝的出現就是為了解決這種問題的;關于零拷貝提供了兩種方式分別是:mmap+write方式,sendfile方式;
2.虛擬記憶體
所有現代作業系統都使用虛擬記憶體,使用虛擬的地址取代物理地址,這樣做的好處是:
1.一個以上的虛擬地址可以指向同一個物理記憶體地址,
2.虛擬記憶體空間可大于實際可用的物理地址;
利用第一條特性可以把內核空間地址和用戶空間的虛擬地址映射到同一個物理地址,這樣DMA就可以填充對內核和用戶空間行程同時可見的緩沖區了,大致如下圖所示:

省去了內核與用戶空間的往來拷貝,java也利用作業系統的此特性來提升性能,下面重點看看java對零拷貝都有哪些支持,
3.mmap+write方式
使用mmap+write方式代替原來的read+write方式,mmap是一種記憶體映射檔案的方法,即將一個檔案或者其它物件映射到行程的地址空間,實作檔案磁盤地址和行程虛擬地址空間中一段虛擬地址的一一對映關系;這樣就可以省掉原來內核read緩沖區copy資料到用戶緩沖區,但是還是需要內核read緩沖區將資料copy到內核socket緩沖區,大致如下圖所示:

4.sendfile方式
sendfile系統呼叫在內核版本2.1中被引入,目的是簡化通過網路在兩個通道之間進行的資料傳輸程序,sendfile系統呼叫的引入,不僅減少了資料復制,還減少了背景關系切換的次數,大致如下圖所示:

資料傳送只發生在內核空間,所以減少了一次背景關系切換;但是還是存在一次copy,能不能把這一次copy也省略掉,Linux2.4內核中做了改進,將Kernel buffer中對應的資料描述資訊(記憶體地址,偏移量)記錄到相應的socket緩沖區當中,這樣連內核空間中的一次cpu copy也省掉了;
Java零拷貝
1.MappedByteBuffer
java nio提供的FileChannel提供了map()方法,該方法可以在一個打開的檔案和MappedByteBuffer之間建立一個虛擬記憶體映射,MappedByteBuffer繼承于ByteBuffer,類似于一個基于記憶體的緩沖區,只不過該物件的資料元素存盤在磁盤的一個檔案中;呼叫get()方法會從磁盤中獲取資料,此資料反映該檔案當前的內容,呼叫put()方法會更新磁盤上的檔案,并且對檔案做的修改對其他閱讀者也是可見的;下面看一個簡單的讀取實體,然后在對MappedByteBuffer進行分析:
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()方法如下:
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()方法的原始碼:
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;
2.DirectByteBuffer
DirectByteBuffer繼承于MappedByteBuffer,從名字就可以猜測出開辟了一段直接的記憶體,并不會占用jvm的記憶體空間;上一節中通過Filechannel映射出的MappedByteBuffer其實際也是DirectByteBuffer,當然除了這種方式,也可以手動開辟一段空間:
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);
如上開辟了100位元組的直接記憶體空間;
3.Channel-to-Channel傳輸
經常需要從一個位置將檔案傳輸到另外一個位置,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零拷貝
netty提供了零拷貝的buffer,在傳輸資料時,最終處理的資料會需要對單個傳輸的報文,進行組合和拆分,Nio原生的ByteBuffer無法做到,netty通過提供的Composite(組合)和Slice(拆分)兩種buffer來實作零拷貝;看下面一張圖會比較清晰:

TCP層HTTP報文被分成了兩個ChannelBuffer,這兩個Buffer對我們上層的邏輯(HTTP處理)是沒有意義的,但是兩個ChannelBuffer被組合起來,就成為了一個有意義的HTTP報文,這個報文對應的ChannelBuffer,才是能稱之為”Message”的東西,這里用到了一個詞”Virtual Buffer”,可以看一下netty提供的CompositeChannelBuffer原始碼:
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里面物件的概率來理解的話,其實就是使用的都是物件的參考,每個參考物件的地方對其改變就都能改變此物件,永遠只存在一份物件,
來源:https://juejin.im/post/6844903815913668615
總結了一些2020年的面試題,這份面試題的包含的模塊分為19個模塊,分別是: Java基礎、容器、多執行緒、反射、物件拷貝、JavaWeb例外、網路、設計模式、Spring/SpringMVC、SpringBoot/SpringCloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM,

獲取資料以上資料:關注公眾號:【有故事的程式員】,獲取學習資料,
記得點個關注+評論哦~
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/248417.html
標籤:其他
