直接内存 Direct Memory
直接内存不是JVM里的内存,而是操作系统里的内存。
(1)常见于NIO操作时,用于数据缓冲区(比如ByteBuffer使用的是直接内存)
(2)分配、回收成本较高,但读写性能高
// 演示ByteBuffer作用
public class Demo {
static final String FORM = "D:\\asd\\asd.mp4";// 选的是比较大的文件,比如200多兆
static final String TO = "D:\\asd.mp4";
static final int _1Mb = 1024 * 1024;
public static void main(String[] args) {
io(); // io 用时:3187.41008(大概用了3秒),多跑几遍,多比较,跑一次不算。
derectBuffer();// directBuffer 用时:951.114625(不到1秒)
}
private static void deirectBuffer() {
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();
print("directBuffer用时:" + (end - start) / 1000_000.0);
}
// 用传统的io方式做文件的读写
private static void io() {
long start = System.nanoTime();
try ( // 网友1:写到try()括号里就不用手动close了
FileInputStream from = new FileInputStream(FROM);
FileOutPutStream to = new FileOutputStream(TO);
) {
byte[] buf = new byte[_1Mb];// byte数组缓冲区(与上面的读写缓冲区设置大小一致,比较时公平)
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();
print("io用时:" + (end - start) / 1000_000.0);
}
}
运行并比较时间后可以发现尤其是读写大文件时使用ByteBuffer的读写性能非常高。
文件的读写过程
用传统的方式
Java本身不具备磁盘的读写能力,他要想实现磁盘读写,必须调用操作系统提供的函数(即本地方法)。在这里CPU的状态改变从用户态(Java)切换到内核态(system)【调用系统提供的函数后】。
内存这边也会有一些相关的操作,当切换到内核态以后,他就可以由CPU的函数,去真正读取磁盘文件的内容,在内核状态时,读取内容后,他会在操作系统内存中划出一块儿缓冲区,其称之为系统缓冲区,磁盘的内容先读入到系统缓冲区中(分次进行读取),系统的缓冲区Java代码是不能够运行的,所以Java在堆内存中分配一块儿Java的缓冲区,即代码中的new byte[大小],我们Java的代码要能访问到刚才读取的那个流中的数据,必须再从系统缓冲区的数据间接再读入到Java缓冲区,然后CPU的状态又切换到用户态了,然后再去调用Java的那个输出流的写入操作,就这样反复进行读写读写,把整个文件复制到目标位置。
在这里可以发现,由于有两块儿内存,两块儿缓冲区,即系统内存和Java堆内存都有缓冲区,那读取的时候必然涉及到这数据存两份,第一次先读到系统缓冲区还不行,因为Java代码访问不到他们,所以把系统缓冲区数据再读入到Java缓冲区中,这样就造成了一种不必要的数据的复制,效率因而不是很高。
用directBuffer时的过程
当ByteBuffer把allocateDirect这个方法调用以后,意味着我会在操作系统这边划出一块缓冲区,即direct memory,这段区域与之前不一样的地方在于这个操作系统划出来的内存Java代码可以直接访问,即系统可以同他,Java代码也可以访问他,即他对两对代码都是可以共享的一段内存区域,这就是直接内存。
即磁盘文件读到直接内存后,Java代码直接访问直接内存,比刚才的传统代码少了一次缓冲区里的复制操作,所以速度得到了成倍的提高。这也是直接内存带来的好处,他适合做文件的这种io操作。(更多的相关优化可以参考NIO更多相关知识)
(3)不受JVM内存回收管理(直接内存的分配和释放是Java会通过UnSafe对象来管理的)
既然这样,直接内存会不会存在内存泄露问题呢?
// 演示直接内存溢出
public class Demo {
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);// 每次分配100兆内存
list.add(byteBuffer);// 把这玩意放到List中,一直循环
i++;
}
}finally {
print(i);
}
}
}
运行后,输出36。即循环36次(一次100兆,循环36次也算3个G多了)后,爆出直接内存溢出异常。
Exception in thread “main” java.lang.OutOfMemoryError: Direct buffer memory
直接内存分配和回收原理
- 使用了 Unsafe对象完成直接内存的分配、回收,并且回收需要主动调用freeMemory方法
- ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被(Java)垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的clean方法调用freeMemory来释放直接内存。
public class Demo {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
print("分配完毕");
print("开始释放");
byteBuffer = null;
System.gc();// 显式的垃圾回收
}
}
这里的System.gc()触发的是一次Full GC,是比较影响性能的垃圾回收 ,不光要回收新生代,还要回收老年代,所以他造成的程序暂停时间比较长。
所以为了防止一些程序员不小心在自己代码里经常写这个System.gc()以触发显式的垃圾回收,做一些JVM调优时经常会加上 -XX:+DisableExplicitGC
这个虚拟机参数,禁用这种显式的垃圾回收,也就是让你这行System.gc()代码无效,达到这种效果的。
但是加上这个虚拟机参数后,可能会影响到直接内存的回收机制。所以,加参数之后,再运行上面的代码时,可以发现并不会触发Java的垃圾回收,所以创建的直接内存1G也是一致存在着(没加虚拟机参数的话,触发Java垃圾回收时,由于byteBuffer被null了,所以也导致了直接内存也被释放掉)。
由于Java垃圾回收没做,虽然byteBuffer被null了,但由于内存比较充足,所以他还存活着。既然他存活着,他所对应的那块儿直接内存(ByteBuffer.allocateDirect(-1Gb))也没有被回收掉,windows从任务管理器
就能看得出(运行上述代码后,任务管理器就出现一个Java进程,这里分配了1G,所以那个进程占用内存也1G)。
所以禁用System.gc()之后,会发现别的代码不受太大影响,但直接内存会受到影响,因为我们不能用显示的方法回收掉Bytebuffer,所以ByteBuffer只能等到真正的垃圾回收时,才会被清理,从而他所对应的那块儿直接内存也会被清理掉。
所以这就造成了直接内存可能占用较大,长时间得不到释放这样一个现象。所以直接内存使用情况比较多的时候,对直接内存的管理方式是,释放直接内存时,可以直接调用Unsafe对象
的freeMemory
方法,所以最终还是程序员手动的管理直接内存,所以推荐用Unsafe的相关方法。
ByteBuffer底层分配和释放直接内存的大概情况:
调用Unsafe对象
的allocateMemory(_1Gb)方法
分配直接内存,返回long base,即内存地址
,然后调用unsafe对象的freeMemory(base)
释放直接内存。(据说不建议普通程序员使用Unsafe类)