【NIO】直接内存 与 零拷贝 详解
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,间接把那些没有被使用的直接内存回收掉。
讲完了 直接内存
的相关知识,相信细心的同学已经发现本人在上文的比较中,已经讲到了 非直接内存 的 数据访问 需要 “拷贝”
零拷贝
是 直接内存
的一个 显著特征
那么,本人现在就来细说下 零拷贝
的 原理:
零拷贝:
非直接内存(堆内存):
从上图中,我们可以看出,一次数据的读写操作,需要经过 如下步骤:
- 数据 从
磁盘/网卡
拷贝到内核空间
,再从内核空间
拷贝到用户空间(JVM)
- 程序 可能进行 数据修改 等操作
- 再将 处理后的数据 拷贝到
内核空间
,内核空间
再拷贝到磁盘/网卡
,通过网络发送出去(或拷贝到磁盘)
从上面的分析,我们也能看出:
一次 数据的读写(用户空间 发到 网络 也算作 写),都 至少 需要 两次拷贝,至多 需要 四次拷贝
在这期间,由于 涉及到 用户空间 和 内核空间 的操作,因此需要涉及到 用户态
和 内核态
的相互切换
而根据上述的过程描述,我们也能了解,总共经历了 4次权限切换:
接下来,本人来讲解下 使用 直接内存
,和上文中所讲解的 非直接内存
的过程有什么 区别:
直接内存:
从上图中,我们可以看出,一次数据的读写操作,需要经过 如下步骤:
- 数据 从
磁盘/网卡
拷贝到直接内存
- 程序 可能进行 数据修改 等操作,直接操作
直接内存
中的对象实例- 再将 处理后的数据,从
直接内存
拷贝到磁盘/网卡
而根据上述的过程描述,我们也能了解,总共经历了 2次权限切换:
总结:
在看完了上文所讲的内容之后,我们也能发现:
零拷贝
是直接内存
的 特征,直接内存
是零拷贝
的 实现原理零拷贝
并 不是 代表 不进行拷贝
而是 少了 用户空间 和 内核空间 之间的拷贝,但是 数据从直接内存
与磁盘/网卡
的 拷贝确是 少不了的