NIO之缓冲区
NIO引入了三个概念:
- Buffer 缓冲区
- Channel 通道
- selector 选择器
1、java.io优化建议
操作系统与Java基于流的I/O模型有些不匹配。操作系统要移动的是大块数据(缓冲区),这往往是在硬件直接存储器存取(DMA)的协助下完成的。I/O类喜欢操作小块数据——单个字节、几行文本。结果,操作系统送来整缓冲区的数据,java.io的流数据类再花大量时间把它们拆成小块,往往拷贝一个小块就要往返于几层对象。操作系统喜欢整卡车地运来数据,java.io类则喜欢一铲子一铲子地加工数据。
—— 引自《JAVA NIO》
在传统的java.io中,面向单字节的读写效率是十分低下的,尤其说频繁的读写和操作大文件,效率相差可达千百倍。[注:这里说的是面向字节的读写效率底下,而不是说传统的io效率底下]
下面是一些优化建议:
- 尽量避免单字节读写,例如 IuputStream.read(byte b),OutputStream.write(byte b)
- 尽量使用基于数组API的读写数据,例如 IuputStream.read(byte[] b),OutputStream.write(byte[] b)
- 尽量使用基于缓冲的类进行读写,例如BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter
- 对文件随机操作读取和写入时,用RandomAccessFile类
- 用System.arrayCopy在数组间进行复制
2、java.nio中Buffer
从上面IO的优化建议中,我们可以看到很多Buffer的影子。基于缓冲的读写,大多数情况下可以提高IO效率。nio中Buffer的引入,使得java的IO模型更贴近操作系统底层,面向Buffer的读写操作更高效,同时也在API层面避免了单字节操作。
- 2.1 ByteBuffer字节缓冲区
操作系统的IO是以字节为单位的,因此,字节缓冲区跟其他缓冲区不同,对操作系统的IO只能是基于字节缓冲区的,所以通道(channel)只接收ByteBuffer作为参数。
- 2.2 直接缓冲区和非直接缓冲区
ByteBuffer又分为直接缓冲区和非直接缓冲区。
非直接缓冲区可以通过ByteBuffer.wrap(byte[] array);ByteBuffer.allocate(int capacity)这两个方法来创建
直接缓冲区可通过ByteBuffer.allocateDirect(int capacity)来创建
字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
对直接缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过映射将文件区域直接映射到内存中来创建。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。
—— 引自《JDK API 1.6.0》
2.3 直接缓冲区跟非直接缓冲区的区别
JDK中的说明不太容易理解,我们从源码层面来分析二者的区别。
1 public abstract class Buffer { 2 ............ 3 // Used only by direct buffers 4 // NOTE: hoisted here for speed in JNI GetDirectBufferAddress 5 long address; 6 ........... 7 }
在Buffer类中定义了一个变量adress,注释为仅作为直接缓冲区使用,通过调用JNI的方法来获得一个内存地址。
也就是说,直接缓冲区说指向内存中的某个地址,而不是JVM中的某个区域。由于是内存中的某个区域,并且通过JNI去调用操作系统底层的指令,因此在某些情况下相对的高效。非直接缓冲区指向的是JVM内某个数组空间。
再来看创建直接缓冲区的一些有趣细节
1 DirectByteBuffer(int cap) { 2 3 super(-1, 0, cap, cap, false); 4 Bits.reserveMemory(cap); 5 int ps = Bits.pageSize();//1、获取内存分页大小 6 long base = 0; 7 try { 8 base = unsafe.allocateMemory(cap + ps);//2、分配内存空间,分配的空间比容量大,多出一个分页大小,为了后面调整起始位置也分页对齐 9 } catch (OutOfMemoryError x) { 10 Bits.unreserveMemory(cap); 11 throw x; 12 } 13 unsafe.setMemory(base, cap + ps, (byte) 0); 14 if (base % ps != 0) { 15 // Round up to page boundary 16 address = base + ps - (base & (ps - 1));//3、缓冲区起始地址与分页对齐,方便寻址 17 } else { 18 address = base; 19 } 20 cleaner = Cleaner.create(this, new Deallocator(base, cap)); 21 }
虽然直接缓冲区说独立于JVM外的一块区域,但是在创建的时候,可以通过设置JVM的启动参数来限制大小。
-XX:MaxDirectMemorySize=<size>
继续看Bits.reserveMemory(cap);,这个类并没有对内存进行实际的操纵,只是记录内存对应的一些参数信息。
static void reserveMemory(long size) { synchronized (Bits.class) { if (!memoryLimitSet && VM.isBooted()) { maxMemory = VM.maxDirectMemory(); memoryLimitSet = true; } if (size <= maxMemory - reservedMemory) {//如果创建直接缓冲区后的内存占用不超过最大内存限制 reservedMemory += size;//更新已分配的内存大小 return; } } //如果超过最大内存限制,执行垃圾回收 System.gc(); try { Thread.sleep(100);//等待垃圾回收完成 } catch (InterruptedException x) { // Restore interrupt status Thread.currentThread().interrupt(); } synchronized (Bits.class) { if (reservedMemory + size > maxMemory)//如果依然超过最大内存限制,则抛出内存溢出异常 throw new OutOfMemoryError("Direct buffer memory"); reservedMemory += size; }
- 2.4 非直接缓冲区的释放
由于DirectByteBuffer直接开辟一块内存当作缓冲区,并且调用操作系统的方法去读写,因此效率高。但是,也不盲目的去用DirectByteBuffer,如果使用不当,它也会带来一些问题,例如直接缓冲区独立于JVM之外,GC不能对这部分空间进行释放。
那么直接缓冲区是如何被释放的?来看源码
cleaner = Cleaner.create(this, new Deallocator(base, cap));
private static class Deallocator implements Runnable { ...... public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address);//通知操作系统释放对应的内存区域 address = 0; Bits.unreserveMemory(capacity);//更新JVM参数 } ........ }
创建直接缓冲的时候,会创建一个Cleaner来,在Deallocator中释放对应的内存区域。但是cleaner没法显示调用,因此无法手动释放直接缓冲区。
在使用直接缓冲区的时候应该注意:只有等DirectByteBuffer对象被jvm垃圾回收时,才会给操作指令去释放对应的内存。由于垃圾回收具有不确定行,即使显示调用GC,也可能不进行垃圾回收,因此这部分区域可能无法及时释放。
这里提供一种手动释放的方法,用到了反射,仅用来交流,但是不推荐使用(破坏了原有的java规范),除非在必要的情况下。
public static void destroyDirectByteBuffer(ByteBuffer toBeDestroyed) throws Exception { if (!toBeDestroyed.isDirect()) { return; } Method cleanerMethod = toBeDestroyed.getClass().getMethod("cleaner"); cleanerMethod.setAccessible(true); Object cleaner = cleanerMethod.invoke(toBeDestroyed); Method cleanMethod = cleaner.getClass().getMethod("clean"); cleanMethod.setAccessible(true); cleanMethod.invoke(cleaner); }