1.定义
不是虚拟机的内存,是系统内存。Direct Memory
- 常见于NIO操作时,用于数据缓存区
- 分配回收成本过高,但读写性能高
- 不受JVM内存回收管理
2.基本使用
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 = "E:\\b.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
}
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);
}
}
2.1.Java操作磁盘文件
当java
读取磁盘文件时,会从用户态切换到内核态,才能去操作系统内存。读取时,系统内存先开辟一块缓存空间,磁盘文件分块读取。然后java虚拟机内存再开辟缓存空间new Byte[]
来读取系统内存的文件。由于有从系统内存读取到java虚拟机的内存,所以效率较低。
2.2.NIO操作磁盘文件
读取磁盘文件时,会有一块直接内存,Java
虚拟机和视同内存都能访问使用,所以效率更高。
2.3.内存溢出
/**
* 演示直接内存溢出
*/
public class Demo1_10 {
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);
}
// 方法区是jvm规范, jdk6 中对方法区的实现称为永久代
// jdk8 对方法区的实现称为元空间
}
}
2.4.分配和释放原理
- 使用了
Unsafe
对象完成直接内存的分配回收,并且回收需要主动调用freeMemory
方法。 ByteBuffer
的实现类内部,使用了Cleaner
(虚引用)来检测ByteBuffer
对象,一旦ByteBuffer
对象被垃圾回收,那么就会由ReferenceHandler
线程通过Cleaner的clean方法调用freeMemory
来释放直接内存。
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
/*
* -XX:+DisableExplicitGC 显式的
*/
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();
}
}
- 直接内存不受jvm内存管理,但是这段代码示例中,当
byteBuffer
= null,执行垃圾回收后,直接内存却被释放了。这是因为跟jdk
中的一个类Unsafe
有关。 -XX:+DisableExplicitGC
禁用显式的垃圾回收。只有等到jvm自己进行垃圾回收才会回收。
/**
* 直接内存分配的底层原理:Unsafe
*/
public class Demo1_27 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
Unsafe
类调用其方法freeMemory
来释放了直接内存。
ByteBuffer源码
ByteBuffer
分配直接内存是通过new
了一个DirectByteBuffer
在DirectByteBuffer
内部可以看到,确实是用Unsafe
类去分配内存。
这里调用了一个Cleaner
的create
方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer
)被回收以后,就会调用Cleaner
的clean
方法,来清除直接内存中占用的内存
DirectByteBuffer
的run
方法释放了直接内存。
3.直接内存的回收机制总结
使用了 Unsafe
对象完成直接内存的分配回收,并且回收需要主动调用 freeMemory
方法
ByteBuffer
的实现类内部,使用了 Cleaner
(虚引用)来监测 ByteBuffer
对象,一旦 ByteBuffer 对象被垃圾回收,那么就会由 ReferenceHandler
线程通过 Cleaner
的 clean
方法调 用 freeMemory
来释放直接内存