BIO、NIO、AIO 内部原理分析
NIO 之 Selector实现原理
NIO 之 Channel实现原理java
Java NIO 主要由下面3部分组成:数组
在传统IO中,流是基于字节的方式进行读写的。
在NIO中,使用通道(Channel)基于缓冲区数据块的读写。缓存
流是基于字节一个一个的读取和写入。
通道是基于块的方式进行读取和写入。app
Buffer 的类结构图以下:jvm
从图中发现java中8中基本的类型,除了boolean外,其它的都有特定的Buffer子类。性能
每一个缓冲区都有这4个属性,不管缓冲区是何种类型都有相同的方法来设置这些值测试
private int mark = -1; private int position = 0; private int limit; private int capacity;
初始值-1,表示未标记。
标记一个位置,方便之后reset从新从该位置读取数据。this
public final Buffer mark() { mark = position; return this; } public final Buffer reset() { int m = mark; if (m < 0) throw new InvalidMarkException(); position = m; return this; }
缓冲区中读取或写入的下一个位置。这个位置从0开始,最大值等于缓冲区的大小spa
//获取缓冲区的位置 public final int position() { return position; } //设置缓冲区的位置 public final Buffer position(int newPosition) { if ((newPosition > limit) || (newPosition < 0)) throw new IllegalArgumentException(); position = newPosition; if (mark > position) mark = -1; return this; }
//获取limit位置 public final int limit() { return limit; } //设置limit位置 public final Buffer limit(int newLimit) { if ((newLimit > capacity) || (newLimit < 0)) throw new IllegalArgumentException(); limit = newLimit; if (position > limit) position = limit; if (mark > limit) mark = -1; return this; }
缓冲区能够保存元素的最大数量。该值在建立缓存区时指定,一旦建立完成后就不能修改该值。.net
//获取缓冲区的容量 public final int capacity() { return capacity; }
public final Buffer flip() { limit = position; position = 0; mark = -1; return this; }
public final Buffer rewind() { position = 0; mark = -1; return this; }
从源码中发现,rewind修改了position和mark,而没有修改limit。
public final Buffer clear() { position = 0; limit = capacity; mark = -1; return this; }
从clear方法中,咱们发现Buffer中的数据没有清空,若是经过Buffer.get(i)的方式仍是能够访问到数据的。若是再次向缓冲区中写入数据,他会覆盖以前存在的数据。
查看当前位置和limit之间的元素数。
public final int remaining() { return limit - position; }
判断当前位置和limit之间是否还有元素
public final boolean hasRemaining() { return position < limit; }
ByteBuffer类结果图
从图中咱们能够发现 ByteBuffer继承于Buffer类,ByteBuffer是个抽象类,它有两个实现的子类HeapByteBuffer和MappedByteBuffer类
HeapByteBuffer:在堆中建立的缓冲区。就是在jvm中建立的缓冲区。
MappedByteBuffer:直接缓冲区。物理内存中建立缓冲区,而不在堆中建立。
public static ByteBuffer allocate(int capacity) { if (capacity < 0) throw new IllegalArgumentException(); return new HeapByteBuffer(capacity, capacity); }
咱们发现allocate方法建立的缓冲区是建立的HeapByteBuffer实例。
HeapByteBuffer(int cap, int lim) { // package-private super(-1, 0, lim, cap, new byte[cap], 0); }
从堆缓冲区中看出,所谓堆缓冲区就是在堆内存中建立一个byte[]数组。
public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); }
咱们发现allocate方法建立的缓冲区是建立的DirectByteBuffer实例。
DirectByteBuffer 构造方法
直接缓冲区是经过java中Unsafe类进行在物理内存中建立缓冲区。
public static ByteBuffer wrap(byte[] array) public static ByteBuffer wrap(byte[] array, int offset, int length);
能够经过wrap类把字节数组包装成缓冲区ByteBuffer实例。
这里须要注意的的,把array的引用赋值给ByteBuffer对象中字节数组。若是array数组中的值更改,则ByteBuffer中的数据也会更改的。
ByteBuffer能够转换成其它类型的Buffer。例如CharBuffer、IntBuffer 等。
public ByteBuffer compact() { System.arraycopy(hb, ix(position()), hb, ix(0), remaining()); position(remaining()); limit(capacity()); discardMark(); return this; }
一、把缓冲区positoin到limit中的元素向前移动positoin位
二、设置position为remaining()
三、 limit为缓冲区容量
四、取消标记
例如:ByteBuffer.allowcate(10);
内容:[0 ,1 ,2 ,3 4, 5, 6, 7, 8, 9]
[0 ,1 ,2 , 3, 4, 5, 6, 7, 8, 9]
pos=4
lim=10
cap=10
[4, 5, 6, 7, 8, 9, 6, 7, 8, 9]
pos=6
lim=10
cap=10
public ByteBuffer slice() { return new HeapByteBuffer(hb, -1, 0, this.remaining(), this.remaining(), this.position() + offset); }
建立一个分片缓冲区。分配缓冲区与主缓冲区共享数据。
分配的起始位置是主缓冲区的position位置
容量为limit-position。
分片缓冲区没法看到主缓冲区positoin以前的元素。
下面咱们从缓冲区建立的性能和读取性能两个方面进行性能对比。
public static void directReadWrite() throws Exception { int time = 10000000; long start = System.currentTimeMillis(); ByteBuffer buffer = ByteBuffer.allocate(4*time); for(int i=0;i<time;i++){ buffer.putInt(i); } buffer.flip(); for(int i=0;i<time;i++){ buffer.getInt(); } System.out.println("堆缓冲区读写耗时 :"+(System.currentTimeMillis()-start)); start = System.currentTimeMillis(); ByteBuffer buffer2 = ByteBuffer.allocateDirect(4*time); for(int i=0;i<time;i++){ buffer2.putInt(i); } buffer2.flip(); for(int i=0;i<time;i++){ buffer2.getInt(); } System.out.println("直接缓冲区读写耗时:"+(System.currentTimeMillis()-start)); }
输出结果:
堆缓冲区读写耗时 :70 直接缓冲区读写耗时:47
从结果中咱们发现堆缓冲区读写比直接缓冲区读写耗时更长。
public static void directAllocate() throws Exception { int time = 10000000; long start = System.currentTimeMillis(); for (int i = 0; i < time; i++) { ByteBuffer buffer = ByteBuffer.allocate(4); } System.out.println("堆缓冲区建立时间:"+(System.currentTimeMillis()-start)); start = System.currentTimeMillis(); for (int i = 0; i < time; i++) { ByteBuffer buffer = ByteBuffer.allocateDirect(4); } System.out.println("直接缓冲区建立时间:"+(System.currentTimeMillis()-start)); }
输出结果:
堆缓冲区建立时间:73 直接缓冲区建立时间:5146
从结果中发现直接缓冲区建立分配空间比较耗时。
直接缓冲区比较适合读写操做,最好能重复使用直接缓冲区并屡次读写的操做。
堆缓冲区比较适合建立新的缓冲区,而且重复读写不会太多的应用。
建议:若是通过性能测试,发现直接缓冲区确实比堆缓冲区效率高才使用直接缓冲区,不然不建议使用直接缓冲区,由于JVM垃圾回收不会回收直接分配的物理内存,只回收虚拟机堆内存。