直接内存(Direct Memory)
特点
-
直接内存并不属于jvm的内存管理,而是属于操作系统内存。
-
常见于NIO操作,用于数据缓冲区,分配回收成本高,但是读写性能高
-
通过DirectByteBuffer对象操作Native内存,访问速度优于Java堆
-
不受jvm垃圾回收管理
基本使用
演示一下分别使用ByteBuffer和阻塞io拷贝文件
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 演示 ByteBuffer 作用
*/
public class Demo1_9 {
static final String FROM = "D:\\Files\\科大讯飞开发套件客户附送资料V2.0_20220826\\3.开发套件关键传感器使用资料\\M1系列麦克风阵列(M160C)\\1.ROS语音功能包视频教程\\语音视频教程4:ROS功能包功能详解.mp4";
static final String TO = "D:\\Files\\科大讯飞开发套件客户附送资料V2.0_20220826\\3.开发套件关键传感器使用资料\\M1系列麦克风阵列(M160C)\\1.ROS语音功能包视频教程\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用时:147.2171
directBuffer(); // directBuffer 用时:73.9831
}
private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel();
) {
ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(bb);
if (len == -1) {
break;
}
bb.flip();
to.write(bb);
bb.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时:" + (end - start) / 1000_000.0);
}
private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb]; //分配缓冲区
while (true) {
int len = from.read(buf);
if (len == -1) {
break;
}
to.write(buf, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时:" + (end - start) / 1000_000.0);
}
}
从运行结果我们可以看到ByteBuffer的效率是阻塞io的两倍
原理
那么为什么ByteBuffer在读写大文件的时候比io效率高呢
我们先来看一下io的读写过程,java本身不具备读写能力,要进行磁盘读写的话必须调用操作系统提供的函数(Native方法),此时CPU的运行状态会从用户态切换到内核态,CPU会先将磁盘文件的内容读到操作系统内存中的系统缓冲区,java无法直接操作操作系统的缓冲区,因此会在堆内存中先分配一段java缓冲区,对应代码中 “ byte[] buf = new byte[_1Mb]; ”部分,最后再从系统缓冲区把数据读到Java缓冲区,这样就造成了不必要的数据复制,因此效率很低。
使用DirectBuffer对象后,当代码运行到“ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb);“时,会在操作系统内存中划分一段直接内存,跟之前不一样的地方在于这段内存Java可以直接访问(操作系统和java都可以直接访问),CPU会将磁盘文件直接读到直接内存,java直接从直接内存中取数据,跟普通io相比少了一次缓冲区的复制操作,因此速度得到了成倍的提升。
内存溢出
前面说到直接内存的特点是不受jvm垃圾回收管理,jvm垃圾回收时不会直接释放直接内存(划重点!是不会直接释放!下节会解释!),那么直接内存会不会出现内存溢出呢,答案是会的。下面代码我们每次分配100M直接内存,并且为了长时间占用把它加入到生命周期更长的一个list中,一直循环。
public class outOfmMemory{
static int _100Mb = 1024 * 1024 * 100;
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} finally {
System.out.println(i);
}
}
}
报错:
内存释放
先来演示一下直接内存的内存释放,为了效果明显,我们直接分配1GB直接内存
import java.io.IOException;
import java.nio.ByteBuffer;
public class releaseMemory {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
System.in.read();
}
}
可以看到分配内存后,
释放内存之后
那是不是垃圾回收把直接内存释放掉了呢,
不是的!
直接内存的分配和释放是通过unsafe对象来管理的,接下来我们从源码的角度分析一下
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
可以看到在DirectByteBuffer的构造器里调用了“unsafe对象的allocateMemory(size)”方法实现了对直接内存的分配。
注意看这行代码:
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
cleaner是一个虚引用类型,它的特点是当他所关联的对象被回收时,会触发clean方法。会调用new Deallocator(base, size, cap))
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
可以看到“unsafe.freeMemory(address);”,unsafe对象主动调用freeMemory释放了直接内存。
因此在内存释放代码中,并不是垃圾回收直接释放掉了直接内存,而是,垃圾回收回收了byteBuffer对象,触发了clean方法,unsafe对象调用了freeMemory()方法导致了直接内存的释放!