直接内存
直接内存并不是JVM的内存结构,直接内存是操作系统的内存,Java本身并不能对操作系统的内存进行操作,而是通过调用本地方法。直接内存常用于NIO作为缓冲区存在,分配成本较高但是读写性能好,并且不受JVM内存回收管理
NIO与IO的区别
public class demo5 {private static final String From = "下载文件路径";private static final String TO = "保存文件路径";private static final int _1MB = 1024 * 1024;public static void main(String[] args) {io();directBuffer();}public static void io() {long start = System.nanoTime();//开始时间byte[] buf = new byte[_1MB];try {FileInputStream inputStream = new FileInputStream(From);FileOutputStream outputStream = new FileOutputStream(TO);while (true) {int len = inputStream.read(buf);if (len == -1) {break;}outputStream.write(buf);}} catch (Exception e) {e.printStackTrace();}long end = System.nanoTime();System.out.println(end - start);}//NIOpublic static void directBuffer() {long start = System.nanoTime();//开始时间try (FileChannel channel = new FileInputStream(From).getChannel();FileChannel to = new FileOutputStream(TO).getChannel()) {ByteBuffer buf = ByteBuffer.allocateDirect(_1MB);while (true) {int len = channel.read(buf);if (len == -1) {break;}//flip()大概意思是记录当前的缓冲位置,下次读入缓冲区从保存的位置开始读取buf.flip();to.write(buf);buf.clear();}} catch (Exception e) {e.printStackTrace();}long end = System.nanoTime();System.out.println(end - start);}
}
对同一个文件进行下载保存操作,使用IO要比NIO慢很多,这个时候就要看IO与NIO的实现原理了。
IO实现原理
Java在运行到读取文件时,由于Java本身不能对操作系统的内存进行读取,所以需要调用本地方法对操作系统内存进行操作(也就是上图CPU时间轴的System部分),操作系统需要从磁盘文件读取文件到系统的缓冲空间(保存的第一份),系统缓冲区再写入Java的缓冲区(程序中定义的byte数组充当缓冲区,相当于二次保存),然后本地方法调用结束,CPU再转换到Java程序去读取Java缓冲区保存。
NIO实现原理
NIO与IO的区别在于操作系统会分出一块直接内存,这块内存java可以直接访问到,省去了操作系统的缓冲区到Java缓冲区的部分。因此读写性能比较好。对应的代码为ByteBuffer.allocateDirect()
直接内存的回收原理
这是还未进行分配直接内存是内存占用比为47%。
public class demo6 {public static void main(String[] args) throws IOException {//使用直接内存并分配1G大小ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);System.out.println("分配完成");System.in.read();//将引用置空,使其可以被回收byteBuffer = null;System.gc();System.out.println("释放完成");System.in.read();}
}
接下来观察内存占用比
加了1G的直接内存后,占比为54%接下来调用gc垃圾回收
可以看到内存恢复为47%,说明直接内存被释放,但是直接内存是不受GC回收管理的,为什么会被释放呢?
实际上释放直接内存是JVM自己完成的,由Java底层Unsafe类实现。简单模拟一下
public class demo7 {public static void main(String[] args) throws IOException {Unsafe unsafe = getUnsafe();//base是指分配的内存地址long base = unsafe.allocateMemory(1024 * 1024 * 1024);unsafe.setMemory(base,1024 * 1024 * 1024,(byte) 0);System.in.read();unsafe.freeMemory(base);System.in.read();}public static Unsafe getUnsafe() {
// Unsafe unsafe = Unsafe.getUnsafe();//通过暴力反射拿到底层类对象Unsafetry {Field f = Unsafe.class.getDeclaredField("theUnsafe");f.setAccessible(true);Unsafe unsafe = (Unsafe) f.get(null);return unsafe;} catch (NoSuchFieldException | IllegalAccessException e) {e.printStackTrace();throw new RuntimeException(e);}}
}
上面代码手动释放直接内存的片段执行结果和第一个例子结果完全相同。那么我们去看一下ByteBuffer.allocateDirect()方法源码
该方法创建了一个DirectByteBuffer类对象,接着查看对应源码。
DirectByteBuffer(int cap) { // package-privatesuper(-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 boundaryaddress = base + ps - (base & (ps - 1));} else {address = base;}//之所以能被回收直接内存与Cleaner有直接关联cleaner = Cleaner.create(this, new Deallocator(base, size, cap));att = null;}
Clearner类型在Java的类库中叫做虚引用类型,特点是虚引用所关联的对象被GC回收时,会自动触发create方法。在JVM中有一个单独线程监视虚引用对象的状态,如果关联对象被回收就会执行对应的run方法。
由这个构造方法可以看出来,从第9行开始,做了手动释放直接内存代码块相同的事情。都是调用Unsafe对象去分配内存空间,不同的是,它创建了一个Cleaner对象,并调用了create方法,查看这个方法参数Deallocator对象源码
可以看出来它实现了Runnable接口,相当于由其他线程去执行run方法而不是主线程。run方法中调用了Unsafe的freeMemory()方法释放内存。
总结就是:在客户端分配直接内存时,创建了一个Clearner对象与客户端对象相绑定,当客户端对象被垃圾回收时,就会执行虚引用监视线程中的任务线程由JVM释放直接内存。
禁用显式回收对直接内存的影响
所谓禁用显式回收就是在运行前添加的一个JVM参数-XX:+DisableExplicitGC,添加该参数后,在代码中程序员编写的System.gc()就无法生效(因为手动的gc操作是一个Full GC是一个耗时比较久的操作,因此在大多时候,等待程序自己进行gc即可,手动的GC会影响程序运行效率。)由于手动gc失效,那么在JVM内存充足的情况下,与之关联的对象即使为null也不会立即被回收,那么直接内存也无法释放。为了避免这个问题,我们可以在频繁操作直接内存时,通过调用Unsafe类中的freeMemory方法来手动释放直接内存。