一、关于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中针对这些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
】
3. NIO Channel transferTo & transferFrom
- 对应Linux底层API:sendfile()
利用底层sendfile()技术,即发起一次系统调用,在内核态完成所有的数据传递。
发送磁盘文件到网络:disk -> kernel buffer -> socket buffer -> network interface
Linux2.1 sendfile():
4. NIO Channel Pipe
- 对应Linux底层API:splice()