Java IO流程全解析(图文+源码):包括传统IO(Stream/Channel)/直接内存DirectBuffer/零拷贝之MMAP

Java IO流程

传统IO

在这里插入图片描述

读写流程

  • 写(棕色线条):

    用户向Java heap的Buffer对象写数据并调用相关api

    → cpu将数据拷贝到堆外内存的DirectBuffer

    → cpu调用JNI的pwrite0/write0方法向内核空间写(用户态切换到内核态)

    → DMA控制器将内核缓冲区的数据拷贝到硬盘/显卡(切换回用户态)

  • 读(绿色线条):

    用户调用read相关api

    → cpu调用到底层JNI的pread0/read方法(用户态切换到内核态)

    → DMA将数据从硬盘/显卡拷贝到内核缓冲区,并读到DirectBuffer(内核态切换到用户态)

    → 读进Java heap的Buffer对象,返回数据

源码验证

FileChannel.write(ByteBuffer)的关键语句为例:

👉首先进入实例方法,FileChannelImpl.write

public int write(ByteBuffer src) throws IOException {
    // ......(省略了其他部分)
            do {
                // 🔥关键语句
                n = IOUtil.write(fd, src, -1, direct, alignment, nd);
            } while ((n == IOStatus.INTERRUPTED) && isOpen());
            // ......
}

👉进入IOUtil.write

static int write(FileDescriptor fd, ByteBuffer src, long position,
                 boolean directIO, int alignment, NativeDispatcher nd)
    throws IOException
{
    // 如果是DirctBuffer,直接调用writeFromNativeBuffer并返回
    if (src instanceof DirectBuffer) {
        return writeFromNativeBuffer(fd, src, position, directIO, alignment, nd);
    }
    // ......
    ByteBuffer bb;
    if (directIO) {
        Util.checkRemainingBufferSizeAligned(rem, alignment);
        // 🔥关键语句
        bb = Util.getTemporaryAlignedDirectBuffer(rem, alignment);
    } else {
        // 🔥关键语句
        bb = Util.getTemporaryDirectBuffer(rem);
    }
    // ......
}

👉进入Util.getTemporaryDirectBufferUtil.getTemporaryAlignedDirectBuffer

public static ByteBuffer getTemporaryDirectBuffer(int size) {
    if (isBufferTooLarge(size)) {
        return ByteBuffer.allocateDirect(size);
    }

    BufferCache cache = bufferCache.get();
    ByteBuffer buf = cache.get(size);
    if (buf != null) {
        return buf;
    } else {
// ......
        return ByteBuffer.allocateDirect(size);
    }
}

最终,都返回了一个ByteBuffer.allocateDirect(size);,也就是一个DirectBuffer的实例对象

👉重新进入IOUtil.write

我们可以知道,bb就是一个DirectBuffer的实例对象

static int write(FileDescriptor fd, ByteBuffer src, long position,
                 boolean directIO, int alignment, NativeDispatcher nd)
    throws IOException
{
    // ......
    try {
        // 🔥关键语句
        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, directIO, alignment, nd);
        if (n > 0) {
            // now update src
            src.position(pos + n);
        }
        return n;
    } finally {
        Util.offerFirstTemporaryDirectBuffer(bb);
    }
}

调用bb.put(src);将原ByteBuffer里的数据写到DirectBuffer bb,验证了cpu复制那一步

👉进入writeFromNativeBuffer

private static int writeFromNativeBuffer(FileDescriptor fd, ByteBuffer bb,
                                         long position, boolean directIO,
                                         int alignment, NativeDispatcher nd)
    throws IOException
{
    // ......
    if (position != -1) {
        // 🔥关键语句
        written = nd.pwrite(fd,
                            ((DirectBuffer)bb).address() + pos,
                            rem, position);
    } else {
        // 🔥关键语句
        written = nd.write(fd, ((DirectBuffer)bb).address() + pos, rem);
    }
    if (written > 0)
        bb.position(pos + written);
    return written;
}

调用了writepwrite

👉进入writepwrite

发现调用的是FileDispatcherImpl的方法:

int write(FileDescriptor fd, long address, int len) throws IOException {
    return write0(fd, address, len, fdAccess.getAppend(fd));
}

int pwrite(FileDescriptor fd, long address, int len, long position)
    throws IOException
{
    return pwrite0(fd, address, len, position);
}

点进去一看

static native int write0(FileDescriptor fd, long address, int len, boolean append)
    throws IOException;

static native int pwrite0(FileDescriptor fd, long address, int len,
                         long position) throws IOException;

是JNI调用,发起了用户态向内核态的上下文切换,验证了流程

PS:DMA

对于一个IO操作而言,都是通过CPU发出对应的指令来完成,但是相比CPU来说,IO的速度太慢了,CPU有大量的时间处于等待IO的状态。因此就产生了DMA(Direct Memory Access)直接内存访问技术,本质上来说他就是一块主板上独立的芯片,通过它来进行内存和IO设备的数据传输,从而减少CPU的等待时间

PS:很多人以为DirectBuffer是内核态的缓冲区,这是错误的,DirectBuffer是由malloc()方法分配的Java堆外空间,但仍是用户空间
本文图片为了简明易懂,将DirectBuffer画到了堆外,实际上在Java中DirectBuffer对象肯定是在堆内的,是他的address属性为堆外的某个地址,一块调用 malloc() 申请到的native memory,类似于下图:
在这里插入图片描述

🔥为什么一定要先拷贝到DirectBuffer?直接从堆中的Buffer到内核空间不可以吗?

因为HotSpot VM里的GC除了CMS之外都是要移动对象的,当一个Java里的 byte[] 对象的引用传给native代码,让native代码直接访问数组的内容,就必须要保证native代码在访问的时候这个 byte[] 对象不能被移动,即这个地址上的内容不能失效,这就与上面相悖了,内存可能因为GC整理内存而失效

有两种解决方法:

  1. 暂时禁用GC
  2. 先把 HeapByteBuffer 背后的 byte[] 的内容拷贝到一个 DirectByteBuffer 背后的native memory去,GC管不着了

于是采用了方法2,数据被拷贝到native memory之后,就将 DirectByteBuffer 背后的native memory地址传给真正做I/O的函数,保证地址不会失效了

直接内存

如果是直接使用堆外内存呢?ByteBuffer buffer = ByteBuffer.allocateDirect(x)

在这里插入图片描述

就少了一次在Java堆内和堆外之间拷贝的过程,源码中表现为:

👉进入IOUtil.write

static int write(FileDescriptor fd, ByteBuffer src, long position,
                 boolean directIO, int alignment, NativeDispatcher nd)
    throws IOException
{
    // 如果是DirctBuffer,直接调用writeFromNativeBuffer并返回
    if (src instanceof DirectBuffer) {
        return writeFromNativeBuffer(fd, src, position, directIO, alignment, nd);
    }
    // ......
}

零拷贝之—MMAP

零拷贝是什么?

零拷贝技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域(以内核的角度看待),这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽

一般在Java中,可用MMAP实现零拷贝

MMAP是什么?

是一种内存映射方式,将虚拟地址的某一段与磁盘文件的某一段进行映射,造成直接操作磁盘文件的假象

FileChannel fc = file.getChannel();
// 返回DirectByteBuffer对象,建立DirectByteBuffer与磁盘文件之间的映射
MappedByteBuffer map = fc.map(FileChannel.MapMode.READ_WRITE, 0, 5);

以读操作为例,以往的IO:

在这里插入图片描述

采用了MMAP:
在这里插入图片描述

少了一次内核copy到用户空间的过程

但实际上,还是会进入内核态的,因为一开始用户空间的虚拟内存是空的,mmap只是做了映射,没有把数据加载到内存中。在后面访问的时候,如果没有加载到内存就会产生缺页异常,陷入内核,内核会分配出对应的物理页,并把文件数据从磁盘读到物理内存中,然后把物理页与虚拟地址建立映射,这样间接映射了虚拟地址与文件,用户就可以读写操作了。流程如下:

在这里插入图片描述

  1. 建立mmap映射
  2. 用户进行读写操作,发现对应的虚拟地址页框是空的,产生缺页中断,陷入内核态
  3. os根据mmap映射关系找到磁盘上对应文件段,读到物理内存中,并在mmu(内存管理单元)下建立虚拟内存到对应物理内存的映射
  4. 读操作,直接返回结果;写操作,写物理页对应内容,然后系统调用fsync刷脏,刷回磁盘
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值