【JAVA】普通IO数据拷贝次数的问题探讨

news2024/11/27 6:32:41

最近看到网上有些文章在讨论JAVA中普通文件IO读/写的时候经过了几次数据拷贝,如果从系统调用开始分析,以读取文件为例,数据的读取过程如下(以缓存I/O为例):

  1. 应用程序调用read函数发起系统调用,此时由用户空间切换到内核空间;
  2. 内核通过DMA从磁盘拷贝数据到内核缓冲区;
  3. 将内核缓冲区的数据拷贝到用户空间的缓冲区,回到用户空间;

整个读取过程发生了两次数据拷贝,一次是DMA将磁盘上的文件数据拷贝到内核缓冲区,一次是将内核缓冲区的数据拷贝到用户缓冲区。

在JAVA中,JVM划分了堆内存,平时创建的对象基本都在堆中,不过也可以通过NIO包下的ByteBuffer申请堆外内存DirectByteBuffer:

ByteBuffer.allocateDirect(size);

无论是普通IO或者是NIO,在进行文件读写的时候一般都会创建一个buffer作为数据的缓冲区,读写相关方法底层是通过调用native函数(JNI调用)来实现的,在进行读写时将buffer传递给JNI。
JNI一般使用C/C++代码实现,JNI底层调用C函数库时,要求buffer所在内存地址上的内容不能失效,但是JVM在进行垃圾回收的时候有可能对对象进行移动,导致地址发生变化,所以通过NIO进行文件读取的时候,从源码中可以明显看到对buffer的对象类型进行了判断,如果buffer是DirectByteBuffer类型,使用的是堆外内存,直接使用即可,反之则认为使用的是堆内内存,此时需要先申请一块堆外内存作为堆外内存buffer,然后进行系统调用,进行数据读取,读取完毕后将堆外内存buffer的内容再拷回JVM堆内内存buffer中,这里一般是没有疑问的。

比较有疑问的点是在普通IO中,读写文件传入的是字节数组byte[],一种说法是数组一般分配的是连续的内存空间,即使内存地址发生了变化,根据数组的首地址依旧可以找到整个数组的内存,所以使用普通IO进行文件读写的时候,不需要重新分配堆外内存,直接使用堆内的字节数组即可,为了探究普通IO到底有没有重新申请堆外内存,接下来我们去看下源码。

普通IO

首先来看一下使用普通IO进行文件读取的例子,创建一个文件输入流和字节数组,通过输入流读取文件到字节数组中,这里的字节数组占用的是JVM的堆内内存

   // 创建输入流
    try (InputStream is = new FileInputStream("/document/123.txt")) {
        // 创建字节数组(堆内内存)
        byte[] bytes = new byte[1024];
        int len = 0;
        // 通过read方法读取数据到bytes数组
        while ((len = is.read(bytes)) != -1){
            String content = new String(bytes, 0, len);
            System.out.print(content);
        }
        is.read(bytes);
    } catch (Exception e) {
        e.printStackTrace();
    }

由于输入流使用的FileInputStream,所以读取文件会进入到FileInputStream中的read方法,可以看到里面又调用了readBytes方法,readBytes是一个native方法,里面传入了三个参数,分别为存放数据的字节数组、读取文件的起始位置和读取数据的长度:

public class FileInputStream extends InputStream {
    /**
     * 读取数据
     */
    public int read(byte b[]) throws IOException {
        return readBytes(b, 0, b.length);
    }

    /**
     * 读取字节数据
     * @param b 数据读取后放入的字节数组
     * @param off 读取起始位置
     * @param len 读取数据的长度
     * @exception IOException If an I/O error has occurred.
     */
    private native int readBytes(byte b[], int off, int len) throws IOException;
}

接下来就需要去readBytes中看下到底有没有使用传入的堆内内存进行数据拷贝,由于readBytes是native方法,所以需要借助openjdk源码来查看具体的实现过程。

openjdk源码下载地址:http://hg.openjdk.java.net/

这里以openjdk1.8为例,看一下readBytes的实现过程。

readBytes方法在源码解压后的src\share\native\java\io\io_util.h文件中,它的处理逻辑如下:

  1. 创建一个字符数组stackBuf(堆外内存),大小为BUF_SIZE,从BUF_SIZE的定义中可以看出大小为8192字节
  2. 对读取数据长度进行判断,如果大于8192,则根据长度重新分配一块内存(堆外内存)作为数据缓冲区赋给buf变量,如果小于就使用预先分配的字符数组stackBuf赋给buf变量
  3. 调用IO_Read函数读取数据到buf变量中,IO_Read函数中进行了系统调用,通过DMA从磁盘读取数据到内核缓冲区
  4. 调用SetByteArrayRegionbuf数据拷贝到bytes数组中

readBytes的处理逻辑来看,并没有直接使用传入的字节数组(堆内内存)进行数据拷贝,而是重新分配了内存,这里分配的是堆外内存,然后进行系统调用从磁盘读取数据到内核缓冲区,再将内核缓冲区的数据拷贝到这里分配的堆外内存中,最后调用SetByteArrayRegion将堆外内存的数据拷贝到堆内内存字节数组中。

/* 最大buffer大小
 */
#define BUF_SIZE 8192

// bytes对应传入的字节数组(堆内内存),off对应起始位置,len对应读取数据的长度
jint
readBytes(JNIEnv *env, jobject this, jbyteArray bytes,
          jint off, jint len, jfieldreadBytesID fid)
{
    jint nread;
    // 创建一个字符数组,大小为BUF_SIZE,这里分配的是堆外内存
    char stackBuf[BUF_SIZE];
    // 数据缓冲区
    char *buf = NULL;
    FD fd;
    // 校验bytes是否为空
    if (IS_NULL(bytes)) {
        JNU_ThrowNullPointerException(env, NULL);
        return -1;
    }
    // 校验是否越界
    if (outOfBounds(env, off, len, bytes)) {
        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
        return -1;
    }
    if (len == 0) { // 如果读取数据长度为0直接返回
        return 0;
    } else if (len > BUF_SIZE) { // 如果读取长度大于BUF_SIZE
        buf = malloc(len); // 分配内存(堆外内存)
        if (buf == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            return 0;
        }
    } else {
        // 使用预先分配的数组
        buf = stackBuf;
    }

    fd = GET_FD(this, fid);
    if (fd == -1) {
        JNU_ThrowIOException(env, "Stream Closed");
        nread = -1;
    } else {
        // 数据读取
        nread = IO_Read(fd, buf, len);
        if (nread > 0) {
            // 将数据拷贝到堆内内存bytes中
            (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);
        } else if (nread == -1) {
            JNU_ThrowIOExceptionWithLastError(env, "Read error");
        } else { /* EOF */
            nread = -1;
        }
    }

    if (buf != stackBuf) {
        free(buf);
    }
    return nread;
}

由于操作系统不同,系统调用的方法也不同,这里以UNIX为例,看下IO_Read函数的具体实现。

IO_Read函数的定义在解压后的src\solaris\native\java\io\io_util_md.h文件中,可以看到IO_Read指向的是handleRead方法:

#define IO_Read handleRead

handleReadsrc\solaris\native\java\io\io_util_md.c中实现,可以看到里面进行了系统调用,通过read函数读取数据

ssize_t
handleRead(FD fd, void *buf, jint len)
{
    ssize_t result;
    // 进行系统调用,通过read函数读取数据
    RESTARTABLE(read(fd, buf, len), result);
    return result;
}

普通IO数据读取流程总结

  1. 发起JNI调用,创建堆外缓冲区;
  2. JNI中发起read系统调用,此时需要由用户空间切换到内核空间;
  3. 进入到内核空间,DMA读取文件数据到内核缓冲区;
  4. 将内核缓冲区的数据拷贝到用户缓冲区,切换回用户空间;
  5. 将堆外缓冲区的数据拷贝到JVM堆内缓冲区中;

普通IO文件读取过程中并没有因为使用字节数组而减少一次拷贝,读取过程中数据发生了三次拷贝,分别是从DMA读取数据到内核缓冲区、从内核缓冲区拷贝到用户空间的堆外缓冲区和从堆外缓冲区拷贝到JVM堆内缓冲区。

文件写入的逻辑与读取类似,具体可以通过源码查看。

NIO

接下来再来看下NIO读取文件的过程。
使用NIO的FileChannel读取文件的例子:

		try (FileInputStream fileInputStream = new FileInputStream("/document/123.txt")) {
			// 获取文件对应的channel
			FileChannel channel = fileInputStream.getChannel();
                       // 分配buffer
			ByteBuffer buffer = ByteBuffer.allocate(1024);
			// 将数据读取到buffer
			channel.read(buffer);
		} catch (Exception e) {
			e.printStackTrace();
		}

接下来进入到FileChannelImpl的read方法中,由于jdk中没有sun包下面的源码,IDEA只能通过反编译查看源码,有些参数会是var1、var2…这样的变量名,不便于阅读,所以还可以借助openjdk中的源码来查看实现,当然也可以从网上下载sun包的源码,放入jdk的源码包中。

FileChannelImplsrc/share/classes/sun/nio/ch/FileChannelImpl.java中,里面又是通过IOUtilread方法读取数据放入buffer中的:

public class FileChannelImpl extends FileChannel {
   public int read(ByteBuffer dst) throws IOException {
        ensureOpen();
        if (!readable)
            throw new NonReadableChannelException();
        synchronized (positionLock) {
            int n = 0;
            int ti = -1;
            try {
                begin();
                ti = threads.add();
                if (!isOpen())
                    return 0;
                do {
                    // 通过IOUtil的read方法读取数据,fd为文件描述符,dst为传入的buffer
                    n = IOUtil.read(fd, dst, -1, nd);
                } while ((n == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(n);
            } finally {
                threads.remove(ti);
                end(n > 0);
                assert IOStatus.check(n);
            }
        }
    }
}

IOUtilsrc/share/classes/sun/nio/ch/IOUtil.java中,可以看到首先对传入的buffer类型进行了判断:

  1. 如果是DirectBuffer,直接调用readIntoNativeBuffer读取数据即可;
  2. 如果不是DirectBuffer,表示占用的堆内内存,此时需要UtilgetTemporaryDirectBuffer申请一块堆外内存,然后调用readIntoNativeBuffer读取数据;
public class IOUtil {
    static int read(FileDescriptor fd, ByteBuffer dst, long position,
                    NativeDispatcher nd)
        throws IOException
    {
        if (dst.isReadOnly())
            throw new IllegalArgumentException("Read-only buffer");
        // 如果目标buffer是DirectBuffer
        if (dst instanceof DirectBuffer)
            return readIntoNativeBuffer(fd, dst, position, nd); // 直接读取数据

        // 重新分配一块native buffer,也就是堆外内存
        ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining());
        try {
            // 读取数据
            int n = readIntoNativeBuffer(fd, bb, position, nd);
            bb.flip();
            if (n > 0)
                dst.put(bb);
            return n;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(bb);
        }
    }
}

Utilsrc/share/classes/sun/nio/ch/Util.java中。
Util中,使用了ThreadLocal缓存了每个线程申请的内存buffer,在调用
getTemporaryDirectBuffer方法获取内存时,首先会根据大小从ThreadLocal中获取是否有满足条件的buffer,如果有直接返回即可,如果大小不够则重新申请,可以看到申请的是堆外内存:

public class Util {
    // Per-thread cache of temporary direct buffers
    private static ThreadLocal<BufferCache> bufferCache =
        new ThreadLocal<BufferCache>()
    {
        @Override
        protected BufferCache initialValue() {
            // 初始化,创建一个BufferCache
            return new BufferCache();
        }
    };
    
     /**
     * Returns a temporary buffer of at least the given size
     */
    public static ByteBuffer getTemporaryDirectBuffer(int size) {
        // 先从缓存中获取
        BufferCache cache = bufferCache.get();
        ByteBuffer buf = cache.get(size);
        // 如果获取不为空
        if (buf != null) {
            return buf;
        } else {
            // 如果没有合适的buffer则重新申请
            if (!cache.isEmpty()) {
                buf = cache.removeFirst();
                free(buf);
            }
            // 申请堆外内存
            return ByteBuffer.allocateDirect(size);
        }
    }
}

ByteBufferallocateDirect方法返回的是DirectByteBuffer

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>

    public static ByteBuffer allocateDirect(int capacity) {
        // 创建DirectByteBuffer
        return new DirectByteBuffer(capacity);
    }
}

参考

Java NIO direct buffer的优势在哪儿?

JAVA IO专题一:java InputStream和OutputStream读取文件并通过socket发送,到底涉及几次拷贝

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1056102.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1574. 删除最短的子数组使剩余数组有序

1574. 删除最短的子数组使剩余数组有序 原题链接&#xff1a;完成情况&#xff1a;解题思路&#xff1a;参考代码&#xff1a; 原题链接&#xff1a; 1574. 删除最短的子数组使剩余数组有序 https://leetcode.cn/problems/shortest-subarray-to-be-removed-to-make-array-so…

竞赛选题 机器视觉 opencv 深度学习 驾驶人脸疲劳检测系统 -python

文章目录 0 前言1 课题背景2 Dlib人脸识别2.1 简介2.2 Dlib优点2.3 相关代码2.4 人脸数据库2.5 人脸录入加识别效果 3 疲劳检测算法3.1 眼睛检测算法3.2 打哈欠检测算法3.3 点头检测算法 4 PyQt54.1 简介4.2相关界面代码 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#x…

redis runtime error: invalid memory address or nil pointer dereference

var Conn redis.Connfunc RedisInit(){Conn, err redis.Dial("tcp", "127.0.0.1:6379")if err ! nil {fmt.Println("redis.Dial err", err)return}fmt.Println("conn suc.11..", Conn)//defer Conn.Close()看看这两段代码的区别 而且…

游戏报错提示d3dx9_43.dll丢失怎么解决?这4个解决方法可以解决

随着科技的发展&#xff0c;电脑已经成为我们生活中不可或缺的一部分。然而&#xff0c;在使用电脑的过程中&#xff0c;我们可能会遇到各种问题&#xff0c;其中之一就是“d3dx9_43.dll丢失”。这个问题可能会影响到我们的电脑性能&#xff0c;甚至导致一些软件无法正常运行。…

管理经济学基本概念(三): 制定战略、竞争优势、员工利益等

1、怎么制定战略才能具备可持续的竞争优势 1.1、什么是可持续的竞争优势&#xff1f; 拥有以下两点&#xff0c;厂商就有了一种竞争优势&#xff0c;具有竞争优势的厂商能够赚取正值的经济利润&#xff1a; 以更低的成本像竞争对手一样提供相同的产品或服务效益 以相同的成本…

使用华为eNSP组网试验⑷-OSPF多区域组网

今天进行了OSPF的多区域组网试验&#xff0c;本来这是个很简单的操作&#xff0c;折腾了好长时间&#xff0c;根本原因只是看了别人写的配置代码&#xff0c;没有真正弄明白里面对应的规则。 一般情况下&#xff0c;很多单位都使用OSPF进行多区域的组网&#xff0c;大体分为1个…

[React] Zustand状态管理库

文章目录 1.Zustand介绍2.创建一个store3.使用方法3.1 获取状态3.2 更新状态3.3 访问存储状态3.4 处理异步数据3.5 在状态中访问和存储数组3.6 持续状态 4.总结 1.Zustand介绍 状态管理一直是现代程序应用中的重要组成部分, Zustand使用 hooks 来管理状态无需样板代码。 更少…

2023腾讯云服务器优惠价格表_10月更新报价

阿里云服务器10月报价表来了&#xff0c;和9月份价格差不多&#xff0c;再等一个月就到腾讯云双十一优惠活动了&#xff0c;腾讯云百科先来说说10月腾讯云服务器优惠价格表&#xff1a;轻量应用服务器2核2G3M带宽95元一年、2核4G5M带宽218元一年、2核2G4M带宽三年价540元一年、…

多头注意力机制

前面已经讲完了自注意力机制&#xff0c;简单来讲&#xff0c;就是对一组向量空间分别求内积&#xff0c;然后进行缩放&#xff0c;最后对不同的向量使用压缩后的分数累加求和。 1.多头是个什么东西&#xff1f; 实际上很简单&#xff0c;自注意力层的输出空间被分解为一组独立…

(ubuntu)Docker 安装linux 详情过程

文章目录 前言Docker 安装linux第一步&#xff1a;使用dokcker 拉取镜像&#xff1a;第二步&#xff1a;创建本地目录&#xff08;用于挂载&#xff09;第三步&#xff1a;&#xff08;上传配置文件&#xff09;修改配置文件第四步&#xff1a;创建docker容器第五步: 测试本地连…

检测防火墙是否开启、判断程序是否加入防火墙白名单(附源码)

VC常用功能开发汇总&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&#xff09;https://blog.csdn.net/chenlycly/article/details/124272585C软件异常排查从入门到精通系列教程&#xff08;专栏文章列表&#xff0c;欢迎订阅&#xff0c;持续更新...&a…

软考——软件设计师中级2023年11月备考(1.计算机组成原理)

一、计算机组成原理 1.数据的表示 1.1 十进制转R进制 方法&#xff1a;对十进制数除R取余&#xff0c;最后对余数取倒序 如&#xff1a; 1.2 原码反码补码 1.3 浮点数 1.4 校验码 —— 海明码 &#xff08;非重点&#xff0c;了解即可&#xff09; 海明码的构成方法&…

2023年Linux总结常用命令

1.常用命令 1.1创建文件夹 mkdir -p forever/my 1.2当前目录 pwd 1.3创建文件 touch 1.txt 1.4查看文件 cat 1.txt 1.5复制文件 说明&#xff1a;-r是复制文件夹 cp -r my myCopy 1.6删除文件 说明&#xff1a;-r带包删除文件夹&#xff0c;-f表示强制删除(保存问题) rm -r…

BEAPP:脑电批处理平台

摘要 脑电图(EEG)提供了与各种神经系统和神经精神疾病相关的脑功能信息。EEG包含复杂的高时间分辨率信息&#xff0c;而计算分析可以最大限度地利用这些信息。在这里&#xff0c;本研究提出了批量脑电图自动处理平台(BEAPP)&#xff0c;这是一个自动化、灵活的EEG处理平台&…

freertos简介与移植

freertos是一个可裁剪的小型rtos系统&#xff0c;特点&#xff1a; 支持抢占式&#xff0c;合作式和时间片调度saferos衍生自freertos&#xff0c;更完整提供了一个用于低功耗的tickless模式系统的组件在创建时可以选择动态或者静态的ram&#xff0c;例如任务&#xff0c;消息…

python之阈值分割

阈值分割法是一种基于区域的图像分割技术&#xff0c;原理是把图像像素点分为若干类。图像阈值化分割是一种传统的最常用的图像分割方法&#xff0c;因其实现简单、计算量小、性能较稳定而成为图像分割中最基本和应用最广泛的分割技术。它特别适用于目标和背景占据不同灰度级范…

python之股票财务分析

#import akshare as ak import pandas as pd import matplotlib.pyplot as plt symbol1"资产负债表" symbol2"利润表" symbol3"现金流量表" #df1ak.stock_financial_report_sina(stock"601633",symbolsymbol1) #df2ak.stock_financial…

Linux CentOS7 vim多窗口编辑

我们在用vim编辑文件时&#xff0c;有各种需求。如有时需要在多个文件之间来回操作&#xff0c;一会关闭一个文件&#xff0c;一会再打开另外一个文件&#xff0c;这样来回操作显得太笨拙。有时&#xff0c;vim编辑多行的大文件&#xff0c;来回查看、编辑前面一部分及最后一部…

《从零开始的Java世界》03面向对象(进阶)

《从零开始的Java世界》系列主要讲解Javase部分&#xff0c;从最简单的程序设计到面向对象编程&#xff0c;再到异常处理、常用API的使用&#xff0c;最后到注解、反射&#xff0c;涵盖Java基础所需的所有知识点。学习者应该从学会如何使用&#xff0c;到知道其实现原理全方位式…

竞赛 机器学习股票大数据量化分析与预测系统 - python 竞赛

文章目录 0 前言1 课题背景2 实现效果UI界面设计web预测界面RSRS选股界面 3 软件架构4 工具介绍Flask框架MySQL数据库LSTM 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 机器学习股票大数据量化分析与预测系统 该项目较为新颖&am…