1.直接内存概述
它不能存在于JVM内存结构中
属于操作系统,常见于NIO操作时,用于数据缓冲区
bytebuffer所使用的内存就是直接内存
分配回收成本较高,但读写性能高
不受JVM内存回收管理
通过以下程序测试直接内存的读写性能
/**
* 演示 ByteBuffer 作用
*/
public class Demo1_9 {
//文件大概200M
static final String FROM = "E:\\编程资料\\第三方教学视频\\youtube\\Getting Started with Spring Boot-sbPSjI4tt10.mp4";
static final String TO = "E:\\a.mp4";
static final int _1Mb = 1024 * 1024; // //读写缓冲区代码都使用1M
public static void main(String[] args) {
io(); // io 用时:1535.586957 1766.963399 1359.240226
directBuffer(); // directBuffer 用时:479.295165 702.291454 562.56592
}
//使用by
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.基本使用
为了使用直接内存,文件读写效率会大大提高。
Java要想读写磁盘,就需要调用操作系统提供的函数,也就是之前native函数。CPU状态会从用户态切换到内核态。当切换到内核态之后,CPU函数就能读取磁盘文件的内容了。内核态的时候,会在操作系统内核中划出一块缓冲区,称之为系统缓存区。分次进行读取。此时java代码不能够访问,Java又会在堆内存中划分一块java缓冲区,**然后再从系统缓存区中读取到Java缓冲区,**然后调用输入输出流的读写操作进行读写。
此时有两块缓冲区。这样就造成了不必要的数据复制。
direct memory:当我们调用bytebuffer的allocateDirect方法时,就会分配一块直接内存,我们会在操作系统这边划出一块缓冲区,与之前不一样的是,操作系统划出的这块内存,java代码可以直接访问。对两端内存都是共享的。比之前少了一次缓冲区的复制操作,文件读取效率得到了大的提升。
直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率
3.内存溢出
direct memory不受Java虚拟机回收管理,不会去释放directmemory所占用的内存。
public class Demo1_10 {
static int _100Mb = 1024 * 1024 * 100; //每次分配100M的内存
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 对方法区的实现称为元空间
}
}
循环了17次,报了Java.lang.OutOfMemoryError:Direct buffer memory错误
4.释放原理
直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放
通过
//通过ByteBuffer申请1M的直接内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);
申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?
allocateDirect的实现
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer类
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)); //通过虚引用,来实现直接内存的释放,this为虚引用的实际对象
att = null;
}
这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer)被回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存
public void clean() {
if (remove(this)) {
try {
this.thunk.run(); //调用run方法
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
对应对象的run方法
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address); //释放直接内存中占用的内存
address = 0;
Bits.unreserveMemory(size, capacity);
}
直接内存的回收机制总结:
使用了Unsafe类来完成直接内存的分配回收,回收需要主动调用freeMemory方法
ByteBuffer的实现内部使用了Cleaner(虚引用)来检测ByteBuffer。一旦ByteBuffer被垃圾回收,那么会由ReferenceHandler来调用Cleaner的clean方法调用freeMemory来释放内存