NIO效率高的原理之零拷贝与直接内存映射

2023-11-12

前言

在笔者上一篇博客,详解了NIO,并总结NIO相比BIO的效率要高的三个原因,点击查看

这篇博客将针对第三个原因,进行更详细的讲解。

首先澄清,零拷贝与内存直接映射并不是Java中独有的概念,并且这两个技术并不是等价的。

零拷贝

零拷贝是指避免在用户态(User-space) 与内核态(Kernel-space) 之间来回拷贝数据的技术。

传统IO

传统IO读取数据并通过网络发送的流程,如下图

640?wx_fmt=gif

  1. read()调用导致上下文从用户态切换到内核态。内核通过sys_read()(或等价的方法)从文件读取数据。DMA引擎执行第一次拷贝:从文件读取数据并存储到内核空间的缓冲区。

  2. 请求的数据从内核的读缓冲区拷贝到用户缓冲区,然后read()方法返回。read()方法返回导致上下文从内核态切换到用户态。现在待读取的数据已经存储在用户空间内的缓冲区。至此,完成了一次IO的读取过程。

  3. send()调用导致上下文从用户态切换到内核态。第三次拷贝数据从用户空间重新拷贝到内核空间缓冲区。但是,这一次,数据被写入一个不同的缓冲区,一个与目标套接字相关联的缓冲区。

  4. send()系统调用返回导致第四次上下文切换。当DMA引擎将数据从内核缓冲区传输到协议引擎缓冲区时,第四次拷贝是独立且异步的。

内存缓冲数据(上图中的read buffer和socket buffer ),主要是为了提高性能,内核可以预读部分数据,当所需数据小于内存缓冲区大小时,将极大的提高性能。

640?wx_fmt=gif

磁盘到内核空间属于DMA拷贝,用户空间与内核空间之间的数据传输并没有类似DMA这种可以不需要CPU参与的传输方式,因此用户空间与内核空间之间的数据传输是需要CPU全程参与的(如上图所示)。

DMA拷贝即直接内存存取,原理是外部设备不通过CPU而直接与系统内存交换数据

所以也就有了使用零拷贝技术,避免不必要的CPU数据拷贝过程。

NIO的零拷贝

NIO的零拷贝由transferTo方法实现。transferTo方法将数据从FileChannel对象传送到可写的字节通道(如Socket Channel等)。在transferTo方法内部实现中,由native方法transferTo0来实现,它依赖底层操作系统的支持。在UNIX和Linux系统中,调用这个方法会引起sendfile()系统调用,实现了数据直接从内核的读缓冲区传输到套接字缓冲区,避免了用户态(User-space) 与内核态(Kernel-space) 之间的数据拷贝。

640?wx_fmt=gif

使用NIO零拷贝,流程简化为两步:

  1. transferTo方法调用触发DMA引擎将文件上下文信息拷贝到内核读缓冲区,接着内核将数据从内核缓冲区拷贝到与套接字相关联的缓冲区。

  2. DMA引擎将数据从内核套接字缓冲区传输到协议引擎(第三次数据拷贝)。

内核态与用户态切换如下图:

640?wx_fmt=gif

相比传统IO,使用NIO零拷贝后改进的地方:

  1. 我们已经将上下文切换次数从4次减少到了2次;

  2. 将数据拷贝次数从4次减少到了3次(其中只有1次涉及了CPU,另外2次是DMA直接存取)。

如果底层NIC(网络接口卡)支持gather操作,可以进一步减少内核中的数据拷贝。在Linux 2.4以及更高版本的内核中,socket缓冲区描述符已被修改用来适应这个需求。这种方式不但减少上下文切换,同时消除了需要CPU参与的重复的数据拷贝。

640?wx_fmt=gif

用户这边的使用方式不变,依旧通过transferTo方法,但是方法的内部实现发生了变化:

  1. transferTo方法调用触发DMA引擎将文件上下文信息拷贝到内核缓冲区。

  2. 数据不会被拷贝到套接字缓冲区,只有数据的描述符(包括数据位置和长度)被拷贝到套接字缓冲区。DMA 引擎直接将数据从内核缓冲区拷贝到协议引擎,这样减少了最后一次需要消耗CPU的拷贝操作。

NIO零拷贝适用于以下场景:

  1. 文件较大,读写较慢,追求速度

  2. JVM内存不足,不能加载太大数据

  3. 内存带宽不够,即存在其他程序或线程存在大量的IO操作,导致带宽本来就小

NIO的零拷贝代码示例

/**
 * filechannel进行文件复制(零拷贝)
 *
 * @param fromFile 源文件
 * @param toFile   目标文件
 */
public static void fileCopyWithFileChannel(File fromFile, File toFile) {
    try (// 得到fileInputStream的文件通道
         FileChannel fileChannelInput = new FileInputStream(fromFile).getChannel();
         // 得到fileOutputStream的文件通道
         FileChannel fileChannelOutput = new FileOutputStream(toFile).getChannel()) {

        //将fileChannelInput通道的数据,写入到fileChannelOutput通道
        fileChannelInput.transferTo(0, fileChannelInput.size(), fileChannelOutput);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

static final int BUFFER_SIZE = 1024;
/**
 * BufferedInputStream进行文件复制(用作对比实验)
 *
 * @param fromFile 源文件
 * @param toFile   目标文件
 */
public static void bufferedCopy(File fromFile,File toFile) throws IOException {
    try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fromFile));
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(toFile))){
        byte[] buf = new byte[BUFFER_SIZE];
        while ((bis.read(buf)) != -1) {
            bos.write(buf);
        }
    }
}

 

 

在不需要进行数据文件操作时,可以使用NIO的零拷贝。但如果既需要IO速度,又需要进行数据操作,则需要使用NIO的直接内存映射。

直接内存映射

Linux提供的mmap系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间;同样地, 内核空间对这段区域的修改也直接反映用户空间。正因为有这样的映射关系, 就不需要在用户态(User-space)与内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率,这就是内存直接映射技术。

NIO的直接内存映射

JDK1.4加入了NIO机制和直接内存,目的是防止Java堆和Native堆之间数据复制带来的性能损耗,此后NIO可以使用Native的方式直接在 Native堆分配内存。

背景:堆内数据在flush到远程时,会先复制到Native 堆,然后再发送;直接移到堆外就更快了。

在JDK8,Native Memory包括元空间和Native 堆。更多有关JVM的知识,点击查看JVM内存模型和垃圾回收机制

640?wx_fmt=jpeg

直接内存的创建

在ByteBuffer有两个子类,HeapByteBuffer和DirectByteBuffer。前者是存在于JVM堆中的,后者是存在于Native堆中的。

640?wx_fmt=png

申请堆内存

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

 

申请直接内存

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

 

 

使用直接内存的原因

  1. 对垃圾回收停顿的改善。因为full gc时,垃圾收集器会对所有分配的堆内内存进行扫描,垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理。这样做的结果就是能保持一个较小的JVM堆内存,以减少垃圾收集对应用的影响。(full gc时会触发堆外空闲内存的回收。)

  2. 减少了数据从JVM拷贝到native堆的次数,在某些场景下可以提升程序I/O的性能。

  3. 可以突破JVM内存限制,操作更多的物理内存。

当直接内存不足时会触发full gc,排查full gc的时候,一定要考虑。

有关JVM和GC的相关知识,请点击查看JVM内存模型和垃圾回收机制

使用直接内存的问题

  1. 堆外内存难以控制,如果内存泄漏,那么很难排查(VisualVM可以通过安装插件来监控堆外内存)。

  2. 堆外内存只能通过序列化和反序列化来存储,保存对象速度比堆内存慢,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。

  3. 直接内存的访问速度(读写方面)会快于堆内存。在申请内存空间时,堆内存速度高于直接内存。

直接内存适合申请次数少,访问频繁的场合。如果内存空间需要频繁申请,则不适合直接内存。

NIO的直接内存映射

NIO中一个重要的类:MappedByteBuffer——java nio引入的文件内存映射方案,读写性能极高。MappedByteBuffer将文件直接映射到内存。可以映射整个文件,如果文件比较大的话可以考虑分段进行映射,只要指定文件的感兴趣部分就可以。

由于MappedByteBuffer申请的是直接内存,因此不受Minor GC控制,只能在发生Full GC时才能被回收,因此Java提供了DirectByteBuffer类来改善这一情况。它是MappedByteBuffer类的子类,同时它实现了DirectBuffer接口,维护一个Cleaner对象来完成内存回收。因此它既可以通过Full GC来回收内存,也可以调用clean()方法来进行回收

NIO的直接内存映射的函数调用

FileChannel提供了map方法来把文件映射为内存对象:

MappedByteBuffer map(int mode,long position,long size);

可以把文件的从position开始的size大小的区域映射为内存对象,mode指出了 可访问该内存映像文件的方式

  • READ_ONLY,(只读): 试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException.(MapMode.READ_ONLY)

  • READ_WRITE(读/写): 对得到的缓冲区的更改最终将传播到文件;该更改对映射到同一文件的其他程序不一定是可见的。 (MapMode.READ_WRITE)

  • PRIVATE(专用): 对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其他程序也不是可见的;相反,会创建缓冲区已修改部分的专用副本。 (MapMode.PRIVATE)

使用参数-XX:MaxDirectMemorySize=10M,可以指定DirectByteBuffer的大小最多是10M。

直接内存映射代码示例

static final int BUFFER_SIZE = 1024;

/**
 * 使用直接内存映射读取文件
 * @param file
 */
public static void fileReadWithMmap(File file) {

    long begin = System.currentTimeMillis();
    byte[] b = new byte[BUFFER_SIZE];
    int len = (int) file.length();
    MappedByteBuffer buff;
    try (FileChannel channel = new FileInputStream(file).getChannel()) {
        // 将文件所有字节映射到内存中。返回MappedByteBuffer
        buff = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
        for (int offset = 0; offset < len; offset += BUFFER_SIZE) {
            if (len - offset > BUFFER_SIZE) {
                buff.get(b);
            } else {
                buff.get(new byte[len - offset]);
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    System.out.println("time is:" + (end - begin));
}

/**
 * HeapByteBuffer读取文件
 * @param file
 */
public static void fileReadWithByteBuffer(File file) {

    long begin = System.currentTimeMillis();
    try(FileChannel channel = new FileInputStream(file).getChannel();) {
        // 申请HeapByteBuffer
        ByteBuffer buff = ByteBuffer.allocate(BUFFER_SIZE);
        while (channel.read(buff) != -1) {
            buff.flip();
            buff.clear();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    long end = System.currentTimeMillis();
    System.out.println("time is:" + (end - begin));
}

 

更多内容,欢迎关注微信公众号:全菜工程师小辉~

640?wx_fmt=png

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

NIO效率高的原理之零拷贝与直接内存映射 的相关文章

  • jboss的netty中下游事件是如何工作的?

    刚刚开始使用 netty 来实现我自己的服务器 我花了一段时间才掌握它的窍门 但现在我能够通过编写自己的 MessageHandler 来接受客户端 并且在 messageReceived 内部我能够从缓冲区中读取数据并执行一些与接收到的数
  • 如何在 Netty 通道处理程序中安全地执行阻塞操作?

    我正在构建一个基于 Netty 的小型应用程序 该应用程序通过套接字连接 即 telnet ssh 执行 I O 操作 我正在使用 Netty 启动我的套接字服务器ServerBootstrap类 给出 类型的事件循环NioEventLoo
  • Java中的IO与NIO篇----第二篇

    系列文章目录 文章目录 系列文章目录 前言 一 阻塞 IO 模型 二 非阻塞 IO 模型 三 多路复用 IO 模型 前言 前些天发现了一个巨牛的人工智能学习网站 通俗易懂 风趣幽默 忍不住分享一下给大家 点击跳转到网站 这篇文章男女通用 看
  • DELETE_ON_CLOSE 在 Linux 上关闭之前删除文件

    我使用 Java 7 nio API 有以下代码 import java io IOException import java io ObjectOutputStream import java io OutputStream import
  • 解决 Java7 中 FileWalking 树中拒绝访问的问题

    下面是一些简单的代码 只是为了测试Files walkFileTree 方法 然而 该文件夹 etc ssl private 它具有这些权限 rwx x 抛出异常 即使我认为我用 if 语句来保护它 if permissions equal
  • 高并发 Apache 异步 HTTP 客户端 IOReactor 问题

    应用说明 我正在使用由 Comsat 的 Quasar FiberHttpClient 版本 0 7 0 包装的 Apache HTTP 异步客户端 版本 4 1 1 来运行和执行高度并发的 Java 应用程序 该应用程序使用光纤在内部将
  • 如何从 java.nio.Path 获取路径字符串?

    使用更多信息重写问题 我有一些代码可以创建Path使用相对路径的对象 如下所示 Paths get folder resolve filename 后来 我想从中获取路径字符串 folder filename 我在Windows上 所以有反
  • 对于 Linux 上的 Windows 文件路径,Path.startsWith 返回 false

    为什么会这样呢 Path parent1 Paths get flugel borf noggin Path child1 Paths get flugel borf noggin foo bar baz jpg System out pr
  • 每个 UDP 数据报的 Netty 不同管道

    我们有一个已经在 TCP IP 中实现的服务器 但现在我们要求该协议也支持 UDP 发送的每个 UDP 数据报都包含我需要解码的所有内容 因此这是一个非常简单的回复和响应系统 数据报中的数据由换行符分隔 服务器启动时的引导代码如下所示 SE
  • 如何绕过java.nio.file.DirectoryNotEmptyException? [复制]

    这个问题在这里已经有答案了 有没有办法绕过java nio file DirectoryNotEmptyException 我希望能够删除其中包含内容的文件夹 有没有办法绕过java nio file DirectoryNotEmptyEx
  • Java N/IO 中的行分隔符?

    使用以下命令写入 txt 文件时如何插入新行java nio file 以下代码生成一个只有一行的txt文件ABCDEF 而不是两条单独的线ABC and DEF public static void main String args th
  • 让Java文件传输更高效

    我有两台无线计算机连接到 N 个无线路由器 每台 PC 的连接速度都在 108 150Mbps 之间 理论上 在绝对最佳的条件下 我应该能够以 13 5MB s 到 18 75MB s 的速度传输 第一台计算机 正在发送 使用非常快的 SS
  • 强制停止在外部线程上运行的 Java Files.copy()

    这里的答案似乎是 Java 8 之前的有效解决方案 如何在Java中取消Files copy https stackoverflow com q 17083896 但现在不行了 因为ExtendedCopyOption INTERRUPTI
  • 尽管通道已准备就绪,Java NIO 选择器 select() 返回 0

    我的 Java NIO 选择器是使用实现的select http docs oracle com javase 6 docs api java nio channels Selector html select 28 29所以它会阻塞 直到
  • 即使没有来自客户端的连接,选择器也会无限循环

    我是 Java NIO 的新手 在阅读了一些教程后 我尝试自己编写一个简单的 NIO 服务器和客户端 我的服务器只做了一件简单的事情 就是从客户端监听并打印到控制台 客户端只需连接到服务器并向其发送 3 条消息 Hello 问题是我的服务器
  • WatchService 和 SwingWorker:如何正确执行?

    WatchService 听起来像是一个令人兴奋的想法 不幸的是 它似乎像教程 api 中警告的那样低级 而且并不真正适合 Swing 事件模型 或者我错过了一些明显的东西 非零概率 获取代码来自教程中的 WatchDir 示例 http
  • 我应该如何处理Java中非常大的数组?

    我有一个算法 当前分配一个非常大的双精度数组 它经常更新和搜索 数组的大小为 N 2 2 其中 N 是算法运行的行数 我还必须保留整个内容的副本 以用于与算法周围的应用程序相关的目的 当然 这对我的算法可以处理的行数施加了限制 因为我需要应
  • Java NIO 服务器/客户端聊天应用程序 - 仅通过关闭套接字来发送数据

    朋友们 我是 Java NIO 的新手 目前正在尝试制作一个非阻塞聊天应用程序 客户端连接到服务器没有问题 客户端向服务器写入一条消息或几条消息 但服务器仅在客户端代码关闭 Socket 连接时才开始读取消息 因此必须在客户端代码中为每条消
  • 如何从 Java watchservice 取消注册目录?

    我向我的 watchService 注册了一个文件夹 path register watchService ENTRY CREATE ENTRY DELETE ENTRY MODIFY 后来我想取消这个注册 我知道我需要以某种方式告诉 wa
  • 导致崩溃转储的 Java 错误的解决方法

    我开发的一个程序偶尔会由于这个错误而导致 JVM 崩溃 http bugs java com bugdatabase view bug do bug id 8029516 http bugs java com bugdatabase vie

随机推荐

  • 什么是向上管理?意义?

    今天聊一个职场热词 向上管理 这两天刷到字节跳动的招聘官网 他们在阐述 坦诚清晰 的字节范儿时 提到了一句话 实事求是 暴露问题 反对 向上管理 这一点很有意思 因为 向上管理 几乎是职场入门必修课 所以我试着在字节跳动的文化语境中 来理解
  • 使用gcov+lcov工具可视化代码分支覆盖率数据

    通过一个简单的例子 来展示如何使用gcov lcov来生成代码分支覆盖率数据并将其图形化显示 假设有一个main cpp文件 1 首先使用gcc编译该文件 g main cpp fprofile arcs ftest coverage l
  • CUDA代码笔记(二) cudaOpenMP

    cudaOpenMP项目展示了如何在cuda项目中运用openmp技术 该项目位于cuda samples文件夹下的0 Simple cudaOpenMP文件夹下 在正式开始剖析代码之前 让我们先来了解一下openmp的背景知识 OpenM
  • Docker进阶:mysql 主从复制、redis集群3主3从【扩缩容案例】

    Docker进阶 mysql 主从复制 redis集群3主3从 扩缩容案例 一 Docker常规软件安装 1 1 docker 安装 tomcat 默认最新版 1 2 docker 指定安装 tomcat8 0 1 3 docker 安装
  • WinLicense&Themida 2019革命性升级,软件加密后破解难度突破天际

    随着软件普及程度 互联网技术的发展 以及正版软件购买用户数量和软件版本的增加 软件的保护变得越来越重要 而我们常见的软件保护方式有软件授权和软件加密 Oreans是西班牙非常著名的软件系统保护公司 提供代码混淆 版本控制等多种工具 其中Wi
  • CMake编译QT项目,解决undefined reference to vtable问题

    项目布局应为 src文件夹中存放cpp文件和包含了继承了QObject类的class的头文件 include文件夹中存放其他头文件 在项目文件夹中新建CMakeLists txt文件 新建build文件夹和lib文件夹 此时项目文件夹的结构
  • 通过maven配置不同的开发环境

    前言 项目有开发 测试 生产至少有这三个环境 所需要的配置信息肯定不一样 比如需要开发环境的时候 注解掉测试和生产的配置信息 打开开发的配置信息 后来工作接触到新项目 发现是通过maven来控制加载不同的配置文件 非常方便 所以这篇博客学习
  • 不一样的命令提示符、 PowerShell、windows terminal

    展示图片 怎么操作 1 打开Microsoft Store 软件商店 搜索windows terminal 2 安装即可 3 点击windows 最近添加windows terminal 4 点击设置 gt 默认值 gt 外观 5 滑到最底
  • 爬虫技术可以分析数据吗?

    目前在不少大数据团队中 数据分析和数据挖掘工程师通常都有明确的分工 数据采集往往并不是数据分析和挖掘工程师的任务 通常做爬虫的是大数据应用开发程序员或者是数据采集工程师 使用爬虫工具 的工作任务 但是对于数据分析工程师来说 掌握爬虫技术也是
  • 【RF时序预测】基于随机森林算法的时间序列预测附matlab代码

    作者简介 热爱科研的Matlab仿真开发者 修心和技术同步精进 matlab项目合作可私信 个人主页 Matlab科研工作室 个人信条 格物致知 内容介绍 随机森林算法是一种集成学习方法 通过组合多个决策树来进行分类和回归 算法的原理如下
  • c++ 实现贪吃蛇(含技术难点解析和完整代码)

    文章目录 0 参考资料 1 技术难点 1 1 关于光标的移动 1 2 关于蛇的移动 1 2 1 从键盘上读取输入 1 2 2 蛇的移动 1 3 食物的生成 2 完整代码 0 参考资料 借鉴了这位大佬的博客及代码 在其基础上进行了修改 特此鸣
  • Antd Pro新增表格页面(二)

    前言 承接上一篇博客Ant Design Pro 新增一个表格页面 一 继续对index tsx进行修改 请求相关修改 复制过来的index tsx文件中的请求都是rule的增删查改 将其修改为前文已经写好的接口请求 import rule
  • VUE利用el-upload实现文件上传的功能,后端获取文件数据

    vue点击组件弹出窗口 span class tool btn i class table tool btn add icon i 批量新增 span 在方法中定义一个控制器 export default data return files
  • PTA---C++实现,定义抽象类Person、派生类Student和类Teacher

    问题描述 设计抽象类Person 派生出具体类 学生类Student和教师类Teacher 创建若干不同类对象后并在主方法中测试 数据成员定义 Person ID 姓名 生日 Student 专业 成绩 Teacher 职称 工资 带参构造
  • 贝叶斯定理及其Matlab程序设计

    目录 1 贝叶斯定理介绍 2 贝叶斯定理的Matlab代码实现 3 贝叶斯定理的C语言代码实现 4 贝叶斯定理的应用实例分析 1 贝叶斯定理介绍 贝叶斯定理 Bayes theorem 又称贝叶斯法则或贝氏定理 是概率论与统计学的基本定理之
  • OpenStack企业级实战

    一 环境初始化 1 CentOS7一键安装OpenStack 安装参考 Install RDO 前提条件 准备一个8g或16g内存的centos7 有一块网卡可以访问internet 磁盘 40G cpu分2 4线程 cpu要开启虚拟化 r
  • 串---KMP模式匹配算法之获取next数组

    一 获取模式串T的next数组值 1 回顾 我们所知道的KMP算法next数组的作用 next j 表示当前模式串T的j下标对目标串S的i值失配时 我们应该使用模式串的下标为next j 接着去和目标串失配的i值进行匹配 而KMP算法的ne
  • __attribute__((visibility("default")))

    设置符号可见性 vis c include
  • python创建可以迭代的类

    python中可以使用 for in 进行遍历 迭代 的数据类型 都是可迭代的对象 如 列表 字符串 字典 元组 都是可迭代的 Iterable 而浮点数 整型 布尔值都是不可迭代 for i in 1 2 3 4 print i for
  • NIO效率高的原理之零拷贝与直接内存映射

    前言 在笔者上一篇博客 详解了NIO 并总结NIO相比BIO的效率要高的三个原因 点击查看 这篇博客将针对第三个原因 进行更详细的讲解 首先澄清 零拷贝与内存直接映射并不是Java中独有的概念 并且这两个技术并不是等价的 零拷贝 零拷贝是指