最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java?NIO的零拷貝實現(xiàn)高效數(shù)據(jù)傳輸

 更新時間:2026年03月11日 08:44:03   作者:SevenCoding  
這篇文章主要為大家詳細(xì)介紹了如何使用Java?NIO的零拷貝實現(xiàn)高效數(shù)據(jù)傳輸,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

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ù)減 1
  • offset:文件偏移量。進(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)文章

最新評論

武强县| 唐河县| 滨海县| 金寨县| 洞口县| 佛冈县| 江安县| 雅安市| 岑巩县| 锡林郭勒盟| 山阳县| 绥芬河市| 罗源县| 丘北县| 昂仁县| 大新县| 旌德县| 乌兰浩特市| 武功县| 宜昌市| 安远县| 庄河市| 泰兴市| 区。| 佛冈县| 库伦旗| 星座| 新平| 得荣县| 叙永县| 美姑县| 沐川县| 永康市| 高清| 房产| 龙游县| 西乌珠穆沁旗| 九台市| 中阳县| 栖霞市| 甘孜县|