一、定义
Direct Memory 直接内存,是属于系统内存的。
- 常见于NIO操作时,用于数据缓冲区。
- 分配回收成本较高,但读写性能高。
- 不受JVM内存回收管理。
二、使用直接内存的优点
下面这段代码展示了不使用直接内存(io())与使用直接内存(directBuffer())在耗时上的区别。
public class DirectMemoryDemo {
static final String FROM = "D:\\BaiduNetdiskDownload\\AutoCAD 2020\\AutoCAD_2020_Simplified_Chinese_Win_64bit_dlm.sfx";
static final String TO = "D:\\documents";
static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
//io 用时 1535.586957
io();
//directBuffer 用时 479.295165
directBuffer();
}
private static void io() {
//获取正在运行的Java虚拟机的高分辨率时间源的当前值,单位为纳秒。1000000纳秒 = 1000微秒 = 1毫秒
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM); //FileInputStream从文件系统中的文件中获取输入字节。
FileOutputStream to = new FileOutputStream(TO); //FileOutputStream是用于将字节数据写入File 或 FileDescriptor。
) {
//初始化字节数组,大小是1M
byte[] buf = new byte[_1MB];
while (true) {
// 将from输入流中len个长度字节的数据读取到字节数组中。
// 此方法会阻塞,直到有可用的输入为止。
int len = from.read(buf);
// -1表示已到达文件末尾而没有更多数据。
if (len == -1) {
break;
}
//将从偏移量off开始的指定字节数组中的len个字节写入此文件输出流。
to.write(buf, 0 , len);
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
//由操作系统分配大小为1M的直接内存区域
ByteBuffer bb = ByteBuffer.allocateDirect(_1MB);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
// 翻转缓冲区。
// 在一系列 channel-read 或 put 操作之后,调用此方法为一系列channel-write 或 relative get 操作做准备。
bb.flip();
to.write(bb);
bb.clear();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
经过测试发现,io()用时1535.586957s,directBuffer()用时479.295165s。可以看到,使用直接内存可以使接口性能提高好几倍。
为什么使用直接内存后,接口响应会变得这么快呢?
结合下面的两张图给大家分析一下。
第一张图展示了不使用直接内存时文件的读写过程。
- JDK本身不具备磁盘读写的能力,需要调用操作系统提供的native方法。当需要调用操作系统的Native方法时,CPU的运行状态由用户态切换到内核态。
- CPU切换到内核态后,开始准备读取磁盘文件。首先将文件内容读取到在系统内存中开辟的系统缓存区中,然后再从系统缓存区中读取数据存入到Java堆内存的Java缓冲区中。
- CPU再由内核态切换到用户态后,Java程序就可以读取Java缓冲区中的数据了。
第二张图展示了使用直接内存时文件的读写过程。
- 由操作系统分配的直接内存区域,系统接口和Java程序都可以直接访问。
- CPU切换到内核态后,由CPU函数开始读取磁盘文件存放到直接内存区域。
- CPU再由内核态切换到用户态后,Java程序就可以读取直接内存区域中的数据了。
总结
使用直接内存时,数据只需要读取一次就可以,与不使用直接内存相比,少了一次读取过程,占用空间少,而且读写性能高。
三、直接内存-内存溢出
内存溢出错误:java.lang.OutOfMemoryError: Direct buffer memory.
四、直接内存-分配及回收原理
既然直接内存不受JVM内存回收管理,那么直接内存如何回收呢?
DirectByteBuffer(int cap) {
// 此处省略代码
// base:直接内存的首地址
long base = 0;
//分配直接内存
base = UNSAFE.allocateMemory(size);
UNSAFE.setMemory(base, size, (byte) 0);
// 此处省略代码
// Cleaner 继承了PhantomReference(虚引用),虚引用对象的特点是:当它关联的对象(DirectByteBuffer)被回收时,会触发Cleaner对象的clean()方法。
// Deallocator 实现了Runnable,
cleaner = Cleaner.create(this, new DirectByteBuffer.Deallocator(base, size, cap));
}
// 实现了Runnable接口,run()中释放内存
private static class Deallocator implements Runnable
{
/**
* 释放直接内存
*/
public void run() {
// address:直接内存的起始地址
UNSAFE.freeMemory(address);
address = 0;
// size:直接内存的大小
// capacity:初始容量
Bits.unreserveMemory(size, capacity);
}
}
/**
* 调用Deallocator.run()方法,释放直接内存。
* 此方法不在主线程中执行,后台有一个ReferenceHandler线程,专门监控虚引用对象关联的对象,一旦关联对象被回收,就会调用此方法。
*/
public void clean() {
// thunk就是Deallocator
thunk.run();
}
上述代码展示了,底层如何分配以及释放直接内存。
- 调用Unsafe.allocateMemory方法、setMemory方法分配直接内存。
- ByteBuffer的实现类内部,使用了Cleaner对象,它是一个虚引用对象,其特点是:当它关联的对象(DirectByteBuffer)被回收时,后台的ReferenceHandler线程(守护线程)会触发Cleaner对象的clean()方法,clean()方法中调用了Deallocator的run()。
- Deallocator实现了Runnable接口,在run()中实现了释放直接内存,调用Unsafe.freeMemory方法。
五、禁用显式垃圾回收
当在代码中执行System.gc()时,就会执行一次Full GC,这个回收会阻塞线程的执行并且比较耗时。
为了使System.gc()无效执行,可以设置禁用显式垃圾回收。
-XX:+DisableExplicitGC //禁用显式垃圾回收
禁用显式垃圾回收后,如果直接内存一直没有被回收,可以显式调用unsafe.freeMemory方法。