直接内存
定义
直接内存(Direct Memory):直接内存是系统的直接内存,常见于NIO操作时,用于数据缓冲区。分配回收成本较高,但读写性能高,不受JVM内存回收管理。
下面,我们说一下啊为什么使用直接内存读写的性能会比较高。请看下图:
这里我们知道,我们的Java是运行在我们的虚拟机中的,通过虚拟机命令再去间接调用系统的内核函数,即我们上面看到的用户态跟内核态。(可以简单想象成,Java代码运行后调用本地方法区的代码,然后本地方法区的代码调用系统的内核函数在CPU上运行,CPU运行完再把结果返回给虚拟机)。而内存呢,我们也不是直接读取的,使用虚拟机我们需要让系统内存先读取我们的磁盘文件并缓存到系统内存中,然后虚拟机再分配对应的空间缓存来接受我们系统缓存区的数据。如此,我们的数据其实就需要缓存两份(一份在系统内存,一份在Java堆内存),自然就比较慢了。
但是,如果我们使用直接内存的话: 我们开辟的直接内存空间,是系统内存和Java堆内存公用的内存区域,读取磁盘文件后,Java堆内存可以直接访问。
直接内存的基本使用
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class Demo1_9 {
static final String FROM = "D:\\QLDownload\\完美世界\\video.qlv";
static final String TO = "D:\\Download";
static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
directBuffer();
io();
}
传统读取文件的写法如下:
private static void io(){
long start = System.nanoTime();
try(FileInputStream in = new FileInputStream(FROM);
FileOutputStream out = new FileOutputStream(TO)){
byte[] buf = new byte[_1MB];
while (true){
int len = in.read(buf);
if(len == -1){
break;
}
out.write(buf, 0, len);
}
}catch (IOException e){
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io用时:"+ (end - start) / 1000000.0);
}
使用直接内存的写法如下:
private static void directBuffer(){
long start = System.nanoTime();
try(FileInputStream in = new FileInputStream(FROM);
FileOutputStream out = new FileOutputStream(TO)){
ByteBuffer buf = ByteBuffer.allocateDirect(_1MB);
while (true){
int len = in.read(buf);
if(len == -1){
break;
}
buf.flip();
out.write(buf, 0, len);
buf.clear();
}
}catch (IOException e){
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io用时:"+ (end - start) / 1000000.0);
}
内存溢出
前面我们说到了,直接内存不受JVM内存回收管理。那么,他会不会发生内存溢出呢?答案是肯定的。我们可以演示一下: 我们在while循环中每次开辟100M的内存空间,并将其添加到List集合中,防止垃圾回收。 运行结果如下: 可以看到,运行36次后,程序就报错了,报错信息也直接告诉我们问题出在我们的直接缓冲内存中。
直接内存的释放及其原理
这里,我们演示一下: 这里,我们分配1G的直接内存,等分配完成后,我们让他等一下(使用System.in.read()读取键盘的操作)。然后再将byteBuffer的内存置空,并进行垃圾回收。回收了,为了方便观察,我们继续让他等一下。 OK,我们看一下我们的运行结果: 这里,因为我们的直接内存不属于Java虚拟机内存,所以我们得借助任务管理器来查看: 未运行程序,我们的IDEA大概占用1G的内存。 然后我们运行程序且等到控制台输出分配完毕…,我们再看一下我们的任务管理器: 此时我们的IDEA占用了大概2G的内存(其中1G是我们刚才运行的程序占用的): 在控制台输入回车后且控制台输出开始释放…后,我们再看一下任务管理器: 可以看到,直接内存被成功释放了。有人肯定会说了,说好的不受JVM内存回收管理呢?不急,我们来看一下他内部回收的原理:
我们借助IDEA工具来看一下,按住ctrl后点击byteBuffer的allocateDirect方法,我们来看一下源码: 他返回的是一个DirectByteBuffer对象。我们再看一下这个类的构造器:
这里,其实他是用了unsafe对象来请求我们的内存空间,而后面又通过Cleaner类(Cleaner类是JDK的一个虚引用类型,他的特点是,当他所关联的对象被回收的时候,Cleaner会触发虚引用的create方法。)来关联我们的DirectByteBuffer对象,当该对象被回收的时候,我们就调用后面的new Deallocator(base, size, cap)线程的run()方法。我们可以看一下clean()的源码:
接着,我们可以按住ctrl,回到前面看一下Deallocator类构造器方法。 这里可以看到,最后其实是通过调用unfase对象的freeMemory方法来主动释放直接内存的。
总结一下: 1、Java对直接内存的使用和回收,底层其实都是使用了Unsafe对象完成,且回收的时候,我们需要主动使用Unsafe对象调用freeMemory方法。 2、ByteBuffer的实现类内部,使用了Cleaner来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放内存。
做JVM调优的时候,我们经常会使用虚拟机命令
-XX:+DisableExplicitGC
禁止使用 System.gc() 方法显示地进行垃圾回收,因为他会回收新生代和老生代的资源,所以比较费时。此时,我们以下的代码将无法实现对直接内存的释放。 如此,我们就必须使用我们的Unsafe对象主动调用freeMemory()方法来释放直接内存,而我们的Unsafe对象也无法直接使用,必须要使用Java8的反射机制来调用这个对象。即,最后我们的代码应该改为:
|