Netty源码分析-ByteBuf

143 阅读8分钟

Jdk得Buffer的局限性

NIO缓冲区,Jdk提供的是Buffer类

image.png 可以看到7种基本类型数据都有具体的实现,那么为什么Netty还要提供自己的Buffer呢? 是因为Jdk提供的Buffer有其局限性。下面以ByteBuffer为例来说明:

  1. BtyeBuffer长度固定,不支持动态扩容,当数据大于ByteBuffer的容量的时候,会发生索引越界异常。

image.png

image.png

image.png 2. ByteBuffer只有一个位置指针position,读写的时候需要手动调用flip()rewind()等函数。

image.png

image.png 3. ByteBuffer的功能局限性大,一些高级的功能需要自己实现。

Netty的ByteBuf

ByteBuffer

Jdk的ByteBuffer只有一个位置指针,在读写操作的时候需要自行调用函数才能让程序正常运行。

ByteBuffer buffer = ByteBuffer.allocate(10);
buffer.put("Netty".getBytes());
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
System.out.println(new String(bytes));

上面是一段ByteBuffer写入数据和读取数据的程序,来看一下flip()操作前后的对比。

image.png flip()之前

position在写入数据的位置,limit限制写入的大小。

image.png flip()之后

position设置为0,limit=position,capacity不变,positon到limit之间是可读数据的内容。

ByteBuf

Netty的ByteBuf提供了两个指针,读指针readerIndex和写指针writerIndex,用来支持顺序的读操作和写操作。

image.png

readable bytes

readable bytes是实际存储数据的区域,以read或skip开头的任何操作都会读或跳过数据从当前的readindex,并且readindex会被增加为读取或跳过的字节数。如果读取操作的参数也是一个 ByteBuf,并且没有指定目标索引,那么指定的缓冲区的 writerIndex(写入索引)会一起增加。如果读取的字节长度大于实际可读的字节数,抛出数组越界异常。新分配、包装或复制的ByteeBuf readerIndex 的默认值为 0。

writable bytes

这个区域是一个尚未被使用的空间,任何以write开头的操作,都会增加writerIndex,根据写入的字节数。如果写操作的参数也是 ByteBuf,并且没有指定源索引,则指定缓冲区的readerIndex 会一起增加。

discardable bytes

这个区域的数据是已经被读操作读取了,起初这个区域的容量是0,读操作执行的时候长度会被增加,调用discardReadBytes()可以回收次区域。

调用discardReadBytes()之前

image.png

调用discardReadBytes()之后

image.png

clear()

clear之前

image.png

clear之后

image.png

ByteBuf的源码分析

主要实现类关系

image.png

从内存分配角度看,ByteBuf可以分为两类:

  • 堆内存(HeapByteBuf)字节缓冲区:内存分配和回收快,可以被jvm自动回收;缺点是如果进行Socket的I/O读写,需要额外的做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定的下降。
  • 直接内存(DirectByteBuf)字节缓冲区:在堆外进行分配,相比于堆内存分配和回收速度会比较慢,但是将它写入或者从Socket中读取时,由于少了一次内存复制,速度比堆内存快。

从内存回收角度看,ByteBuf也可以分为两类:基于对象池的ButeBuf和普通的ByteBuf。两者主要区别在于对象池的ByteBuf可以重用。

AbstractByteBuf

image.png AbstractByteBuf继承ByteBuf,ByteBuf的抽象接口能力会在AbstractByteBuf中实现。

image.png 成员变量:读写指针,最大容量

在AbstractByteBuf中并没有定义ByteBuf缓冲区的实现,AbstractByteBuf实现了通用的功能,具体能力交由子类实现。

读操作

image.png

由于都操作的接口很多,不进行一一分析,重点分析readBytes(byte[] dst, int dstIndex, int length)接口

@Override
public ByteBuf readBytes(byte[] dst, int dstIndex, int length) {
    checkReadableBytes(length);
    getBytes(readerIndex, dst, dstIndex, length);
    readerIndex += length;
    return this;
}

在读取操作之前先进行缓冲区的校验

protected final void checkReadableBytes(int minimumReadableBytes) {
    checkReadableBytes0(checkPositiveOrZero(minimumReadableBytes, "minimumReadableBytes"));
}
public static int checkPositiveOrZero(int i, String name) {
    if (i < 0) {
        throw new IllegalArgumentException(name + " : " + i + " (expected: >= 0)");
    } else {
        return i;
    }
}

长度小于0抛IllegalArgumentException异常

private void checkReadableBytes0(int minimumReadableBytes) {
    ensureAccessible();
    if (checkBounds && readerIndex > writerIndex - minimumReadableBytes) {
        throw new IndexOutOfBoundsException(String.format(
                "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",
                readerIndex, minimumReadableBytes, writerIndex, this));
    }
}

可读的字节数不够读取的长度抛出IndexOutOfBoundsException异常。

校验完成后开始读取数据,从byte[] dst字节数组dstIndex开始,读取length长度的数据

写操作

image.png

public ByteBuf writeBytes(ByteBuf src, int srcIndex, int length) {
    ensureWritable(length);
    setBytes(writerIndex, src, srcIndex, length);
    writerIndex += length;
    return this;
}

拿writeBytes(ByteBuf src, int srcIndex, int length)方法分析

@Override
public ByteBuf ensureWritable(int minWritableBytes) {
    ensureWritable0(checkPositiveOrZero(minWritableBytes, "minWritableBytes"));
    return this;
}

长度校验

public static int checkPositiveOrZero(int i, String name) {
    if (i < 0) {
        throw new IllegalArgumentException(name + " : " + i + " (expected: >= 0)");
    } else {
        return i;
    }
}
final void ensureWritable0(int minWritableBytes) {
    final int writerIndex = writerIndex();
    final int targetCapacity = writerIndex + minWritableBytes;
    // using non-short-circuit & to reduce branching - this is a hot path and targetCapacity should rarely overflow
    if (targetCapacity >= 0 & targetCapacity <= capacity()) {
        ensureAccessible();
        return;
    }
    if (checkBounds && (targetCapacity < 0 || targetCapacity > maxCapacity)) {
        ensureAccessible();
        throw new IndexOutOfBoundsException(String.format(
                "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                writerIndex, minWritableBytes, maxCapacity, this));
    }

    // Normalize the target capacity to the power of 2.
    final int fastWritable = maxFastWritableBytes();
    int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
            : alloc().calculateNewCapacity(targetCapacity, maxCapacity);

    // Adjust to the new capacity.
    capacity(newCapacity);
}
if (targetCapacity >= 0 & targetCapacity <= capacity()) {
    ensureAccessible();
    return;
}

targetCapacity >= 0 & targetCapacity <= capacity()容量满足直接返回

if (checkBounds && (targetCapacity < 0 || targetCapacity > maxCapacity)) {
    ensureAccessible();
    throw new IndexOutOfBoundsException(String.format(
            "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
            writerIndex, minWritableBytes, maxCapacity, this));
}

targetCapacity < 0 || targetCapacity > maxCapacity 小于0或者大于最大容量抛出IndexOutOfBoundsException异常

int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
        : alloc().calculateNewCapacity(targetCapacity, maxCapacity);

fastWritable >= minWritableBytes 可写最大字节数>=要写入的字节数,newCapacity就是写指针位置加上最大可写字节数,否则为2的幂

重用缓冲区

@Override
public ByteBuf discardReadBytes() {
    if (readerIndex == 0) {
        ensureAccessible();
        return this;
    }

    if (readerIndex != writerIndex) {
        setBytes(0, this, readerIndex, writerIndex - readerIndex);
        writerIndex -= readerIndex;
        adjustMarkers(readerIndex);
        readerIndex = 0;
    } else {
        ensureAccessible();
        adjustMarkers(readerIndex);
        writerIndex = readerIndex = 0;
    }
    return this;
}
if (readerIndex == 0) {
    ensureAccessible();
    return this;
}

读索引为0,说明没有可重用的缓冲区,校验访问权限后返回

if (readerIndex != writerIndex) {
    setBytes(0, this, readerIndex, writerIndex - readerIndex);
    writerIndex -= readerIndex;
    adjustMarkers(readerIndex);
    readerIndex = 0;
} 

readerIndex != writerIndex说明有被读过可以重用的缓冲区,也有未读的缓冲区数据。调用setBytes(0, this, readerIndex, writerIndex - readerIndex),将readerIndex到writerIndex之间的未读取的数据复制到0位置到writerIndex-readerIndex上。设置读写指针。

else {
    ensureAccessible();
    adjustMarkers(readerIndex);
    writerIndex = readerIndex = 0;
}

没有可读的字节数组读写指针都指向0

AbstractReferenceCountedByteBuf

此类的作用是对引用进行计数,类似于垃圾回收的引用计数算法,做自动内存回收。

ReferenceCounted

ReferenceCounted类定义了引用计数的接口,在Netty中具备引用计数能力的类都实现了此类。当一个新的ReferenceCounted类被创建时,引用计数从0开始。retain()方法增加引用计数,release()方法减小引用计数。如果引用计数减小到0,对象将会被释放,访问被释放的对象将导致违规访问。

一个实现ReferenceCounted对象是其他实现ReferenceCounted对象的容器,容器对象释放了,容器里的对象也会被释放。

方法介绍:

  • int refCnt(),返回此引用对象的计数,如果计数是0,这个对象会被回收
  • ReferenceCounted retain(),增加引用计数
  • ReferenceCounted retain(int increment);,增加指定的引用计数
  • ReferenceCounted touch(),记录此对象的当前访问位置,以便进行调试。如果确定该对象为泄露,该操作记录的信息将通过 {@link ResourceLeakDetector} 提供给你。此方法是 {@link touch(Object) touch(null)} 的快捷方式。
  • ReferenceCounted touch(Object hint),记录此对象的当前访问位置,并附加任意信息以进行调试。如果确定该对象为泄露,该操作记录的信息将通过 {@link ResourceLeakDetector} 提供给你。
  • boolean release(),减小引用计数为1,并且计数为0时释放对象。引用数量为0时返回true
  • boolean release(int decrement),减小指定的引用计数数量。引用数量为0时返回true

ReferenceCountUpdater

是ReferenceCounted的通用实现类

image.png

/*
 * Implementation notes:
 *
 * For the updated int field:
 *   Even => "real" refcount is (refCnt >>> 1)
 *   Odd  => "real" refcount is 0
 *
 * (x & y) appears to be surprisingly expensive relative to (x == y). Thus this class uses
 * a fast-path in some places for most common low values when checking for live (even) refcounts,
 * for example: if (rawCnt == 2 || rawCnt == 4 || (rawCnt & 1) == 0) { ...
 */

先来简单理解下源码中上面的注释,这对理解整个流程有很大的帮助

 For the updated int field:
*   Even => "real" refcount is (refCnt >>> 1)
*   Odd  => "real" refcount is 0
  • 偶数的引用计数是refcnt>>>1,refcnt类似于 8>>>1=4,4>>>1=2,2>>>1;奇数的引用计数为0说明就是要被回收了。所以整个逻辑可以理解为,引用计数是偶数的时候代表有被使用不能回收,为奇数的时候没被使用要被回收,是不是要释放判断奇偶性就可以了。
 (x & y) appears to be surprisingly expensive relative to (x == y). Thus this class uses
* a fast-path in some places for most common low values when checking for live (even) refcounts,
* for example: if (rawCnt == 2 || rawCnt == 4 || (rawCnt & 1) == 0) { ...

(x & y)没有(x == y)的效率高

  • 初始时引用计数的值是2,retain()后 2<<1 为4,releanse()后4>>>1为2。

image.png

image.png

image.png

UnpooledHeapByteBuf

基于堆内存的字节缓冲区,没有基于对象池去实现,每次操作都会创建一个新的UnpooledHeapByteBuf.

  1. 扩容
public ByteBuf capacity(int newCapacity) {
    checkNewCapacity(newCapacity);
    byte[] oldArray = array;
    int oldCapacity = oldArray.length;
    if (newCapacity == oldCapacity) {
        return this;
    }

    int bytesToCopy;
    if (newCapacity > oldCapacity) {
        bytesToCopy = oldCapacity;
    } else {
        trimIndicesToCapacity(newCapacity);
        bytesToCopy = newCapacity;
    }
    byte[] newArray = allocateArray(newCapacity);
    System.arraycopy(oldArray, 0, newArray, 0, bytesToCopy);
    setArray(newArray);
    freeArray(oldArray);
    return this;
}

newCapacity == oldCapacity新的容量和老的容量相等返回。新的容量大于老的容量,说明老的数据都要被拷贝到新的缓冲区中,否则(else逻辑),writerIndex() > newCapacity写指针大于新的容量,setIndex0(Math.min(readerIndex(), newCapacity), newCapacity)设置读写指针。byte[] newArray = allocateArray(newCapacity)创建新的字节数组,System.arraycopy(oldArray, 0, newArray, 0, bytesToCopy)数组拷贝,setArray(newArray)设置新的数组。

  1. 字节数组复制
public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
    checkSrcIndex(index, length, srcIndex, src.length);
    System.arraycopy(src, srcIndex, array, index, length);
    return this;
}

checkSrcIndex(index, length, srcIndex, src.length)校验,System.arraycopy(src, srcIndex, array, index, length)数组拷贝