关于java:框架篇ByteBuffer和nettyByteBuf详解

11次阅读

共计 6581 个字符,预计需要花费 17 分钟才能阅读完成。

前言

数据序列化存储,或者数据通过网络传输时,会遇到不可避免将数据转成字节数组的场景。字节数组的读写不会太难,但又有点繁琐,为了防止反复造轮子,jdk 推出了 ByteBuffer 来帮忙咱们操作字节数组;而 netty 是一款以后风行的 java 网络 IO 框架,它外部定义了一个 ByteBuf 来治理字节数组,和 ByteBuffer 大同小异

  • ByteBuffer
  • 零拷贝之 MappedByteBuffer
  • DirectByteBuffer 堆外内存回收机制
  • netty 之 ByteBuf

关注公众号,一起交换,微信搜一搜: 潜行前行

Buffer 构造

public abstract class Buffer {
    // 关系: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    long address; // Used only by direct buffers,间接内存的地址
  • mark:调用 mark()办法的话,mark 值将存储以后 position 的值,等下次调用 reset()办法时,会设定 position 的值为之前的标记值
  • position:是下一个要被读写的 byte 元素的下标索引
  • limit:是缓冲区中第一个不能读写的元素的数组下标索引,也能够认为是缓冲区中理论元素的数量
  • capacity:是缓冲区可能包容元素的最大数量,这个值在缓冲区创立时被设定,而且不可能扭转

Buffer.API

Buffer(int mark, int pos, int lim, int cap)
//Buffer 创立时设置的最大数组容量值
public final int capacity()
// 以后指针的地位
public final int position() 
// 限度可读写大小
public final Buffer limit(int newLimit)
// 标记以后 position 的地位
public final Buffer mark()
// 配合 mark 应用,position 成之前 mark()标记的地位。先前没调用 mark 则报错
public final Buffer reset()
// 写 -> 读模式翻转,单向的
//position 变成了初值地位 0,而 limit 变成了写模式下 position 地位
public final Buffer flip()
// 重置 position 指针地位为 0,mark 为 -1;绝对 flip 办法是 limit 不变
public final Buffer rewind() // 复位
// 和 rewind 一样,多出一步是 limit 会被设置成 capacity
public final Buffer clear() 
// 返回残余未读字节数
public final int remaining()

ByteBuffer 构造

public abstract class ByteBuffer extends Buffer 
            implements Comparable<ByteBuffer>{final byte[] hb;  // 仅限堆内内存应用
    final int offset;
    boolean isReadOnly; 

ByteBuffer.API

// 申请堆外内存
public static ByteBuffer allocateDirect(int capacity)
// 申请堆内内存
public static ByteBuffer allocate(int capacity) 
// 原始字节包装成 ByteBuffer
public static ByteBuffer wrap(byte[] array, int offset, int length)
// 原始字节包装成 ByteBuffer
public static ByteBuffer wrap(byte[] array)
// 创立共享此缓冲区内容的新字节缓冲区
public abstract ByteBuffer duplicate()
// 分片,创立一个新的字节缓冲区
// 新 ByteBuffer 的开始地位是此缓冲区的以后地位 position
public abstract ByteBuffer slice()
// 获取字节内容
public abstract byte get()
// 从 ByteBuffer 偏移 offset 的地位,获取 length 长的字节数组,而后返回以后 ByteBuffer 对象
public ByteBuffer get(byte[] dst, int offset, int length)
// 设置 byte 内存
public abstract ByteBuffer put(byte b);
// 以 offset 为起始地位设置 length 长 src 的内容,并返回以后 ByteBuffer 对象
public ByteBuffer put(byte[] src, int offset, int length 长)
// 将没有读完的数据移到到缓冲区的初始地位,position 设置为最初一没读字节数据的下个索引,limit 重置为为 capacity
// 读 -> 写模式,相当于 flip 的反向操作
public abstract ByteBuffer compact()
// 是否是间接内存
public abstract boolean isDirect()
  • ByteBuffer bf = ByteBuffer.allocate(10);`,创立大小为 10 的 ByteBuffer 对象

  • 写入数据
    ByteBuffer buf ByteBuffer.allocate(10);
    buf.put("csc".getBytes());

  • 调用 flip 转换缓冲区为读模式; buf.flip();

  • 读取缓冲区中到内容:get(); System.out.println((char) buf.get());


零拷贝之 MappedByteBuffer

  • 共享内存映射文件,对应的 ByteBuffer 子操作类,MappedByteBuffer 是基于 mmap 实现的。对于零拷贝的 mmap 的底层原理能够看看: 框架篇:小白也能秒懂的 Linux 零拷贝原理。MappedByteBuffer 须要 FileChannel 调用本地 map 函数映射。C++ 代码能够查阅下 FileChannelImpl.c-Java_sun_nio_ch_FileChannelImpl_map0 办法
  • 应用 MappedByteBuffer 和文件映射,其读写能够缩小内存拷贝次数
FileChannel readChannel = FileChannel.open(Paths.get("./cscw.txt"), StandardOpenOption.READ);
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);

DirectByteBuffer 堆外内存回收机制 Cleaner

  • 上面咱们看看间接内存的回收机制(java8);DirectByteBuffer 外部存在一个 Cleaner 对象,并且委托外部类 Deallocator 对象进行内存回收
class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{
    // 构造函数
    DirectByteBuffer(int cap) { 
        .... // 内存调配
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        ...
    }    
    private static class Deallocator implements Runnable{
        ...
        public void run() {if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address); // 回收内存
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
}
  • 细看下 Cleaner,继承于 PhantomReference,并且在 public void clean() 办法会调用 Deallocator 进行革除操作
public class Cleaner extends PhantomReference<Object> {
    // 如果 DirectByteBuffer 对象被回收,相应的 Cleaner 会被放入 dummyQueue 队列
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
    // 构造函数
    public static Cleaner create(Object var0, Runnable var1) {return var1 == null ? null : add(new Cleaner(var0, var1));
    }
    private Cleaner(Object var1, Runnable var2) {super(var1, dummyQueue);
        this.thunk = var2;
    }
    private final Runnable thunk;
    public void clean() {if (remove(this)) {
                try {this.thunk.run();
                } catch (final Throwable var2) {....
  • 在 Reference 外部存在一个守护线程,循环获取 Reference,并判断是否 Cleaner 对象,如果是则调用其 clean 办法
public abstract class Reference<T> 
    static {ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg; tgn != null; g = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        ...
        handler.setDaemon(true);
        handler.start();
        ...
    }
    ...
    // 外部类调用 tryHandlePending
    private static class ReferenceHandler extends Thread {public void run() {while (true) {tryHandlePending(true);
                    }
                }
      ... 
    static boolean tryHandlePending(boolean waitForNotify) {
        Cleaner c;
        .... // 从链表获取对象被回收的援用
        // 判断 Reference 是否 Cleaner,如果是则调用其 clean 办法
        if (c != null) {c.clean(); // 调用 Cleaner 的 clean 办法
            return true;
        }
        ReferenceQueue<? super Object> q = r.queue;
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;

netty 之 ByteBuf

  • ByteBuf 原理
  • Bytebuf 通过两个地位指针来帮助缓冲区的读写操作,别离是 readIndex 和 writerIndex
 *      +-------------------+------------------+------------------+
 *      | discardable bytes |  readable bytes  |  writable bytes  |
 *      |                   |     (CONTENT)    |                  |
 *      +-------------------+------------------+------------------+
 *      |                   |                  |                  |
 *      0 <= readerIndex <= writerIndex <= capacity
  • ByteBuf.API
// 获取 ByteBuf 分配器
public abstract ByteBufAllocator alloc()
// 抛弃可读字节
public abstract ByteBuf discardReadBytes()
// 返回读指针
public abstract int readerIndex()
// 设置读指针
public abstract ByteBuf readerIndex(int readerIndex);
// 标记以后读指针地位,配合 resetReaderIndex 应用
public abstract ByteBuf markReaderIndex()
public abstract ByteBuf resetReaderIndex()
// 返回可读字节数
public abstract int readableBytes()
// 返回写指针
public abstract int writerIndex()
// 设置写指针
public abstract ByteBuf writerIndex(int writerIndex);
// 标记以后写指针地位,配合 resetWriterIndex 应用
public abstract ByteBuf markWriterIndex()
public abstract ByteBuf resetWriterIndex()
// 返回可写字节数
public abstract int writableBytes()
public abstract ByteBuf clear();
// 设置读写指针
public abstract ByteBuf setIndex(int readerIndex, int writerIndex)
// 指针跳过 length
public abstract ByteBuf skipBytes(int length)
// 以以后地位切分 ByteBuf todo
public abstract ByteBuf slice();
// 切割起始地位为 index,长度为 length 的 ByteBuf todo
public abstract ByteBuf slice(int index, int length);
//Returns a copy of this buffer's readable bytes. // 复制 ByteBuf todo
public abstract ByteBuf copy()
// 是否可读
public abstract boolean isReadable()
// 是否可写
public abstract boolean isWritable()
// 字节编码程序
public abstract ByteOrder order()
// 是否在间接内存申请的 ByteBuf
public abstract boolean isDirect()
// 转为 jdk.NIO 的 ByteBuffer 类
public abstract ByteBuffer nioBuffer()
  • 应用示例
public static void main(String[] args) {
    // 调配大小为 10 的内存
    ByteBuf buf = Unpooled.buffer(10);
    // 写入
    buf.writeBytes("csc".getBytes());
    // 读取
    byte[] b =  new byte[3];
    buf.readBytes(b);
    System.out.println(new String(b));
    System.out.println(buf.writerIndex());
    System.out.println(buf.readerIndex());
}
----result----
csc
3
3
  • ByteBuf 初始化时,readIndex 和 writerIndex 等于 0,调用 writeXXX() 办法 写入数据,writerIndex 会减少 (setXXX 办法无作用);调用readXXX() 办法 读取数据,则会使 readIndex 减少(getXXX 办法无作用),但不会超过 writerIndex
  • 在读取数据之后,0-readIndex 之间的 byte 数据被视为 discard,调用 discardReadBytes(),开释这部分空间,作用相似于 ByteBuffer 的 compact 办法

参考文章

  • java.nio.ByteBuffer 用法小结
  • Netty 系列 - 一分钟理解 ByteBuffer 和 ByteBuf 构造
  • Netty 之无效躲避内存透露
正文完
 0