【NIO】直接内存 与 零拷贝 详解

shadowLogo

NIO 之所以 读写效率高主要原因 就在于 其可以操作 直接内存

直接内存:

首先,本人来详细介绍下,直接内存 是什么:

概念:

直接内存(Direct Memory)
不是 虚拟机运行时数据区 的一部分,也不是 Java虚拟机规范中定义的内存区域
某些情况下这部分内存也会被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
Java里用 DirectByteBuffer 可以分配一块 直接内存(堆外内存)元空间 对应的内存也叫作 直接内存,它们对应的都是 机器的物理内存

如下图:
直接内存
我们可以看到:

直接内存,只是在 虚拟机内存 中,创建了一个指针引用
而 指针所指的 实例数据存储 却在 虚拟机内存之外 的 本机物理内存


而我们看到 “直接内存” 这个名词,就应该能猜得到:

Java 可以 存放/读取 到的 对象实例数据存储空间,可以分为两类:

  • 直接内存
  • 非直接内存(JVM内存,即:堆内存)
    其实 JVM中,存储对象数据,在不发生“逃逸”的情况下,会进行 “栈上分配”,也就是说:对象实例数据 也能存储在 虚拟机栈 上,
    但是这种对象生命只在当前方法中存在,朝生夕死,所以我们一般说的 JVM内存,就是指 堆内存

那么,直接内存非直接内存,有什么 区别 呢?

直接内存 与 非直接内存 的区别:

实例数据 存储空间 方面:

  • 直接内存 是将实例数据存储在 本机物理内存
  • 非直接内存 是将实例数据存储在 JVM内存

如下图:
非直接
我们可以看到:

非直接内存 的 访问 需要 二次拷贝
堆内存 => 直接内存 => 系统调用 => 硬盘/网卡

直接
我们可以看到:

直接内存不需要进行 数据的拷贝:
直接内存 => 系统调用 => 硬盘/网卡


那么,本人现在来测试下 直接内存非直接内存空间申请性能空间访问性能

申请、访问性能 方面:

首先是 空间申请性能

package edu.youzg.demo;

import java.nio.ByteBuffer;

public class DirectMemoryTest {

    /**
     * 测试 非直接内存(堆内存) 的申请效率
     */
    public static void heapAllocate() {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            ByteBuffer.allocate(100);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("堆内存申请,耗时:" + (endTime - startTime) + "ms");
    }

    /**
     * 测试 直接内存 的申请效率
     */
    public static void directAllocate() {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            ByteBuffer.allocateDirect(100);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("直接内存申请,耗时:" + (endTime - startTime) + "ms");
    }

    public static void main(String args[]) {
        /*
            测试并比较 直接内存和非直接内存 的 申请效率
         */
        for (int i = 0; i < 10; i++) {
            heapAllocate();
            directAllocate();
        }
    }
    
}

本人现在来展示下 运行结果
申请
我们可以看到:

直接内存 的 内存申请性能,比 堆内存 低很多


接下来是 空间访问性能

package edu.youzg.demo;

import java.nio.ByteBuffer;

public class DirectMemoryTest {

    /**
     * 测试 非直接内存(堆内存) 的访问效率
     */
    public static void heapAccess() {
        long startTime = System.currentTimeMillis();
        //分配堆内存
        ByteBuffer buffer = ByteBuffer.allocate(1000);
        for (int i = 0; i < 100000; i++) {
            for (int j = 0; j < 200; j++) {
                buffer.putInt(j);
            }
            buffer.flip();
            for (int j = 0; j < 200; j++) {
                buffer.getInt();
            }
            buffer.clear();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("堆内存访问,耗时:" + (endTime - startTime) + "ms");
    }

    /**
     * 测试 直接内存 的访问效率
     */
    public static void directAccess() {
        long startTime = System.currentTimeMillis();
        //分配直接内存
        ByteBuffer buffer = ByteBuffer.allocateDirect(1000);
        for (int i = 0; i < 100000; i++) {
            for (int j = 0; j < 200; j++) {
                buffer.putInt(j);
            }
            buffer.flip();
            for (int j = 0; j < 200; j++) {
                buffer.getInt();
            }
            buffer.clear();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("直接内存访问,耗时:" + (endTime - startTime) + "ms");
    }

    public static void main(String args[]) {
        /*
            测试并比较 直接内存和非直接内存 的 访问效率
         */
        for (int i = 0; i < 10; i++) {
            heapAccess();
            directAccess();
        }
    }
}

那么,本人现在来展示下 运行结果
运行结果
我们可以看到:

直接内存 的 内存访问性能,比 堆内存 高很多


经过了上文的讲述,相信同学们对于 直接内存 也有了一定的理解
那么,本人在这里,来总结下 直接内存非直接内存 相比,有什么 优缺点

优缺点:

优点:

  • 不占用堆内存空间,减少了发生GC的可能
  • java虚拟机实现上,本地IO 会直接操作 直接内存直接内存=>系统调用=>硬盘/网卡),
    非直接内存 则需要 二次拷贝堆内存=>直接内存=>系统调用=>硬盘/网卡

缺点:

  • 初始分配
  • 没有 JVM直接 帮助管理内存,容易发生 OOM
    为了避免一直没有FULL GC,最终导致直接内存把物理内存耗完。
    我们可以指定直接内存的最大值,通过-XX:MaxDirectMemorySize来指定,
    当达到阈值的时候,调用system.gc来进行一次FULL GC,间接把那些没有被使用的直接内存回收掉。

讲完了 直接内存 的相关知识,相信细心的同学已经发现本人在上文的比较中,已经讲到了 非直接内存 的 数据访问 需要 “拷贝”

零拷贝直接内存 的一个 显著特征
那么,本人现在就来细说下 零拷贝 的 原理:

零拷贝:

非直接内存(堆内存):

堆内存
从上图中,我们可以看出,一次数据的读写操作,需要经过 如下步骤

  1. 数据 从 磁盘/网卡 拷贝到 内核空间,再从 内核空间 拷贝到 用户空间(JVM)
  2. 程序 可能进行 数据修改 等操作
  3. 再将 处理后的数据 拷贝到 内核空间内核空间 再拷贝到 磁盘/网卡,通过网络发送出去(或拷贝到磁盘)

从上面的分析,我们也能看出:

一次 数据的读写(用户空间 发到 网络 也算作 ),都 至少 需要 两次拷贝至多 需要 四次拷贝

在这期间,由于 涉及到 用户空间内核空间 的操作,因此需要涉及到 用户态内核态 的相互切换

而根据上述的过程描述,我们也能了解,总共经历了 4次权限切换
非直接切换


接下来,本人来讲解下 使用 直接内存,和上文中所讲解的 非直接内存 的过程有什么 区别

直接内存:

直接内存
从上图中,我们可以看出,一次数据的读写操作,需要经过 如下步骤

  1. 数据 从 磁盘/网卡 拷贝到 直接内存
  2. 程序 可能进行 数据修改 等操作,直接操作 直接内存 中的对象实例
  3. 再将 处理后的数据,从 直接内存 拷贝到 磁盘/网卡

而根据上述的过程描述,我们也能了解,总共经历了 2次权限切换
直接切换


总结:

在看完了上文所讲的内容之后,我们也能发现:

  • 零拷贝直接内存特征直接内存零拷贝实现原理
  • 零拷贝不是 代表 不进行拷贝
    而是 少了 用户空间内核空间 之间的拷贝,但是 数据从 直接内存磁盘/网卡 的 拷贝确是 少不了的

搜嘎

posted @ 2021-05-02 10:32  在下右转,有何贵干  阅读(1050)  评论(0编辑  收藏  举报