NIO 之 ByteBuffer实现原理

相关文章

IO、NIO、AIO 内部原理分析
NIO 之 Selector实现原理
NIO 之 Channel实现原理

前言

Java NIO 主要由下面3部分组成:

  • Buffer
  • Channel
  • Selector

在传统IO中,流是基于字节的方式进行读写的。
在NIO中,使用通道(Channel)基于缓冲区数据块的读写。

流是基于字节一个一个的读取和写入。
通道是基于块的方式进行读取和写入。

Buffer 类结构图

Buffer 的类结构图如下:


Buffer类结构图

从图中发现java中8中基本的类型,除了boolean外,其它的都有特定的Buffer子类。

Buffer类分析

Filed

每个缓冲区都有这4个属性,无论缓冲区是何种类型都有相同的方法来设置这些值

private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

1. 标记(mark)

初始值-1,表示未标记。
标记一个位置,方便以后reset重新从该位置读取数据。

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;
}

2. 位置(position)

缓冲区中读取或写入的下一个位置。这个位置从0开始,最大值等于缓冲区的大小

//获取缓冲区的位置
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;
}

3. 限度(limit)

//获取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;
 }

4. 容量(capacity)

缓冲区可以保存元素的最大数量。该值在创建缓存区时指定,一旦创建完成后就不能修改该值。

//获取缓冲区的容量
public final int capacity() {
    return capacity;
}

filp 方法

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
  1. 将limit设置成当前position的坐标
  2. 将position设置为0
  3. 取消标记

rewind 方法

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

从源码中发现,rewind修改了position和mark,而没有修改limit。

  1. 将position设置为0
  2. 取消mark标记

clear 方法

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
  1. 将position坐标设置为0
  2. limit设置为capacity
  3. 取消标记

从clear方法中,我们发现Buffer中的数据没有清空,如果通过Buffer.get(i)的方式还是可以访问到数据的。如果再次向缓冲区中写入数据,他会覆盖之前存在的数据。

remaining 方法

查看当前位置和limit之间的元素数。

public final int remaining() {
    return limit - position;
}

hasRemaining 方法

判断当前位置和limit之间是否还有元素

public final boolean hasRemaining() {
    return position < limit;
}

ByteBuffer 类分析

ByteBuffer类结果图

从图中我们可以发现 ByteBuffer继承于Buffer类,ByteBuffer是个抽象类,它有两个实现的子类HeapByteBuffer和MappedByteBuffer类

HeapByteBuffer:在堆中创建的缓冲区。就是在jvm中创建的缓冲区。
MappedByteBuffer:直接缓冲区。物理内存中创建缓冲区,而不在堆中创建。

allocate 方法(创建堆缓冲区)

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

我们发现allocate方法创建的缓冲区是创建的HeapByteBuffer实例。

HeapByteBuffer 构造

HeapByteBuffer(int cap, int lim) {            // package-private
    super(-1, 0, lim, cap, new byte[cap], 0);
}

从堆缓冲区中看出,所谓堆缓冲区就是在堆内存中创建一个byte[]数组。

allocateDirect创建直接缓冲区

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

我们发现allocate方法创建的缓冲区是创建的DirectByteBuffer实例。

DirectByteBuffer构造

DirectByteBuffer 构造方法

直接缓冲区是通过java中Unsafe类进行在物理内存中创建缓冲区。

wrap 方法

public static ByteBuffer wrap(byte[] array)
public static ByteBuffer wrap(byte[] array, int offset, int length);

可以通过wrap类把字节数组包装成缓冲区ByteBuffer实例。
这里需要注意的的,把array的引用赋值给ByteBuffer对象中字节数组。如果array数组中的值更改,则ByteBuffer中的数据也会更改的。

get 方法

  1. public byte get()
    获取position坐标元素,并将position+1;
  2. public byte get(int i)
    获取指定索引下标的元素
  3. public ByteBuffer get(byte[] dst)
    从当前position中读取元素填充到dst数组中,每填充一个元素position+1;
  4. public ByteBuffer get(byte[] dst, int offset, int length)
    从当前position中读取元素到dst数组的offset下标开始填充length个元素。

put 方法

  1. public ByteBuffer put(byte x)
    写入一个元素并position+1
  2. public ByteBuffer put(int i, byte x)
    指定的索引写入一个元素
  3. public final ByteBuffer put(byte[] src)
    写入一个自己数组,并position+数组长度
  4. public ByteBuffer put(byte[] src, int offset, int length)
    从一个自己数组的offset开始length个元素写入到ByteBuffer中,并把position+length
  5. public ByteBuffer put(ByteBuffer src)
    写入一个ByteBuffer,并position加入写入的元素个数

视图缓冲区

Paste_Image.png

ByteBuffer可以转换成其它类型的Buffer。例如CharBuffer、IntBuffer 等。

压缩缓冲区

public ByteBuffer compact() {
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());
        limit(capacity());
        discardMark();
        return this;
    }

1、把缓冲区positoin到limit中的元素向前移动positoin位
2、设置position为remaining()
3、 limit为缓冲区容量
4、取消标记

例如:ByteBuffer.allowcate(10);
内容:[0 ,1 ,2 ,3 4, 5, 6, 7, 8, 9]

compact前

[0 ,1 ,2 , 3, 4, 5, 6, 7, 8, 9]
pos=4
lim=10
cap=10

compact后

[4, 5, 6, 7, 8, 9, 6, 7, 8, 9]
pos=6
lim=10
cap=10

slice方法

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

从结果中发现直接缓冲区创建分配空间比较耗时。

对比结论

直接缓冲区比较适合读写操作,最好能重复使用直接缓冲区并多次读写的操作。
堆缓冲区比较适合创建新的缓冲区,并且重复读写不会太多的应用。

建议:如果经过性能测试,发现直接缓冲区确实比堆缓冲区效率高才使用直接缓冲区,否则不建议使用直接缓冲区。


想了解更多精彩内容请关注我的公众号

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容