1.JNI调用1:调用fstat命令获取文件大小:
FileDispatcherImpl.c:
#define fstat64 fstat Java_sun_nio_ch_FileDispatcherImpl_size0(JNIEnv *env, jobject this, jobject fdo) { struct stat64 fbuf; if (fstat64(fdval(env, fdo), &fbuf) < 0) return handle(env, -1, "Size failed"); return fbuf.st_size; }
fstat函数:
定义函数: int fstat(int fildes, struct stat *buf);
函数说明: fstat()用来将参数fildes 所指的文件状态, 复制到参数buf 所指的结构中(struct stat). Fstat()与stat()作用完全相同, 不同处在于传入的参数为已打开的文件描述词. 详细内容请参考stat().
返回值: 执行成功则返回0, 失败返回-1, 错误代码存于errno.
对于返回值如果为EINTR的处理如下: FileDispatcherImpl.c:
static jlong handle(JNIEnv *env, jlong rv, char *msg) { if (rv >= 0) return rv; if (errno == EINTR) return IOS_INTERRUPTED; JNU_ThrowIOExceptionWithLastError(env, msg); return IOS_THROWN; }
2.JNI调用2:调用ftruncate扩展文件:
FileDispatcherImpl.c:
#define ftruncate64 ftruncate JNIEXPORT jint JNICALL Java_sun_nio_ch_FileDispatcherImpl_truncate0(JNIEnv *env, jobject this, jobject fdo, jlong size) { return handle(env, ftruncate64(fdval(env, fdo), size), "Truncation failed"); }
ftruncate函数:
定义函数: int ftruncate(int fd, off_t length);
函数说明: ftruncate()会将参数fd 指定的文件大小改为参数length 指定的大小。参数fd 为已打开的文件描述词,而且必须是以写入模式打开的文件。如果原来的文件大小比参数length 大,则超过的部分会被删去。
返回值: 执行成功则返回0, 失败返回-1, 错误原因存于errno.
错误代码: 1、EBADF 参数fd 文件描述词为无效的或该文件已关闭。 2、EINVAL 参数fd 为一socket 并非文件, 或是该文件并非以写入模式打开。
3. JNI调用3:调用mmap构建内存映射
FileDispatcherImpl.c:
#define mmap64 mmap 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; } //调用mmap 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 */ //内存不足时,抛出OutOfMemoryError 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); }
mmap函数:
定义函数: void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize);
函数说明: mmap()用来将某个文件内容映射到内存中,对该内存区域的存取即是直接对该文件内容的读写。
参数说明:
参数 | 说明 |
在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。 fd open()返回的文件描述词,代表欲映射到内存的文件。 offset 文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。 返回值:若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。
错误代码:
- EBADF 参数fd 不是有效的文件描述词。
- EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED 则要有PROT_WRITE 以及该文件要能写入。
- EINVAL 参数start、length 或offset 有一个不合法。
- EAGAIN 文件被锁住,或是有太多内存被锁住。
- ENOMEM 内存不足。
3.2.1.1. Q1. 内存映射文件占用的是哪里的内存?
通过上面的源码分析,我们可以看到返回的是DirectByteBuffer,但是这个DirectByteBuffer并没有占用到JVM的-XX:MaxDirectMemorySize
的空间。我们可以从两个地方看出来:
- DirectByteBuffer的构造方法与其他的构造方法不一样,调用的是:
protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper)
,而且是通过反射,所以没走到sun对于DirectMemory使用的统计中 - 我们可以在代码中添加查看直接内存占用的代码来看是否有占用,这个参考我的系列上一篇文章JDK核心JAVA源码解析(4) - 堆外内存、零拷贝、DirectByteBuffer以及针对于NIO中的FileChannel的思考 这个mmap占用的内存是一块独立于JVM之外的,可以进程间共享的内存。
3.2.1.2. Q2. 内存映射文件的内存什么时候会被回收?
首先,munmap并没有被JDK开放。我们想主动回收是无法做到的(当然也可以通过反射调用,但是并不推荐)。这个不开放也可以理解,因为用户如果主动调用,会导致GC DirectBuffer的时候,报出内存访问异常导致JVM崩溃(如果用户调用了munmap,对应的MappedByteBuffer被GC时,会在被调用一次。这时如果内存已经被其他程序占用,会报一个内存访问异常)。
我们要想解除映射只能先把buffer置为null,然后祈祷GC赶紧起作用,实在等不及还可以用System.gc()催促一下GC赶快干活,不过后果是会引发FullGC。
所以,在写完当前文件块,需要映射下一块文件时,我们一般就把对应的MappedByteBuffer设置为null就行了,然后继续map就行了,因为在之前的源代码讲解中,我们看到,如果内存不足,会调用System.gc()