jvm hotspot 源码分析 RandomAccessfile vs FileChannel 写文件

标签: jvmbytejavaioobjectfile
4476人阅读 评论(3) 收藏 举报
分类:

RandomAccessFile 和 FileChannel 是常用的写文件的方式,而一般都会推荐使用FileChannelImpl ,同事做了个测试,却发现FileChannelImpl写文件的性能比RandomAccessFile差。

调用接口:

RandomAccessFile.write(byte[] bytes)

FileChannel.write(ByteBuffer src)


FileChannel 

实现类FileChannelImpl.write(ByteBuffer src), 调用了IOUtil.write 方法

static int write(FileDescriptor fd, ByteBuffer src, long position,
                     NativeDispatcher nd, Object lock)
        throws IOException
    {
        if (src instanceof DirectBuffer)
            return writeFromNativeBuffer(fd, src, position, nd, lock);

        // Substitute a native buffer
        int pos = src.position();
        int lim = src.limit();
        assert (pos <= lim);
        int rem = (pos <= lim ? lim - pos : 0);
        ByteBuffer bb = null;
        try {
            bb = Util.getTemporaryDirectBuffer(rem);
            bb.put(src);
            bb.flip();
            // Do not update src until we see how many bytes were written
            src.position(pos);

            int n = writeFromNativeBuffer(fd, bb, position, nd, lock);
            if (n > 0) {
                // now update src
                src.position(pos + n);
            }
            return n;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(bb);
        }
    }

如果ByteBuffer 是directByteBuffer,直接调用函数writeFromNativeBuffer复制

如果不是DirectByteBuffer,会从DirectByteBuffer 池中取一个DirectByteBuffer, 将ByteBuffer中的byte[]数组复制到DirectByteBuffer中,为了避免allocate 内存的开销,对同一个线程会有DirectByteBuffer池的缓存,指定不同的大小,getTemporaryDirectBuffer方法就是从池里找到一个比自己大的DirectByteBuffer。

DirectByteBuffer 和 HeapByteBuffer的区别

在java中关于DirectByteBuffer 和传统的HeapByteBuffer 的区别是,DirectByteBuffer 是一块真实的内存块,里面存放的是byte数组的内容,而HeapByteBuffer里面只是数组的引用。这样就可以理解DirectByteBuffer 只要2个重要元素就够了,一个是内存的起始地址,一个是内存块的长度。一旦要和内核或者别的语言操作的时候,DirectByteBuffer是最好的选择,没有byte[] 或者java的object的结构,只要通过访问地址和长度,就能访问到数据本身,而在sun的unsafe.java里就提供了大量的直接访问内存的函数。


方法writeFromNativeBuffer 最后通过调用FileDispatcher.c linux中调用的就是c 函数pwrite64写文件


RandomAccessFile 

在RandomAccessFile.write 中,直接调用的就是native方法,也就是io_util.c中writeBytes 方法

void
writeBytes(JNIEnv *env, jobject this, jbyteArray bytes,
	  jint off, jint len, jfieldID fid)
{
    int n, datalen;
    char stackBuf[BUF_SIZE];
    char *buf = 0;
    FD fd;

    if (IS_NULL(bytes)) {
	JNU_ThrowNullPointerException(env, 0);
	return;
    }
    datalen = (*env)->GetArrayLength(env, bytes);

    if ((off < 0) || (off > datalen) ||
        (len < 0) || ((off + len) > datalen) || ((INT_MAX - off) < len)) {
        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", 0);
	return;
    }

    if (len == 0) {
        return;
    } else if (len > BUF_SIZE) {
        buf = malloc(len);
	if (buf == 0) {
	    JNU_ThrowOutOfMemoryError(env, 0);
	    return;
	}
    } else {
        buf = stackBuf;
    }

    fd = GET_FD(this, fid);
    (*env)->GetByteArrayRegion(env, bytes, off, len, (jbyte *)buf);

    if (!(*env)->ExceptionOccurred(env)) {
        off = 0;
	while (len > 0) {
	    n = IO_Write(fd, buf+off, len);
	    if (n == JVM_IO_ERR) {
	        JNU_ThrowIOExceptionWithLastError(env, "Write error");
		break;
	    } else if (n == JVM_IO_INTR) {
	        JNU_ThrowByName(env, "java/io/InterruptedIOException", 0);
		break;
	    }
	    off += n;
	    len -= n;
	}
    }
    if (buf != stackBuf) {
        free(buf);
    }
}


通过上面的程序,我们可以看到函数会在初始话一个BUF_SIZE=8k的char数组,如果copy的内容太大,会通过调用malloc直接申请一块内存块。调用GetByteArrayRegion将byte数组的内容复制到申请的内存块或者数组中,而通过调用IO_Write(fd, buf+off, len) 将内存块调用函数write写入文件。


区别:

第一: 两个函数的参数是不一样的,一个是byte数组,一个是bytebuffer, 对filechannel来说,如果传进去是directbuffer,不涉及到内存的复制,那么性能是最好的。

第二: 复制byte数组到内存中,GetByteArrayRegion 用的是memcpy 而 filechannel AMD64用的是memmove ,对别的cpu直接使用汇编, 而memcpy的性能比memove高。


强制sync

在linux中,write 函数只能保证把内存复制到page cache中,需要调用fdatasync,fsync 来强制提交到磁盘中去,一般在db, hadoop, kv 中比较重要的事务日志文件都需要强制sync 保证数据真的在磁盘中。

也就是函数 filechannel.force 和 RandomAccessFile.getFD().sync(); 但这2个函数是性能比较差的。

如何提高sync的性能

1.  用direct_io 去open file

2.  调用 sync_file_range,去跟新部分内容,而不是sync整个文件

long sync_file_range(int fd, loff_t offset, loff_t nbytes, int flags);
 

但这2中方式在java里都没有支持,也是java以后可以提高的地方。

1
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:415812次
    • 积分:4870
    • 等级:
    • 排名:第6112名
    • 原创:97篇
    • 转载:3篇
    • 译文:0篇
    • 评论:70条
    最新评论