各种Buffer傻傻分不清,今天终于有点悟了

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

  • 【各种Buffer傻傻分不清,今天终于有点悟了】DirectBuffer的好处是减少了一次从jvm heap到native memory的copy操作。下面会有部分hotspot源码,可以更直观的感受这点;
    copy究其原因:堆内存在GC,对象地址可能会移动;除了CMS标记整理, 其他垃圾收集算法都会做复制移动整理;
  • 这里还存在一个问题,如果涉及操作系统底层的操作,native memory的数据还要要copy到各种kernel buffer(内核缓冲区,比如socket缓冲、Page Cache)。
二、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
各种Buffer傻傻分不清,今天终于有点悟了
文章图片

3. NIO Channel transferTo & transferFrom
  • 对应Linux底层API:sendfile()
? 利用底层sendfile()技术,即发起一次系统调用,在内核态完成所有的数据传递。
发送磁盘文件到网络:disk -> kernel buffer -> socket buffer -> network interface
Linux2.1 sendfile(): 各种Buffer傻傻分不清,今天终于有点悟了
文章图片

4. NIO Channel Pipe
  • 对应Linux底层API:splice()
Linux2.6.17 splice() 各种Buffer傻傻分不清,今天终于有点悟了
文章图片

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

    推荐阅读