4

一、关于java的DirectBuffer

参考知乎问答 Java NIO中,关于DirectBuffer,HeapBuffer的疑问?
DirectBuffer本身这个对象是在堆中,但是引用了一块非堆的native memory,这块内存实际上还是属于java进程的内存中,这个角度说的话,
DirectBuffer是在用户空间相关内存中。

  • DirectBuffer的好处是减少了一次从jvm heap到native memory的copy操作。下面会有部分hotspot源码,可以更直观的感受这点;

    copy究其原因:堆内存在GC,对象地址可能会移动;除了CMS标记整理, 其他垃圾收集算法都会做复制移动整理;
  • 这里还存在一个问题,如果涉及操作系统底层的操作,native memory的数据还要要copy到各种kernel buffer(内核缓冲区,比如socket缓冲、Page Cache)。

二、java网络传输文件示例

  正常情况下通过java发送本地文件的大致流程如下图所示:

image.png

三、java中针对这些copy操作的优化【零拷贝】

1. DirectBuffer「上面已经提到」

  单纯的DirectBuffer其实并不算零拷贝,直接内存和零拷贝还是两个概念。只是零拷贝的很多概念中都用到了直接内存。

  DirectBuffer只是减少了一次C堆到java堆的一次拷贝。零拷贝更多的是指操作系统底层的一些实现。

  在java本地文件读取过程中【FileInputStream】,会调用到native方法readBytes(),下面是hotspot关于readBytes()的源码,就能看到C数组拷贝到java数据的过程:

// jdk/src/share/native/java/io/FileInputStream.c
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,
        jbyteArray bytes, jint off, jint len) {
    return readBytes(env, this, bytes, off, len, fis_fd);
}
// jdk/src/share/native/java/io/io_util.c
/*
 * The maximum size of a stack-allocated buffer.
 * 栈上能分配的最大buffer大小
 */
#define BUF_SIZE 8192
jint
readBytes(JNIEnv *env, jobject this, jbyteArray bytes,
          jint off, jint len, jfieldID fid)
{
    jint nread;
    char stackBuf[BUF_SIZE]; // BUF_SIZE=8192
    char *buf = NULL;
    FD fd;
    // 传入的Java byte数组不能是null
    if (IS_NULL(bytes)) {
        JNU_ThrowNullPointerException(env, NULL);
        return -1;
    }
    // off,len参数是否越界判断
    if (outOfBounds(env, off, len, bytes)) {
        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
        return -1;
    }
    // 如果要读取的长度是0,直接返回读取长度0
    if (len == 0) {
        return 0;
    } else if (len > BUF_SIZE) {
        // 如果要读取的长度大于BUF_SIZE,则不能在栈上分配空间了,需要在堆上分配空间
        buf = malloc(len);
        if (buf == NULL) {
            // malloc分配失败,抛出OOM异常
            JNU_ThrowOutOfMemoryError(env, NULL);
            return 0;
        }
    } else {
        buf = stackBuf;
    }
    // 获取记录在FileDescriptor中的文件描述符
    fd = GET_FD(this, fid);
    if (fd == -1) {
        JNU_ThrowIOException(env, "Stream Closed");
        nread = -1;
    } else {
        // 调用IO_Read读取
        nread = IO_Read(fd, buf, len);
        if (nread > 0) {
            // 读取成功后,从buf拷贝数据到Java的byte数组中
            (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);
        } else if (nread == -1) {
            // read系统调用返回-1是读取失败
            JNU_ThrowIOExceptionWithLastError(env, "Read error");
        } else { /* EOF */
            // 操作系统read读取返回0认为是读取结束,Java中返回-1认为是读取结束
            nread = -1;
        }
    }
    // 如果使用的是堆空间(len > BUF_SIZE),需要手动释放
    if (buf != stackBuf) {
        free(buf);
    }
    return nread;
}

2. MappedByteBuffer

  • 对应Linux底层API:mmap()

  映射出一个DirectByteBuffer,使用的是native memory「用户缓冲区」,然后利用底层的mmap技术(内存映射(memory map)),将java进程中这块native memory
映射到内核缓冲区中的文件所在地址。 这里其实减少了一次read()的系统调用,即少一次用户态到内核态的切换。「两次系统调用:1、mmap 2、write」

  • 原本的一次本地文件读写操作流程是:【 disk -> kernel buffer -> user buffer[native memory如果是直接内存就没有copy到heap的操作,native memory其实可以理解成c语言的heap,因此所有的native方法的调用都会涉及native memory] -> jvm heap,写入操作就反过来】
  • 使用mmap后的读写流程:【disk -> kernel buffer -> disk】,如果是将磁盘文件发送到网络,流程是这样的:【disk -> kernel buffer -> socket buffer -> network interface

image.png

3. NIO Channel transferTo & transferFrom

  • 对应Linux底层API:sendfile()

  利用底层sendfile()技术,即发起一次系统调用,在内核态完成所有的数据传递。
发送磁盘文件到网络:disk -> kernel buffer -> socket buffer -> network interface

Linux2.1 sendfile():

image.png

4. NIO Channel Pipe

  • 对应Linux底层API:splice()

Linux2.6.17 splice()

image.png


  其他参考文章:Linux I/O 原理和 Zero-copy 技术全面揭秘


开翻挖掘机
225 声望26 粉丝

不忘初心❤️,且行且思考