【Java】Java中的零拷贝

news2024/9/21 23:34:52

物理内存

计算机物理内存条的容量,比如我们买电脑会关注内存大小有多少G,这个容量就是计算机的物理内存。

虚拟内存

操作系统为每个进程分配了独立的虚拟地址空间,也就是虚拟内存,虚拟地址空间又分为用户空间和内核空间,操作系统的位数不同,虚拟地址空间的大小也不同,32位操作系统虚拟地址内核空间为1G,用户空间大小为3G,64位操作系统用户空间和内核空间大小各为128T:

既然每个进程都拥有一块独立的虚拟地址空间,那么所有进程的虚拟地址空间大小加起来必定大于物理内存的大小,所以虚拟地址空间只是一个虚拟的概念,只有需要分配内存的时候才会为虚拟内存分配物理内存,并通过内存映射来管理虚拟地址和物理内存地址之间的映射关系。

用户空间 / 内核空间

**用户空间:**是运行用户程序代码的地方,为了保证系统内核的安全,它不能直接访问内存等硬件设备,必须通过系统调用进入到内核空间来访问那些受限的资源。

**内核空间:**是运行内核代码的地方,可以执行任意的指令访问系统资源,既可以访问内核空间也可以访问用户空间。

**用户态:**进程运行在用户空间时处于用户态。

**内核态:**进程运行在内核空间时处于内核态。

文件I/O

文件I/O与读写文件有关,比如我们启动了一个程序,此时运行在用户空间(用户态),接着准备做一个读取磁盘文件的操作,由于用户空间是无法直接从磁盘读取文件的,所以需要调用内核提供的接口来完成文件的读取,调用内核的接口的过程中由用户空间进入到了内核空间(内核态),DMA从磁盘读取文件到内核的缓冲区,之后再将数据从内核的缓冲区拷贝到用户空间完成文件的读取操作:

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

**可以发现,整个读取过程发生了两次数据拷贝,一次是DMA将磁盘上的文件数据拷贝到内核缓冲区,一次是将内核缓冲区的数据拷贝到用户缓冲区。**写操作与读取操作类似,只不过是将用户缓冲区的数据拷贝到内核缓冲区,再将内核缓冲区的数据拷贝到文件。

文件I/O从操作系统的角度来看还可以划分为缓存I/O、直接I/O和mmap内存映射。

缓存I/O

也称标准I/O,上面提到的文件I/O读取数据的例子就是使用的缓存I/O,它需要将数据先拷贝到内核缓冲区,再将内核缓冲区的数据拷贝到用户缓冲区,数据经过两次拷贝,内核缓冲区和用户缓冲区分别指向不同的物理内存,在文件I/O中,内核缓冲区是在Page Cache层,这也是称为缓存I/O的原因

JAVA中通过java.io包下进行读写文件使用的就是缓存I/O。

为什么需要缓存IO?

因为磁盘I/O是比较耗时的操作,如果每次都从磁盘上读取文件,性能将会大大下降,为了提升读取性能,增加了一层Page Cache,用于缓存读取的文件数据,Page Cache占用的是内存,从内存读取的速度远远大于从磁盘读取,内核缓冲区就是在Page Cache中开辟的一块内存,用户空间进行系统调用读取文件内容时,首先会判断Page Cache中是否缓存了文件的内容,如果缓存了直接读取即可,否则再从磁盘读取,所以缓存I/O可以减少磁盘I/O的次数提升性能。

文件的写操作同样如此,进行写操作时,将数据先写到Page Cache的缓冲区中,后续由操作系统将数据刷回到磁盘中。

缓存I/O的优缺点

优点:减少磁盘I/O次数,提升读写性能。

缺点:数据需要在内核空间和用户空间来回拷贝。

DirectByteBuffer

使用缓存I/O读取数据时,数据会经过两次拷贝,经过两次拷贝是从系统调用开始讲起,在JAVA中由于涉及到JVM堆内和堆外内存,如果使用java.io下的类进行文件读写实际上还会再多一次拷贝(详细可参考【JAVA】普通IO数据拷贝次数的问题探讨 ):

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

img

在Java的NIO中,提供了DirectByteBuffer,可以直接分配堆外内存,减少了一次从堆外内存到堆内内存的复制(CPU复制)

直接I/O

缓存I/O经过了Page Cache,读取过程中需要将数据从Page Cache的缓冲区中拷贝到用户空间的缓存区,那么有没有一种方式可以省去这个拷贝的过程?

答案是有的,那就是直接I/O,应用程序直接访问磁盘数据,绕过了Page Cache,省去了从内核缓冲区拷贝到用户缓冲区的过程:

目前JAVA并没有原生的直接/O操作方式,不过公众号博主Kirito提供了在JAVA中进行直接I/O操作的方法,具体参见【Kirito的技术分享】Java 文件 IO 操作之 DirectIO。

内存映射

内存映射就是将虚拟空间地址映射到物理空间地址,每个进程维护了一张页表,记录虚拟地址和物理地址之间的映射关系,当进程访问的虚拟地址在页表中无法查到映射关系时,系统产生缺页异常,进入内核空间为虚拟地址分配物理内存,并更新页表,记录映射关系。

文件映射

内存映射除了映射虚拟空间地址和物理空间地址,还包括将磁盘的文件内容映射到虚拟地址空间,称为文件映射,此时可以通过访问内存来访问文件里面的数据 。

mmap系统调用可以将文件映射到虚拟内存空间。文件映射的流程如下:

  1. 进行mmap系统调用,将文件和虚拟地址空间建立映射,注意此时还没有分配物理内存空间,只是在逻辑上建立了虚拟地址和文件之间的映射关系,物理内存只有真正使用的时候才会分配。
  2. 应用程序访问用户空间虚拟内存中的某个地址,发现无法在页表中查到数据,产生缺页异常,此时进入内核空间
  3. 因为不能直接使用物理地址,所以需要使用内核的虚拟地址临时建立与物理内存的映射关系,将文件内容读取到物理内存中,待数据读取完毕之后取消临时映射即可。
  4. 缺页异常处理完毕,物理内存中已经加载了文件的数据,此时用户空间就可以通过虚拟地址直接访问物理内存中映射的文件数据。

img

从文件映射的流程中可以看出它与缓存I/O相比,少了从内核缓冲区将数据拷贝到用户缓冲区的步骤,减少了一次拷贝。

Java NIO中提供了MappedByteBuffer来处理文件映射,下面是一个读取文件的例子:

public class MappedByteBufferTest {

    public static void main(String[] args) {
        
        try (RandomAccessFile file = new RandomAccessFile(new File("/Users/sml/test.txt"), "r")) {
            // 获取FileChannel
            FileChannel fileChannel = file.getChannel();
            long size = fileChannel.size();
            // 调用map方法进行文件映射,返回MappedByteBuffer
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
            byte[] bytes = new byte[(int)size];
            for (int i = 0; i < size; i++) {
                // 读取数据
                bytes[i] = mappedByteBuffer.get();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

零拷贝

零拷贝一般指的是从磁盘读取文件发送到网络或者从网络接收数据写入到磁盘文件的过程中,减少数据的拷贝次数。

网络I/O

网络I/O与网络数据发送/接收有关,与文件I/O的底层原理一致,同样以读取数据为例,文件I/O是从磁盘读取文件,网络I/O是从网卡中读取数据。比如客户端与服务端建立了一个连接,客户端向服务端发送数据,服务端从网卡中读取客户端发送的数据到内核中的socket缓冲区,再将socket缓冲区的数据复制到用户空间的缓冲区:

使用缓存I/O发送数据到网络

首先看一下使用缓存I/O从磁盘文件读取数据并发送到网络上的过程:

  1. 用户发起系统调用,进入到内核态,DMA从磁盘上读取数据到内核缓冲区(DMA复制);
  2. CPU将内核缓冲区的数据拷贝到用户缓冲区(CPU复制),切换回到用户空间;
  3. 再次从用户空间切换到内核空间,CPU将用户缓冲区的数据拷贝到socket缓冲区(CPU复制);
  4. DMA将socket缓冲区的数据拷贝到网卡(DMA复制),之后从内核空间切换回用户空间;

使用缓存I/O数据经过了四次拷贝,需要多次在内核空间和用户空间来回切换,影响系统性能。从数据拷贝的过程可以看到有些步骤其实是多余的,比如第二步,如果可以直接将内核缓存区的数据拷贝到socket缓冲区,或者直接将内核缓冲区的数据拷贝到网卡,岂不是减少了数据拷贝的次数?零拷贝就是这样一种致力于减少数据拷贝的技术。

Linux中的零拷贝

sendfile

Linux在2.1版本中引入了sendfile函数,可以实现将数据从一个文件描述符传输到另外一个文件描述符:

  1. 发起sendfile系统调用,进入到内核空间;
  2. DMA从磁盘读取文件到内核缓冲区(DMA复制);
  3. 将内核缓冲区数据拷贝到socket缓冲区(CPU复制);
  4. 将socket缓冲区数据拷贝到网卡(DMA复制),之后切换回用户空间;

sendfile减少了一次数据从内核缓冲区拷贝到用户缓冲区的过程,可以直接将内核缓冲区的数据拷贝到socket缓冲区。

sendfile + DMA GATHER

Linux在2.4版本中引入了gather技术,我们知道内核缓冲区在内存中有对应的地址,gather操作可以将内核缓冲区的内存地址、地址偏移量信息记录到socket缓冲区中,之后DMA根据地址信息从内存中读取数据到网卡中,减少了数据从内核缓冲区到socket缓冲区的拷贝过程:

可以看到零拷贝并不是指的数据一次拷贝都没有发生,而是指减少CPU进行数据拷贝的次数。

Java中的零拷贝

MappedByteBuffer

在内存映射中说过,可以通过文件映射的方式将磁盘的文件内容映射到虚拟地址空间,用户空间就可以通过虚拟地址直接访问物理内存中的映射的文件数据,而Java NIO中也提供了MappedByteBuffer来处理文件映射,使用MappedByteBuffer向网络中发送数据的过程如下:

  1. 使用MappedByteBuffer建立文件映射,用户空间可以通过虚拟地址直接访问映射的文件数据;

  2. 将映射的文件数据拷贝到socket网络缓冲区(CPU复制);

  3. DMA将socket缓冲区的数据拷贝到网卡(DMA复制);

MappedByteBuffer减少了从内核缓冲区到用户缓冲区的数据拷贝,可以直接将内核缓冲区的数据拷贝到网络缓冲区。

FileChannel

Java NIO中的FileChannel可以实现将数据从FileChannel直接传输到另一个Channel,它是sendfile的一种实现:

            RandomAccessFile file = new RandomAccessFile(new File("/Users/sml/test.txt"), "r");
            // 获取FileChannel
            FileChannel fileChannel = file.getChannel();
            long size = fileChannel.size();
            SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
            fileChannel.transferTo(0,size,socketChannel);

参考

【极客时间-倪朋飞】Linux性能优化实战

【极客时间-刘超】趣谈Linux操作系统

【拉勾教育-若地】Netty 核心原理剖析与 RPC 实践

【 Kirito的技术分享】文件IO操作的最佳实践

【小码农叔叔】java使用nio读写文件

【占小狼】深入浅出MappedByteBuffer

【零壹技术栈】深入剖析Linux IO原理和几种零拷贝机制的实现

【tomas家的小拨浪鼓】堆外内存 之 DirectByteBuffer 详解

网络IO和磁盘IO详解

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

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

相关文章

[网鼎杯 2018]Comment git泄露 / 恢复 二次注入 bash_history文件查看

首先我们看到账号密码有提示了 我们bp爆破一下 我首先对数字爆破 因为全字符的话太多了 爆出来了哦 所以账号密码也出来了 zhangwei zhangwei666 没有什么用啊 扫一下吧 有git git泄露 那泄露看看 真有 <?php include "mysql.php"; session_start(); if(…

JUC第二十一讲:JUC线程池:ScheduledThreadPoolExecutor详解

JUC线程池&#xff1a;ScheduledThreadPoolExecutor详解 本文是JUC第二十一讲&#xff0c;JUC线程池: ScheduledThreadPoolExecutor详解。在很多业务场景中&#xff0c;我们可能需要周期性的运行某项任务来获取结果&#xff0c;比如周期数据统计&#xff0c;定时发送数据等。在…

07_项目开发_用户信息列表

1 用户信息列表内容展示 用户信息列表&#xff0c;主要完成用户信息的添加、删除、修改和查找功能。 用户列表页面效果&#xff1a; 单击“添加用户”按钮&#xff0c;进入添加用户页面。 填写正确的信息后&#xff0c;单击“添加用户”按钮&#xff0c;会直接跳转到用户列表…

算法题:买卖股票的最佳时机 II (贪心算法解决股票问题)

这道题是贪心算法的中级难度练习题&#xff0c;由于题目设定&#xff0c;整个价格都是透明的&#xff0c;这里并不涉及需要预测股票涨势的问题。解决思路不难&#xff0c;就是一旦股票价格开始下降了就买入&#xff0c;一旦上升了&#xff0c;就赶紧卖出。&#xff08;完整题目…

算法题:盛最多水的容器(贪心算法双指针问题)

这个题目乍一看就是双指针&#xff0c;没想到官方解答也是双指针&#xff0c;我在官方的基础上优化了一下下&#xff0c;左右两边各一个指针&#xff0c;每次移动短的那一头的时候&#xff0c;不是移动一格&#xff0c;而是找到比短的那一头要长一点的&#xff0c;再进行比较。…

机器学习与模式识别作业----决策树属性划分计算

文章目录 1.决策树划分原理1.1.特征选择1--信息增益1.2.特征选择2--信息增益比1.3.特征选择3--基尼系数 2.决策树属性划分计算题2.1.信息增益计算2.2.1.属性1的信息增益计算2.2.2.属性2的信息增益计算2.2.3.属性信息增益比较 2.2.信息增益比计算2.3.基尼系数计算 1.决策树划分原…

小程序中使用echarts配置以及折线图案例(简单易懂)

第一步&#xff1a;引入echarts文件--此文件需要下载&#xff1a; 下载地址&#xff1a;点击此处进行下载echarts文件 点击Download ZIP下载压缩包&#xff0c;注意&#xff1a;此文件&#xff0c;我是从完整的文件中剥离出来的有用的&#xff0c;不会影响项目。 第二步&#…

# 解析Pikachu靶场:一个安全研究的练习场

引言 Pikachu靶场是一个非常流行的安全研究和渗透测试练习平台。这个环境包括多个安全漏洞&#xff0c;从基础的到高级的&#xff0c;供安全研究人员和渗透测试者进行实验和学习。在这篇博客中&#xff0c;我们将探讨Pikachu靶场的基本概念&#xff0c;功能&#xff0c;以及如…

vue3+vite+ts 组件中自动导入 ref 和 reactive

前言 在每个vue组件中&#xff0c;都去手动引入 ref 和 reactive 是非常繁琐的一件事&#xff0c;我们可以通过插件来完成自动导入 安装插件 npm i unplugin-auto-import -D 配置插件 在 vite.config.ts 中增加如下代码 import { defineConfig } from vite import vue fr…

初识操作系统以及Linux环境搭建

&#x1f4d9;作者简介&#xff1a; 清水加冰&#xff0c;目前大二在读&#xff0c;正在学习C/C、Python、操作系统、数据库等。 &#x1f4d8;相关专栏&#xff1a;C语言初阶、C语言进阶、C语言刷题训练营、数据结构刷题训练营、有感兴趣的可以看一看。 欢迎点赞 &#x1f44d…

大数据Splunk Enterprise 平台+cpolar 实现远程访问

文章目录 前言1. 搭建Splunk Enterprise2. windows 安装 cpolar3. 创建Splunk Enterprise公网访问地址4. 远程访问Splunk Enterprise服务5. 固定远程地址 前言 Splunk Enterprise是一个强大的机器数据管理平台&#xff0c;可帮助客户分析和搜索数据&#xff0c;以及可视化数据…

企架布道:中电金信应邀出席2023佛山敏捷之旅暨DevOps Meetup

近日&#xff0c;2023佛山敏捷之旅暨DevOps Meetup活动顺利举行&#xff0c;本次活动以助力大湾区金融和互联网企业敏捷DevOps实施和效能提升为主题&#xff0c;共设立 2个会场&#xff0c;16个话题分享&#xff0c;200余位金融、互联网企业相关从业人员齐聚一堂&#xff0c;共…

第二证券:市场情绪或逐步修复 十月行情值得期待

第二证券指出&#xff0c;周一A股商场探底回升、小幅轰动收拾&#xff0c;沪指全天底子出现先抑后扬的运转特征。其时上证综指与创业板指数的平均市盈率分别为12.46倍、33.94倍&#xff0c;处于近三年中位数以下水平&#xff0c;商场估值仍然处于较低区域&#xff0c;合适中长期…

excel单元格合并策略

excel单元格合并策略 证明112&#xff1f; 要证明112这个问题&#xff0c;首先我们要找到问题的关键。所谓问题的关键呢&#xff0c;就是关键的问题&#xff0c;那么如何找到问题的关键就是这个问题的关键。 比如说&#xff0c;你有一个苹果&#xff0c;我也有一个苹果&#x…

管理Linux的联网

1. RHEL9版本特点 在RHEL7版本中&#xff0c;同时支持network.service和NetworkManager.service&#xff08;简称NM&#xff09;。 在RHEL8上默认只能通过NM进行网络配置&#xff0c;包括动态ip和静态ip,若不开启NM&#xff0c;否则无法使用网络 RHEL8依然支持network.service&…

【工具软件】Nativefier——把网页打包成exe软件

官方文档 安装 npm install nativefier -g使用 在 nativefier 后加上需要转换的网站地址, 比如: nativefier "https://blog.csdn.net/IAIPython?typeblog"第一次打包需要下载 Eletron 框架, 很慢… 运行完毕, 会生成一个应用, 路径一般为C:\Users\用户名… 如图…

前后端分离项目-基于springboot+vue的图书馆管理系统的设计与实现(内含代码+文档+报告)

博主介绍&#xff1a;✌全网粉丝10W,前互联网大厂软件研发、集结硕博英豪成立工作室。专注于计算机相关专业毕业设计项目实战6年之久&#xff0c;选择我们就是选择放心、选择安心毕业✌ &#x1f345;由于篇幅限制&#xff0c;想要获取完整文章或者源码&#xff0c;或者代做&am…

Servlet开发步骤

标准Java Web工程结构 pom.xml中提供servlet依赖 1.创建java类&#xff0c;继承HttpServlet 2.重写service方法&#xff0c;处理请求&#xff0c;生成响应 3.配置web.xml&#xff0c;绑定访问地址 Servlet接收请求参数 request.getParameter() 接收单个参数 request.ge…

区块链游戏的开发框架

链游&#xff08;Blockchain Games&#xff09;是基于区块链技术构建的游戏。它们与传统游戏有一些显著不同之处&#xff0c;因此需要特定的开发框架和工具。以下是一些用于链游开发的开发框架及其特点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专…

一站式 API 管理和测试工具:PostCat 轻松完成接口调测 | 开源日报 No.49

rubickCenter/rubick Stars: 5.0k License: MIT Rubick 是一个基于 electron 开源工具箱的项目&#xff0c;它允许用户自由集成丰富插件来创建最终桌面效率工具。该项目以 Dota 英雄中的 Rubick 为名&#xff0c;因为他可以使用其他英雄作为插件完成任务。以下是 Rubick 的主要…