缓冲区ByteBuffer

简介

ByteBuffer在NIO通信中负责数据读写,本质就是个固定长度的byte数组,


上图描述了ByteBuffer父子类的关联关系,从Buffer衍生下来的buffer类包括:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer,LongBuffer,ShortBuffer,MappedByteBuffer。而ByteBuffer的实现类包括"HeapByteBuffer"和"DirectByteBuffer"两种。用的比较多,所以今天主要讲这几个。

属性接口

    final byte[] hb;                  // Non-null only for heap buffers
    final int offset;
// Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
  1. capacity
    缓冲区能够容纳的数据元素的最大数量。这个容量在缓冲区创建时被设定,不能为负并且永远不能被改变。
  2. limit
    第一个不应该读取或写入的元素的索引,即执行get()、put操作数据范围不可超过limit值。缓冲区的限制不能为负,并且不能大于其容量。
  3. position
    下一个要读取或写入的元素的索引。缓冲区的位置不能为负,并且不能大于其限制。
  4. mark
    用于备份当前的position。调用mark( )把当前的position赋值给mark,mark=postion。调用reset( )把mark值还原给position,position=mark。标记在设定前是未定义的默认:-1。结合reset(),mark将会跳转到上次标记的位置,循环的读取内容。
    这四个属性之间总是遵循以下关系: 0 <= mark <= position <= limit <= capacity
  5. allocate
    初始化字节数组hd,在虚拟机堆上申请内存空间,分配一个新的字节缓冲区(HeapByteBuffer)。新缓冲区的位置(position)将为零,其界限(limit)将为其容量(capacity),其标记(mark)默认:-1。它将具有一个底层实现数组,且其数组偏移量将为零。源码如下:
public static ByteBuffer allocate(int capacity) {
 if (capacity < 0)
   throw new IllegalArgumentException();
 return new HeapByteBuffer(capacity, capacity);
}
//此HeapByteBuffer构造方法如下
HeapByteBuffer(int cap, int lim) {
 //mark:-1,position:0,limit:lim,capacity:cap,hb:new  byte[cap],offset:0
   super(-1, 0, lim, cap, new byte[cap], 0);
}

  1. allocateDirect
    分配新的直接字节缓冲区(DirectByteBuffer)。新缓冲区的位置(position)将为零,其界限(limit)将为其容量(capacity),其标记(mark)默认:-1。
    尽管该方法的内部逻辑仅一行new DirectByteBuffer(capacity) ,但DirectByteBuffer的构造方法相较于HeapByteBuffer却复杂的多。通过unsafe.allocateMemory申请堆外内存,并在ByteBuffer的address变量中维护指向该内存的地址。unsafe.setMemory(base, size, (byte) 0)方法把新申请的内存数据清零。不仅对象构建耗时较HeapByteBuffer更长,而且脱离了JVM正常gc管理,用完之后需要手动释放或者触发full gc回收
public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
DirectByteBuffer(int cap) {                   // package-private

        super(-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 boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

JVM底层的执行数据读取/发送操作的都是通过DirectByteBuffer。假如我们在应用层将消息封装在HeapByteBuffer中,执行write方法后,JVM会将该对象中的内容拷贝到DirectByteBuffer再执行输出,在这个过程中多了一次内存拷贝。如若开始我们就将数据写入DirectByteBuffer中,将获得更好的性能表现。对于DirectByteBuffer本身存在的创建耗时与对象回收问题,可以依托于内存池技术得以解决。

  1. wrap
    该方法执行的是 new HeapByteBuffer(array, offset, length); 操作。与allocate方法的差异在于byte数组是由外部指定的。
  2. put
    将给定的字节写入此缓冲区的当前位置(position),然后该位置递增。以HeapByteBuffer实现算法为例
public ByteBuffer put(byte x) {

        hb[ix(nextPutIndex())] = x;
        return this;
    }
 final int nextPutIndex() {                          // package-private
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }
protected int ix(int i) {
        return i + offset;
    }

  1. get
    读取此缓冲区当前位置的字节,然后该位置递增。以HeapByteBuffer实现算法为例
 public byte get(int i) {
        return hb[ix(checkIndex(i))];
    }
final int checkIndex(int i) {                       // package-private
        if ((i < 0) || (i >= limit))
            throw new IndexOutOfBoundsException();
        return i;
    }
 protected int ix(int i) {
        return i + offset;
    }
  1. filp
    反转此缓冲区。首先将限制设置为当前位置,然后将位置设置为 0。设置mark为-1。
    通常运用于ByteBuffer从写模式切换到读模式时使用。如下图,写模式下往ByteBuffer写入“hello”,position值为5;此时要从ByteBuffer中取出这些数据,需要执行flip将状态切换到读模式,并限制读取数据的范围limit为当初写入的结尾边界5,设置读位置position为0。之后再执行读操作get的时候,position值递增,但不可操作limit。
public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
  1. compact
    将缓冲区的当前位置和界限之间的字节(如果有)复制到缓冲区的开始处。即将索引 p = position() 处的字节复制到索引 0 处,将索引 p + 1 处的字节复制到索引 1 处,依此类推,直到将索引 limit() - 1 处的字节复制到索引 n = limit() - 1 - p 处。然后将缓冲区的位置设置为 n+1,并将其界限设置为其容量。
public ByteBuffer compact() {
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());
        limit(capacity());
        discardMark();
        return this;
    }

其实只是数据移位。结合上面flip的例子,当position读到位置2的时候,我们想再次切换到写模式,就可以使用compact。如下图所示,原本ByteBuffer中还有三个byte:[L,L,O]可读,执⾏compact会将这三个byte拷贝到区间[0,2]位置,之后的[3,5]区间为可写空间,执行ByteBuffer.put('H')会填充position:3。


  1. clear
    清除此缓冲区,但不会擦除数据。将位置设置为 0,将限制(limit)设置为容量(capacity),设置mark为-1。
public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

样例

public static void main(String[] args) {
        byte[] data = "test-nio".getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(8);
        printBuffer("初始", buffer);
        buffer.put(data);
        printBuffer("写数据", buffer);
        buffer.flip();
        printBuffer("执行flip", buffer);
        byte[] read = new byte[buffer.remaining()];
        buffer.get(read);
        printBuffer("读数据", buffer);
        System.out.println(new String(read));
        System.out.println("-----------------");
 //采用wrap方式构建的初始为读模式。先读取部分内容“test-”,执行compact进行数据移位后再读取剩余部分内容
        buffer = ByteBuffer.wrap(data);
        printBuffer("初始", buffer);
        byte[] read1 = new byte[6];
        buffer.get(read);
        printBuffer("读取\"test-\"", buffer);
        buffer.compact();
        printBuffer("执行compact", buffer);
        read = new byte[buffer.remaining()];
        buffer.get(read);
        printBuffer("读取数据", buffer);
        System.out.println(new String(read));
    }

    private static void printBuffer(String title, ByteBuffer buffer) {
        System.out.println(title + " position:" + buffer.position() + " ,limit:" + buffer.limit() + " ,capacity:"
                + buffer.capacity());
    }
输出
初始 position:0 ,limit:8 ,capacity:8
写数据 position:8 ,limit:8 ,capacity:8
执行flip position:0 ,limit:8 ,capacity:8
读数据 position:8 ,limit:8 ,capacity:8
test-nio
-----------------
初始 position:0 ,limit:8 ,capacity:8
读取"test-" position:8 ,limit:8 ,capacity:8
执行compact position:0 ,limit:8 ,capacity:8
读取数据 position:8 ,limit:8 ,capacity:8
test-nio

NIO的数据访问方式

NIO提供了比传统的文件访问方式更好的方法。一个是FileChannel.transferToFileChannel.transferFromFileChannel.map

FileChannel.transferXX

减少数据从内核到用户空间的复制,数据直接在内核空间中移动,在Linux中使用sendfile系统调用。

FileChannel.map

将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时将直接操作这个文件数据,这种方式省去了数据从内核空间向用户空间复制的损耗。这种方式适合对大文件的只读性操作,如大文件的MD5校验。这种方式和操作系统底层I/O实现相关。

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

推荐阅读更多精彩内容

  • Java NIO主要解决了Java IO的效率问题,解决此问题的思路之一是利用硬件和操作系统直接支持的缓冲区、虚拟...
    小波同学阅读 678评论 0 1
  • Java NIO中的Buffer用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。缓冲区本...
    桥头放牛娃阅读 2,894评论 0 5
  • Buffer java NIO库是在jdk1.4中引入的,NIO与IO之间的第一个区别在于,IO是面向流的,而NI...
    德彪阅读 2,187评论 0 3
  • 转自 http://www.ibm.com/developerworks/cn/education/java/j-...
    抓兔子的猫阅读 2,272评论 0 22
  • # Java NIO # Java NIO属于非阻塞IO,这是与传统IO最本质的区别。传统IO包括socket和文...
    Teddy_b阅读 581评论 0 0