Java?NIO的零拷貝實現(xiàn)高效數(shù)據(jù)傳輸
Java NIO零拷貝
在 Java NIO 中的通道(Channel)就相當(dāng)于操作系統(tǒng)的內(nèi)核空間(kernel space)的緩沖區(qū),而緩沖區(qū)(Buffer)對應(yīng)的相當(dāng)于操作系統(tǒng)的用戶空間(user space)中的用戶緩沖區(qū)(user buffer)。
- 通道(Channel)是全雙工的(雙向傳輸),它既可能是讀緩沖區(qū)(read buffer),也可能是網(wǎng)絡(luò)緩沖區(qū)(socket buffer)。
- 緩沖區(qū)(Buffer)分為堆內(nèi)存(HeapBuffer)和堆外內(nèi)存(DirectBuffer),這是通過 malloc() 分配出來的用戶態(tài)內(nèi)存。
堆外內(nèi)存(DirectBuffer)在使用后需要應(yīng)用程序手動回收,而堆內(nèi)存(HeapBuffer)的數(shù)據(jù)在 GC 時可能會被自動回收。因此,在使用 HeapBuffer 讀寫數(shù)據(jù)時,為了避免緩沖區(qū)數(shù)據(jù)因為 GC 而丟失,NIO 會先把 HeapBuffer 內(nèi)部的數(shù)據(jù)拷貝到一個臨時的 DirectBuffer 中的本地內(nèi)存(native memory),這個拷貝涉及到 sun.misc.Unsafe.copyMemory() 的調(diào)用,背后的實現(xiàn)原理與 memcpy() 類似。 最后,將臨時生成的 DirectBuffer 內(nèi)部的數(shù)據(jù)的內(nèi)存地址傳給 I/O 調(diào)用函數(shù),這樣就避免了再去訪問 Java 對象處理 I/O 讀寫。
內(nèi)存映射文件
內(nèi)存映射文件 I/O 是一種讀和寫文件數(shù)據(jù)的方法,它可以比常規(guī)的基于流或者基于通道的 I/O 快得多。
向內(nèi)存映射文件寫入可能是危險的,只是改變數(shù)組的單個元素這樣的簡單操作,就可能會直接修改磁盤上的文件。修改數(shù)據(jù)與將數(shù)據(jù)保存到磁盤是沒有分開的。
下面代碼行將文件的前 1024 個字節(jié)映射到內(nèi)存中,map() 方法返回一個 MappedByteBuffer,它是 ByteBuffer 的子類。因此,可以像使用其他任何 ByteBuffer 一樣使用新映射的緩沖區(qū),操作系統(tǒng)會在需要時負(fù)責(zé)執(zhí)行映射。
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
MappedByteBuffer
MappedByteBuffer 是 NIO 基于**內(nèi)存映射(mmap)**這種零拷貝方式的提供的一種實現(xiàn),可以減少一次數(shù)據(jù)拷貝的過程。它繼承自 ByteBuffer。FileChannel 定義了一個 map() 方法,它可以把一個文件從 position 位置開始的 size 大小的區(qū)域映射為內(nèi)存映像文件。抽象方法 map() 方法在 FileChannel 中的定義如下:
public abstract MappedByteBuffer map(MapMode mode, long position, long size)
throws IOException;
- mode:限定內(nèi)存映射區(qū)域(MappedByteBuffer)對內(nèi)存映像文件的訪問模式,包括只可讀(READ_ONLY)、可讀可寫(READ_WRITE)和寫時拷貝(PRIVATE)三種模式。
- position:文件映射的起始地址,對應(yīng)內(nèi)存映射區(qū)域(MappedByteBuffer)的首地址。
- size:文件映射的字節(jié)長度,從 position 往后的字節(jié)數(shù),對應(yīng)內(nèi)存映射區(qū)域(MappedByteBuffer)的大小。
MappedByteBuffer 相比 ByteBuffer 新增了 fore()、load() 和 isLoad() 三個重要的方法:
- fore():對于處于 READ_WRITE 模式下的緩沖區(qū),把對緩沖區(qū)內(nèi)容的修改強(qiáng)制刷新到本地文件。
- load():將緩沖區(qū)的內(nèi)容載入物理內(nèi)存中,并返回這個緩沖區(qū)的引用。
- isLoaded():如果緩沖區(qū)的內(nèi)容在物理內(nèi)存中,則返回 true,否則返回 false。
下面給出一個利用 MappedByteBuffer 對文件進(jìn)行讀寫的使用示例:
private final static String CONTENT = "Zero copy implemented by MappedByteBuffer"; private final static String FILE_NAME = "/mmap.txt"; private final static String CHARSET = "UTF-8";
寫文件數(shù)據(jù):打開文件通道 fileChannel 并提供讀權(quán)限、寫權(quán)限和數(shù)據(jù)清空權(quán)限,通過 fileChannel 映射到一個可寫的內(nèi)存緩沖區(qū) mappedByteBuffer,將目標(biāo)數(shù)據(jù)寫入 mappedByteBuffer,通過 force() 方法把緩沖區(qū)更改的內(nèi)容強(qiáng)制寫入本地文件。
@Test
public void writeToFileByMappedByteBuffer() {
Path path = Paths.get(getClass().getResource(FILE_NAME).getPath());
byte[] bytes = CONTENT.getBytes(Charset.forName(CHARSET));
try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ,
StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
MappedByteBuffer mappedByteBuffer = fileChannel.map(READ_WRITE, 0, bytes.length);
if (mappedByteBuffer != null) {
mappedByteBuffer.put(bytes);
mappedByteBuffer.force();
}
} catch (IOException e) {
e.printStackTrace();
}
}
讀文件數(shù)據(jù):打開文件通道 fileChannel 并提供只讀權(quán)限,通過 fileChannel 映射到一個只可讀的內(nèi)存緩沖區(qū) mappedByteBuffer,讀取 mappedByteBuffer 中的字節(jié)數(shù)組即可得到文件數(shù)據(jù)。
@Test
public void readFromFileByMappedByteBuffer() {
Path path = Paths.get(getClass().getResource(FILE_NAME).getPath());
int length = CONTENT.getBytes(Charset.forName(CHARSET)).length;
try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ)) {
MappedByteBuffer mappedByteBuffer = fileChannel.map(READ_ONLY, 0, length);
if (mappedByteBuffer != null) {
byte[] bytes = new byte[length];
mappedByteBuffer.get(bytes);
String content = new String(bytes, StandardCharsets.UTF_8);
assertEquals(content, "Zero copy implemented by MappedByteBuffer");
}
} catch (IOException e) {
e.printStackTrace();
}
}
下面介紹 map() 方法的底層實現(xiàn)原理。map() 方法是 java.nio.channels.FileChannel 的抽象方法,由子類 sun.nio.ch.FileChannelImpl.java 實現(xiàn),下面是和內(nèi)存映射相關(guān)的核心代碼:
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 {
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError x) {
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException y) {
Thread.currentThread().interrupt();
}
try {
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError y) {
throw new IOException("Map failed", y);
}
}
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);
}
}
map() 方法通過本地方法 map0() 為文件分配一塊虛擬內(nèi)存,作為它的內(nèi)存映射區(qū)域,然后返回這塊內(nèi)存映射區(qū)域的起始地址。
- 文件映射需要在 Java 堆中創(chuàng)建一個 MappedByteBuffer 的實例。如果第一次文件映射導(dǎo)致 OOM,則手動觸發(fā)垃圾回收,休眠 100ms 后再嘗試映射,如果失敗則拋出異常。
- 通過 Util 的 newMappedByteBuffer (可讀可寫)方法或者 newMappedByteBufferR(僅讀) 方法方法反射創(chuàng)建一個 DirectByteBuffer 實例,其中 DirectByteBuffer 是 MappedByteBuffer 的子類。
map() 方法返回的是內(nèi)存映射區(qū)域的起始地址,通過(起始地址 + 偏移量)就可以獲取指定內(nèi)存的數(shù)據(jù)。這樣一定程度上替代了 read() 或 write() 方法,底層直接采用 sun.misc.Unsafe類的 getByte() 和 putByte() 方法對數(shù)據(jù)進(jìn)行讀寫。
private native long map0(int prot, long position, long mapSize) throws IOException;
上面是本地方法(native method)map0 的定義,它通過 JNI(Java Native Interface)調(diào)用底層 C 的實現(xiàn),這個 native 函數(shù)(Java_sun_nio_ch_FileChannelImpl_map0)的實現(xiàn)位于 JDK 源碼包下的 native/sun/nio/ch/FileChannelImpl.c這個源文件里面。
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
jint prot, jlong off, jlong len)
{
void *mapAddress = 0;
jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
jint fd = fdval(env, fdo);
int protections = 0;
int flags = 0;
if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
protections = PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_PRIVATE;
}
mapAddress = mmap64(
0, /* Let OS decide location */
len, /* Number of bytes to map */
protections, /* File permissions */
flags, /* Changes are shared */
fd, /* File descriptor of mapped file */
off); /* Offset into file */
if (mapAddress == MAP_FAILED) {
if (errno == ENOMEM) {
JNU_ThrowOutOfMemoryError(env, "Map failed");
return IOS_THROWN;
}
return handle(env, -1, "Map failed");
}
return ((jlong) (unsigned long) mapAddress);
}
可以看出 map0() 函數(shù)最終是通過 mmap64() 這個函數(shù)對 Linux 底層內(nèi)核發(fā)出內(nèi)存映射的調(diào)用, mmap64() 函數(shù)的原型如下:
#include <sys/mman.h> void *mmap64(void *addr, size_t len, int prot, int flags, int fd, off64_t offset);
下面詳細(xì)介紹一下 mmap64() 函數(shù)各個參數(shù)的含義以及參數(shù)可選值:
addr:文件在用戶進(jìn)程空間的內(nèi)存映射區(qū)中的起始地址,是一個建議的參數(shù),通??稍O(shè)置為 0 或 NULL,此時由內(nèi)核去決定真實的起始地址。當(dāng) + flags 為 MAP_FIXED 時,addr 就是一個必選的參數(shù),即需要提供一個存在的地址。len:文件需要進(jìn)行內(nèi)存映射的字節(jié)長度prot:控制用戶進(jìn)程對內(nèi)存映射區(qū)的訪問權(quán)限PROT_READ:讀權(quán)限PROT_WRITE:寫權(quán)限PROT_EXEC:執(zhí)行權(quán)限PROT_NONE:無權(quán)限
flags:控制內(nèi)存映射區(qū)的修改是否被多個進(jìn)程共享MAP_PRIVATE:對內(nèi)存映射區(qū)數(shù)據(jù)的修改不會反映到真正的文件,數(shù)據(jù)修改發(fā)生時采用寫時復(fù)制機(jī)制MAP_SHARED:對內(nèi)存映射區(qū)的修改會同步到真正的文件,修改對共享此內(nèi)存映射區(qū)的進(jìn)程是可見的MAP_FIXED:不建議使用,這種模式下 addr 參數(shù)指定的必須的提供一個存在的 addr 參數(shù)
fd:文件描述符。每次 map 操作會導(dǎo)致文件的引用計數(shù)加 1,每次 unmap 操作或者結(jié)束進(jìn)程會導(dǎo)致引用計數(shù)減 1offset:文件偏移量。進(jìn)行映射的文件位置,從文件起始地址向后的位移量
下面總結(jié)一下 MappedByteBuffer 的特點和不足之處:
- MappedByteBuffer 使用是堆外的虛擬內(nèi)存,因此分配(map)的內(nèi)存大小不受 JVM 的 -Xmx 參數(shù)限制,但是也是有大小限制的。 如果當(dāng)文件超出 Integer.MAX_VALUE 字節(jié)限制時,可以通過 position 參數(shù)重新 map 文件后面的內(nèi)容。
- MappedByteBuffer 在處理大文件時性能的確很高,但也存內(nèi)存占用、文件關(guān)閉不確定等問題,被其打開的文件只有在垃圾回收的才會被關(guān)閉,而且這個時間點是不確定的。
- MappedByteBuffer 提供了文件映射內(nèi)存的 mmap() 方法,也提供了釋放映射內(nèi)存的 unmap() 方法。然而 unmap() 是 FileChannelImpl 中的私有方法,無法直接顯示調(diào)用。因此,用戶程序需要通過 Java 反射的調(diào)用 sun.misc.Cleaner 類的 clean() 方法手動釋放映射占用的內(nèi)存區(qū)域。
public static void clean(final Object buffer) throws Exception {
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
try {
Method getCleanerMethod = buffer.getClass().getMethod("cleaner", new Class[0]);
getCleanerMethod.setAccessible(true);
Cleaner cleaner = (Cleaner) getCleanerMethod.invoke(buffer, new Object[0]);
cleaner.clean();
} catch(Exception e) {
e.printStackTrace();
}
});
}
DirectByteBuffer

DirectByteBuffer 的對象引用位于 Java 內(nèi)存模型的堆里面,JVM 可以對 DirectByteBuffer 的對象進(jìn)行內(nèi)存分配和回收管理,一般使用 DirectByteBuffer 的靜態(tài)方法 allocateDirect() 創(chuàng)建 DirectByteBuffer 實例并分配內(nèi)存。
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer 內(nèi)部的字節(jié)緩沖區(qū)位在于堆外的(用戶態(tài))直接內(nèi)存,它是通過 Unsafe 的本地方法 allocateMemory() 進(jìn)行內(nèi)存分配,底層調(diào)用的是操作系統(tǒng)的 malloc() 函數(shù),因此DirectByteBuffer 使用的是操作系統(tǒng)內(nèi)存。
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
使用 DirectByteBuf 將堆外內(nèi)存映射到 jvm 內(nèi)存中來直接訪問使用
- 這塊內(nèi)存不受 jvm 垃圾回收的影響,因此內(nèi)存地址固定,有助于 IO 讀寫
- java 中的 DirectByteBuf 對象僅維護(hù)了此內(nèi)存的虛引用,內(nèi)存回收分成兩步
- DirectByteBuf 對象被垃圾回收,將虛引用加入引用隊列
- 通過專門線程訪問引用隊列,根據(jù)虛引用釋放堆外內(nèi)存
- 減少了一次數(shù)據(jù)拷貝,用戶態(tài)與內(nèi)核態(tài)的切換次數(shù)沒有減少
除此之外,初始化 DirectByteBuffer 時還會創(chuàng)建一個 Deallocator 線程,并通過 Cleaner 的 freeMemory() 方法來對直接內(nèi)存進(jìn)行回收操作,freeMemory() 底層調(diào)用的是操作系統(tǒng)的 free() 函數(shù)。
private static class Deallocator implements Runnable {
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
由于使用 DirectByteBuffer 分配的是系統(tǒng)本地的內(nèi)存,不在 JVM 的管控范圍之內(nèi),因此直接內(nèi)存的回收和堆內(nèi)存的回收不同,直接內(nèi)存如果使用不當(dāng),很容易造成 OutOfMemoryError。
說了這么多,那么 DirectByteBuffer 和零拷貝有什么關(guān)系?前面有提到在 MappedByteBuffer 進(jìn)行內(nèi)存映射時,它的 map() 方法會通過 Util.newMappedByteBuffer() 來創(chuàng)建一個緩沖區(qū)實例,初始化的代碼如下:
static MappedByteBuffer newMappedByteBuffer(int size, long addr, FileDescriptor fd,
Runnable unmapper) {
MappedByteBuffer dbb;
if (directByteBufferConstructor == null)
initDBBConstructor();
try {
dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance(
new Object[] { new Integer(size), new Long(addr), fd, unmapper });
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new InternalError(e);
}
return dbb;
}
private static void initDBBRConstructor() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
try {
Class<?> cl = Class.forName("java.nio.DirectByteBufferR");
Constructor<?> ctor = cl.getDeclaredConstructor(
new Class<?>[] { int.class, long.class, FileDescriptor.class,
Runnable.class });
ctor.setAccessible(true);
directByteBufferRConstructor = ctor;
} catch (ClassNotFoundException | NoSuchMethodException |
IllegalArgumentException | ClassCastException x) {
throw new InternalError(x);
}
return null;
}});
}
DirectByteBuffer 是 MappedByteBuffer 的具體實現(xiàn)類,也就是基于底層操作系統(tǒng)的 mmap 技術(shù)。實際上,Util.newMappedByteBuffer() 方法通過反射機(jī)制獲取 DirectByteBuffer 的構(gòu)造器,然后創(chuàng)建一個 DirectByteBuffer 的實例,對應(yīng)的是一個單獨用于內(nèi)存映射的構(gòu)造方法:
protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper) {
super(-1, 0, cap, cap, fd);
address = addr;
cleaner = Cleaner.create(this, unmapper);
att = null;
}
因此,除了允許分配操作系統(tǒng)的直接內(nèi)存以外,DirectByteBuffer 本身也具有文件內(nèi)存映射的功能,這里不做過多說明。我們需要關(guān)注的是,DirectByteBuffer 在 MappedByteBuffer 的基礎(chǔ)上提供了內(nèi)存映像文件的隨機(jī)讀取 get() 和寫入 write() 的操作。
內(nèi)存映像文件的隨機(jī)讀操作
public byte get() {
return ((unsafe.getByte(ix(nextGetIndex()))));
}
public byte get(int i) {
return ((unsafe.getByte(ix(checkIndex(i)))));
}
內(nèi)存映像文件的隨機(jī)寫操作
public ByteBuffer put(byte x) {
unsafe.putByte(ix(nextPutIndex()), ((x)));
return this;
}
public ByteBuffer put(int i, byte x) {
unsafe.putByte(ix(checkIndex(i)), ((x)));
return this;
}
內(nèi)存映像文件的隨機(jī)讀寫都是借助 ix() 方法實現(xiàn)定位的, ix() 方法通過內(nèi)存映射空間的內(nèi)存首地址(address)和給定偏移量 i 計算出指針地址,然后由 unsafe 類的 get() 和 put() 方法和對指針指向的數(shù)據(jù)進(jìn)行讀取或?qū)懭搿?/p>
private long ix(int i) {
return address + ((long)i << 0);
}
FileChannel
FileChannel 是一個用于文件讀寫、映射和操作的通道,同時它在并發(fā)環(huán)境下是線程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以創(chuàng)建并打開一個文件通道。FileChannel 定義了 transferFrom() 和 transferTo() 兩個抽象方法,它通過在通道和通道之間建立連接實現(xiàn)數(shù)據(jù)傳輸?shù)摹?/p>
transferTo():通過 FileChannel 把文件里面的源數(shù)據(jù)寫入一個 WritableByteChannel 的目的通道。
public abstract long transferTo(long position, long count, WritableByteChannel target)
throws IOException;
transferFrom():把一個源通道 ReadableByteChannel 中的數(shù)據(jù)讀取到當(dāng)前 FileChannel 的文件里面。
public abstract long transferFrom(ReadableByteChannel src, long position, long count)
throws IOException;
下面介紹 transferTo() 和 transferFrom() 方法的底層實現(xiàn)原理,這兩個方法也是 java.nio.channels.FileChannel 的抽象方法,由子類 sun.nio.ch.FileChannelImpl.java 實現(xiàn)。transferTo() 和 transferFrom() 底層都是基于sendfile的方式 實現(xiàn)數(shù)據(jù)傳輸?shù)?,其?FileChannelImpl.java 定義了 3 個常量,用于標(biāo)示當(dāng)前操作系統(tǒng)的內(nèi)核是否支持 sendfile 以及 sendfile 的相關(guān)特性。
private static volatile boolean transferSupported = true; private static volatile boolean pipeSupported = true; private static volatile boolean fileSupported = true;
transferSupported:用于標(biāo)記當(dāng)前的系統(tǒng)內(nèi)核是否支持 sendfile() 調(diào)用,默認(rèn)為 true。pipeSupported:用于標(biāo)記當(dāng)前的系統(tǒng)內(nèi)核是否支持文件描述符(fd)基于管道(pipe)的 sendfile() 調(diào)用,默認(rèn)為 true。fileSupported:用于標(biāo)記當(dāng)前的系統(tǒng)內(nèi)核是否支持文件描述符(fd)基于文件(file)的 sendfile() 調(diào)用,默認(rèn)為 true。
來分析一下其中原理:
- transferTo()方法直接將當(dāng)前通道內(nèi)容傳輸?shù)搅硪粋€通道,沒有涉及到Buffer的任何操作,NIO中的Buffer是JVM堆或者堆外內(nèi)存,但不論如何他們都是操作系統(tǒng)內(nèi)核空間的內(nèi)存。也就是說這種方式不會有內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的拷貝問題。
- transferTo()的實現(xiàn)方式就是通過系統(tǒng)調(diào)用sendfile()(當(dāng)然這是Linux中的系統(tǒng)調(diào)用),根據(jù)我們上面所寫說這個過程是效率遠(yuǎn)高于從內(nèi)核緩沖區(qū)到用戶緩沖區(qū)的讀寫的。
- 同理transferFrom()也是這種實現(xiàn)方式。
下面以 transferTo() 的源碼實現(xiàn)為例。FileChannelImpl 首先執(zhí)行 transferToDirectly() 方法,以 sendfile 的零拷貝方式嘗試數(shù)據(jù)拷貝。如果系統(tǒng)內(nèi)核不支持 sendfile,進(jìn)一步執(zhí)行 transferToTrustedChannel() 方法,以 mmap 的零拷貝方式進(jìn)行內(nèi)存映射,這種情況下目的通道必須是 FileChannelImpl 或者 SelChImpl 類型。如果以上兩步都失敗了,則執(zhí)行 transferToArbitraryChannel() 方法,基于傳統(tǒng)的 I/O 方式完成讀寫,具體步驟是初始化一個臨時的 DirectBuffer,將源通道 FileChannel 的數(shù)據(jù)讀取到 DirectBuffer,再寫入目的通道 WritableByteChannel 里面。
public long transferTo(long position, long count, WritableByteChannel target)
throws IOException {
// 計算文件的大小
long sz = size();
// 校驗起始位置
if (position > sz)
return 0;
int icount = (int)Math.min(count, Integer.MAX_VALUE);
// 校驗偏移量
if ((sz - position) < icount)
icount = (int)(sz - position);
long n;
if ((n = transferToDirectly(position, icount, target)) >= 0)
return n;
if ((n = transferToTrustedChannel(position, icount, target)) >= 0)
return n;
return transferToArbitraryChannel(position, icount, target);
}
接下來重點分析一下 transferToDirectly() 方法的實現(xiàn),也就是 transferTo() 通過 sendfile 實現(xiàn)零拷貝的精髓所在。可以看到,transferToDirectlyInternal() 方法先獲取到目的通道 WritableByteChannel 的文件描述符 targetFD,獲取同步鎖然后執(zhí)行 transferToDirectlyInternal() 方法。
private long transferToDirectly(long position, int icount, WritableByteChannel target)
throws IOException {
// 省略從target獲取targetFD的過程
if (nd.transferToDirectlyNeedsPositionLock()) {
synchronized (positionLock) {
long pos = position();
try {
return transferToDirectlyInternal(position, icount,
target, targetFD);
} finally {
position(pos);
}
}
} else {
return transferToDirectlyInternal(position, icount, target, targetFD);
}
}
最終由 transferToDirectlyInternal() 調(diào)用本地方法 transferTo0() ,嘗試以 sendfile 的方式進(jìn)行數(shù)據(jù)傳輸。如果系統(tǒng)內(nèi)核完全不支持 sendfile,比如 Windows 操作系統(tǒng),則返回 UNSUPPORTED 并把 transferSupported 標(biāo)識為 false。如果系統(tǒng)內(nèi)核不支持 sendfile 的一些特性,比如說低版本的 Linux 內(nèi)核不支持 DMA gather copy 操作,則返回 UNSUPPORTED_CASE 并把 pipeSupported 或者 fileSupported 標(biāo)識為 false。
private long transferToDirectlyInternal(long position, int icount,
WritableByteChannel target,
FileDescriptor targetFD) throws IOException {
assert !nd.transferToDirectlyNeedsPositionLock() ||
Thread.holdsLock(positionLock);
long n = -1;
int ti = -1;
try {
begin();
ti = threads.add();
if (!isOpen())
return -1;
do {
n = transferTo0(fd, position, icount, targetFD);
} while ((n == IOStatus.INTERRUPTED) && isOpen());
if (n == IOStatus.UNSUPPORTED_CASE) {
if (target instanceof SinkChannelImpl)
pipeSupported = false;
if (target instanceof FileChannelImpl)
fileSupported = false;
return IOStatus.UNSUPPORTED_CASE;
}
if (n == IOStatus.UNSUPPORTED) {
transferSupported = false;
return IOStatus.UNSUPPORTED;
}
return IOStatus.normalize(n);
} finally {
threads.remove(ti);
end (n > -1);
}
}
本地方法(native method)transferTo0() 通過 JNI(Java Native Interface)調(diào)用底層 C 的函數(shù),這個 native 函數(shù)(Java_sun_nio_ch_FileChannelImpl_transferTo0)同樣位于 JDK 源碼包下的 native/sun/nio/ch/FileChannelImpl.c 源文件里面。JNI 函數(shù)Java_sun_nio_ch_FileChannelImpl_transferTo0() 基于條件編譯對不同的系統(tǒng)進(jìn)行預(yù)編譯,下面是 JDK 基于 Linux 系統(tǒng)內(nèi)核對 transferTo() 提供的調(diào)用封裝。
#if defined(__linux__) || defined(__solaris__)
#include <sys/sendfile.h>
#elif defined(_AIX)
#include <sys/socket.h>
#elif defined(_ALLBSD_SOURCE)
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#define lseek64 lseek
#define mmap64 mmap
#endif
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_transferTo0(JNIEnv *env, jobject this,
jobject srcFDO,
jlong position, jlong count,
jobject dstFDO)
{
jint srcFD = fdval(env, srcFDO);
jint dstFD = fdval(env, dstFDO);
#if defined(__linux__)
off64_t offset = (off64_t)position;
jlong n = sendfile64(dstFD, srcFD, &offset, (size_t)count);
return n;
#elif defined(__solaris__)
result = sendfilev64(dstFD, &sfv, 1, &numBytes);
return result;
#elif defined(__APPLE__)
result = sendfile(srcFD, dstFD, position, &numBytes, NULL, 0);
return result;
#endif
}
對 Linux、Solaris 以及 Apple 系統(tǒng)而言,transferTo0() 函數(shù)底層會執(zhí)行 sendfile64 這個系統(tǒng)調(diào)用完成零拷貝操作,sendfile64() 函數(shù)的原型如下:
#include <sys/sendfile.h> ssize_t sendfile64(int out_fd, int in_fd, off_t *offset, size_t count);
下面簡單介紹一下 sendfile64() 函數(shù)各個參數(shù)的含義:
out_fd:待寫入的文件描述符in_fd:待讀取的文件描述符offset:指定 in_fd 對應(yīng)文件流的讀取位置,如果為空,則默認(rèn)從起始位置開始count:指定在文件描述符 in_fd 和 out_fd 之間傳輸?shù)淖止?jié)數(shù)
在 Linux 2.6.3 之前,out_fd 必須是一個 socket,而從 Linux 2.6.3 以后,out_fd 可以是任何文件。也就是說,sendfile64() 函數(shù)不僅可以進(jìn)行網(wǎng)絡(luò)文件傳輸,還可以對本地文件實現(xiàn)零拷貝操作。
其它的零拷貝實現(xiàn)
Netty零拷貝
Netty 中的零拷貝和上面提到的操作系統(tǒng)層面上的零拷貝不太一樣, 我們所說的 Netty 零拷貝完全是基于(Java 層面)用戶態(tài)的,它的更多的是偏向于數(shù)據(jù)操作優(yōu)化這樣的概念,具體表現(xiàn)在以下幾個方面:
Netty 通過 DefaultFileRegion 類對 java.nio.channels.FileChannel 的 tranferTo() 方法進(jìn)行包裝,在文件傳輸時可以將文件緩沖區(qū)的數(shù)據(jù)直接發(fā)送到目的通道(Channel)
ByteBuf 可以通過 wrap 操作把字節(jié)數(shù)組、ByteBuf、ByteBuffer 包裝成一個 ByteBuf 對象, 進(jìn)而避免了拷貝操作 ByteBuf 支持 slice 操作, 因此可以將 ByteBuf 分解為多個共享同一個存儲區(qū)域的 ByteBuf,避免了內(nèi)存的拷貝 Netty 提供了 CompositeByteBuf 類,它可以將多個 ByteBuf 合并為一個邏輯上的 ByteBuf,避免了各個 ByteBuf 之間的拷貝 其中第 1 條屬于操作系統(tǒng)層面的零拷貝操作,后面 3 條只能算用戶層面的數(shù)據(jù)操作優(yōu)化。
RocketMQ和Kafka對比
RocketMQ 選擇了 mmap + write 這種零拷貝方式,適用于業(yè)務(wù)級消息這種小塊文件的數(shù)據(jù)持久化和傳輸;而 Kafka 采用的是 sendfile 這種零拷貝方式,適用于系統(tǒng)日志消息這種高吞吐量的大塊文件的數(shù)據(jù)持久化和傳輸。但是值得注意的一點是,Kafka 的索引文件使用的是 mmap + write 方式,數(shù)據(jù)文件使用的是 sendfile 方式。

到此這篇關(guān)于Java NIO的零拷貝實現(xiàn)高效數(shù)據(jù)傳輸?shù)奈恼戮徒榻B到這了,更多相關(guān)Java數(shù)據(jù)傳輸內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)之并查集的實現(xiàn)
并查集是一種用來管理元素分組情況的數(shù)據(jù)結(jié)構(gòu)。并查集可以高效地進(jìn)行如下操作。本文將通過Java實現(xiàn)并查集,感興趣的小伙伴可以了解一下2022-01-01
SpringBoot生產(chǎn)環(huán)境和測試環(huán)境配置分離的教程詳解
這篇文章主要介紹了SpringBoot生產(chǎn)環(huán)境和測試環(huán)境配置分離的教程詳解,需要的朋友可以參考下2020-08-08
基于RecyclerChart的KLine繪制Volume實現(xiàn)詳解
這篇文章主要為大家介紹了基于RecyclerChart的KLine繪制Volume實現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
IntelliJ IDEA 2020.2正式發(fā)布,兩點多多總能助你提效
這篇文章主要介紹了IntelliJ IDEA 2020.2正式發(fā)布,諸多亮點總有幾款能助你提效,本文通過圖文實例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2020-07-07
SpringMVC實現(xiàn)RESTful風(fēng)格:@PathVariable注解的使用方式
這篇文章主要介紹了SpringMVC實現(xiàn)RESTful風(fēng)格:@PathVariable注解的使用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
Java使用JDBC連接postgresql數(shù)據(jù)庫示例
這篇文章主要介紹了Java使用JDBC連接postgresql數(shù)據(jù)庫,結(jié)合實例形式分析了jdbc連接postgresql數(shù)據(jù)庫及數(shù)值插入、更新、查詢等相關(guān)操作技巧,需要的朋友可以參考下2019-01-01
Mybatis(ParameterType)傳遞多個不同類型的參數(shù)方式
這篇文章主要介紹了Mybatis(ParameterType)傳遞多個不同類型的參數(shù)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-04-04

