为什么DirectBuffer是IO友好的

2023-03-15 13:41:20 浏览数 (3)

我们经常会听到,Java nio中的direct buffer对io更加友好些,但为什么呢?

本文将会从源码角度分析下其根本原因。OpenJDK版本:

➜ jdk hg id 76072a077ee1 jdk-11 28

不过在进入源码分析之前,我们还是先看下Javadoc中是如何介绍direct buffer的。

Java类java.nio.ByteBuffer

Direct vs. non-direct buffers A byte buffer is either direct or non-direct. Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations.

其实说的还是挺明白的,即当我们在做io操作时,如果用的是direct buffer,可以避免数据拷贝。

下面我们从源码角度看下,用direct buffer是如何避免数据拷贝的。

以SocketChannel为例,这个类中涉及到io操作的为write和read方法,我们先看下write。

Java类java.nio.channels.SocketChannel

public abstract int write(ByteBuffer src) throws IOException;

是个abstract方法,看下其具体实现。

Java类sun.nio.ch.SocketChannelImpl

代码语言:javascript复制
@Override
public int write(ByteBuffer buf) throws IOException {
    Objects.requireNonNull(buf);
    writeLock.lock();
    try {
        ...
        try {
            ...
            if (blocking) {
                do {
                    n = IOUtil.write(fd, buf, -1, nd);
                } while (n == IOStatus.INTERRUPTED && isOpen());
            } else {
                n = IOUtil.write(fd, buf, -1, nd);
            }
        } finally {
            ...
        }
        return IOStatus.normalize(n);
    } finally {
        writeLock.unlock();
    }
}

这个方法最终会调用IOUtil.write,看下这个方法

Java类sun.nio.ch.IOUtil

代码语言:javascript复制
static int write(FileDescriptor fd, ByteBuffer src, long position,
                 NativeDispatcher nd)
    throws IOException
{
    return write(fd, src, position, false, -1, nd);
}


static int write(FileDescriptor fd, ByteBuffer src, long position,
                 boolean directIO, int alignment, NativeDispatcher nd)
    throws IOException
{
    if (src instanceof DirectBuffer) {
        return writeFromNativeBuffer(fd, src, position, directIO, alignment, nd);
    }


    // Substitute a native buffer
    ...
    ByteBuffer bb;
    if (directIO) {
        ...
    } else {
        bb = Util.getTemporaryDirectBuffer(rem);
    }
    try {
        bb.put(src);
        ...
        int n = writeFromNativeBuffer(fd, bb, position, directIO, alignment, nd);
        if (n > 0) {
            ...
        }
        return n;
    } finally {
        Util.offerFirstTemporaryDirectBuffer(bb);
    }
}


private static int writeFromNativeBuffer(FileDescriptor fd, ByteBuffer bb,
                                         long position, boolean directIO,
                                         int alignment, NativeDispatcher nd)
    throws IOException
{
    // 最终会调用native方法做操作系统层面的write操作
}

由上面的方法可以看到,如果我们提供的是DirectBuffer,就直接write了,如果不是,还要拷贝到一个临时的DirectBuffer中做write操作。

我们再看下SocketChannel的read方法

Java类java.nio.channels.SocketChannel

public abstract int read(ByteBuffer dst) throws IOException;

同样是abstract方法,看下其具体实现

Java类sun.nio.ch.SocketChannelImpl

代码语言:javascript复制
@Override
public int read(ByteBuffer buf) throws IOException {
    Objects.requireNonNull(buf);
    readLock.lock();
    try {
        ...
        try {
            ...
            if (blocking) {
                do {
                    n = IOUtil.read(fd, buf, -1, nd);
                } while (n == IOStatus.INTERRUPTED && isOpen());
            } else {
                n = IOUtil.read(fd, buf, -1, nd);
            }
        } finally {
            ...
        }
        return IOStatus.normalize(n);
    } finally {
        readLock.unlock();
    }
}

同样是转到IOUtil类,调用其read方法,看下该方法

Java类sun.nio.ch.IOUtil

代码语言:javascript复制
static int read(FileDescriptor fd, ByteBuffer dst, long position,
                NativeDispatcher nd)
    throws IOException
{
    return read(fd, dst, position, false, -1, nd);
}


static int read(FileDescriptor fd, ByteBuffer dst, long position,
                boolean directIO, int alignment, NativeDispatcher nd)
    throws IOException
{
    ...
    if (dst instanceof DirectBuffer)
        return readIntoNativeBuffer(fd, dst, position, directIO, alignment, nd);


    // Substitute a native buffer
    ByteBuffer bb;
    ...
    if (directIO) {
    ...
    } else {
        bb = Util.getTemporaryDirectBuffer(rem);
    }
    try {
        int n = readIntoNativeBuffer(fd, bb, position, directIO, alignment,nd);
        bb.flip();
        if (n > 0)
            dst.put(bb);
        return n;
    } finally {
        Util.offerFirstTemporaryDirectBuffer(bb);
    }
}


private static int readIntoNativeBuffer(FileDescriptor fd, ByteBuffer bb,
                                        long position, boolean directIO,
                                        int alignment, NativeDispatcher nd)
    throws IOException
{
    // 最终会转到native方法调用操作系统层面的read
}

由上面的代码我们可以看到,如果我们提供是DirectBuffer,就直接read,如果不是,还要先read到一个临时的DirectBuffer中,然后再拷贝到我们的buffer中。

现在我们就明白了,在io操作中,用DirectBuffer的确是少了一次数据拷贝的过程。

但是为什么做io操作一定要用DirectBuffer呢?用HeapBuffer不行吗?

我猜应该和JVM的GC操作会移动Java对象的内存位置有些关系。改天另写一篇文章详细分析下。

0 人点赞