NIO学习笔记(一)入门ByteBuffer类

ByteBuffer类是Buffer类的子类,可以在缓冲区中以字节为单位对数据进行存取,而且他也是比较常用和重要的缓冲区类。在使用NIO技术时,有很大的概率使用ByteBuffer类来进行数据处理。

ByteBuffer类提供6类操作:

  1. 以绝对位置和相对位置读写单个字符的get()和put()方法。
  2. 使用相对批量get(byte[ ] dst)方法可以将缓冲区中的连续字节传输到byte[ ] dst目标数组中
  3. 使用相对批量put(byte[ ] dst)方法可以将byte[ ] dst目标数组或其他字节缓冲区中的连续字节传输到此缓冲区中
  4. 使用绝对和相对getType和putType方法可以按照字节顺序在字节序列中读写其他基本类型的值,方法getType和putType可以进行数据类型的自动转换
  5. 提供了创建视图缓冲区的方法,这些方法允许将自己饿缓冲区视为包含其他基本数据类型值得缓冲区,这些方法有asCharBuffer()、asDoubleBuffer()、asIntBuffer()、asLongBuffer()和asShortBuffer()
  6. 提供了对字节缓冲区进行压缩(compacting)、复制(duplicating)和截取(slicing)的方法

1、ByteBuffer缓冲区的直接与非直接缓冲区

ByteBuffer分为两类,一个是分为两类直接字节缓冲区,另一个是非直接字节缓冲区。

如果字节缓冲区为直接字节缓冲区,则JVM会尽量在直接字节缓冲区上执行本机I/O操作,也就是直接对内核空间进行访问,以提高运行效率。提高运行效率的原理就是在每次调用基于操作系统的I/O操作之前和之后,JVM都会尽量避免将缓冲区的内容复制到中间缓冲区,或者从中间缓冲区复制内容,这样就节省了一个步骤。

那么非直接缓冲区是什么样的呢?当我们通过ByteBuffer向硬盘存取数据时是需要将数据暂存在JVM的中间缓冲区,如果有频繁操作数据的情况发生,则每次操作时都会都会将数据暂存在JVM的中间缓冲区,再交给ByteBuffer处理,这样做大大降低了软件对数据的吞吐量,提高了内存占有率,造成了软件运行效率低下。这就是非直接缓冲区

2、创建ByteBuffer缓冲区

因为缓冲区分为两类,所以这里我一一介绍,如何创建这两种缓冲区。

首先ByteBuffer是被abstract修饰的,所以不可以使用new来创建实例。这里ByteBuffer提供了工厂方法allocateDirect()和allcate()。(英文好的可能一眼就知道这个方法是用来干嘛的,allocate是分配的意思,direct是直接的意思)我们可以通过这个方法创建字节缓冲区。通过工厂方法allocateDirect()返回的缓冲区进行内存的分配和释放所需要的时间成本通常都要高于非直接缓冲区。直接缓冲区操作的数据不在JVM堆中,而是在内核空间中,根据这个可以分析出,直接缓冲区善于保存那些易受操作系统本机I/O影响的大量、长时间保存的数据。

allocateDirect(int capacity)方法的作用是:分配一个新的直接字节缓冲区。

allocate(int capacity)方法的作用是:分配一个新的非直接字节缓冲区。

当然除了allocate我们还可以使用wrap()这个方法来创建缓冲区。当然wrap方法创建出来的也是非直接字节缓冲区。

具体创建示例如下:

public class Niodemo {
    public static void main(String[] args) {
        //分配可以容纳100个字节的直接字节缓冲区
        ByteBuffer buffer1 = ByteBuffer.allocateDirect(100);
        //分配可以容纳100个字节的非直接字节缓冲区
        ByteBuffer buffer2 = ByteBuffer.allocate(100);

        byte[] bytes = new byte[100];
        //将bytes这个字节类型数组,包装成字节缓冲区,容量依旧为100
        ByteBuffer buffer3 = ByteBuffer.wrap(bytes);
    }
}

在JDK中,可以查看一下allocate()的源码,从中你会发现其中创建一个新的数组,而wrap()方法是使用传入的数组作为存储空间,说明对wrap()关联的数组进行操作会影响到缓冲区的数据,而操作缓冲区中的数据也会影响到与wrap()关联的数组中的数据,原理其实就是因为引用了同一个数组对象。

那么使用allocateDirect()方法创建的直接缓冲区如何释放内存?有两种方法,一种是手动释放,另一种其实是交给JVM进行处理。先来看第一种手动释放:

public class ReleaseDemo {
    public static void main(String[] args) throws SecurityException,
            InterruptedException, NoSuchMethodException,
            InvocationTargetException, IllegalAccessException {
        //A阶段:创建直接缓冲区,容量int的最大值2147483647
        System.out.println("A");
        ByteBuffer buffer = ByteBuffer.allocateDirect(Integer.MAX_VALUE);
        //B阶段:将创建的直接缓冲区填充1
        System.out.println("B");
        byte[] byteArray = new byte[]{1};
        System.out.println(Integer.MAX_VALUE);
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            buffer.put(byteArray);
        }
        //填充结束
        System.out.println("put end !");
        //休眠1s
        Thread.sleep(1000);
        //使用反射调用DirectByteBuffer的cleaner()方法,返回一个Cleaner类
        Method cleanerMethod = buffer.getClass().getMethod("cleaner");
        cleanerMethod.setAccessible(true);
        Object returnValue = cleanerMethod.invoke(buffer);
        //通过反射调用Cleaner类的clean方法,释放内存
        Method cleanMethod = returnValue.getClass().getMethod("clean");
        cleanMethod.setAccessible(true);
        cleanMethod.invoke(returnValue);
    }
}

3、wrap包装数据的处理

之前已经知道了,wrap(byte[ ] array)这个方法的作用了。他是将byte数组包装到缓冲区中。新的缓冲区将由给定的byte数组支持,也就是说,缓冲区如果修改了数据,那么会导致原数组的数据也被修改。新的缓冲区的capacity(容量)和limit(读取限制)都将为array.length,其读取位置position为0,其标记mark为undefined,也就是未定义。其底层实现的数组为给定的数组,arrayOffset为0.

当然wrap还有另外一种实现。wrap(byte[ ] array, int offset , int length),它的作用是将byte数组包装到缓冲区中。新的缓冲区将由给定的byte数组支持,也就是说,缓冲区修改则导致原数组修改。新缓冲区的容量为array.length,他的position为offset,其limit为offset+length,其标记mark依然为undefined。其底层实现的数组为给定的数组,arrayOffset为0.

注意:wrap(byte[ ] array, int offset , int length)并不具有subString()的截取作用,他的参数offset只是设置position的值,而length确定limit的值

public class WrapDemo {
    public static void main(String[] args) {
        byte[] byteArray = new byte[]{1,2,3,4,5,6,7,8};
        ByteBuffer buffer1 = ByteBuffer.wrap(byteArray);
        ByteBuffer buffer2 = ByteBuffer.wrap(byteArray , 2 , 4);
        System.out.println("buffer1 capacity = " + buffer1.capacity() +
                " limit="+buffer1.limit()+" position="+buffer1.position());
        System.out.println(" ");
        System.out.println("buffer2 capacity = " + buffer2.capacity() +
                " limit="+buffer2.limit()+" position="+buffer2.position());
    }
}
## 运行结果
buffer1 capacity = 8 limit=8 position=0
 
buffer2 capacity = 8 limit=6 position=2