【Netty源码】ByteBuf源码剖析

来源:互联网 发布:渝商创投网络借贷 编辑:程序博客网 时间:2024/04/28 09:43

ByteBuf概述

1.ByteBuf与ByteBuffer的区别

  • ByteBuf不是对ByteBuffer的封装,而是重新实现了一个缓冲区。ByteBuffer只使用了一个position指针来记录当前的读写位置,ByteBuf使用了两个指针readerIndex, writerIndex分别来记录当前的读写位置,使用起来更加简单和方便。

  • ByteBuffer是一个固定长度的缓冲区,当put方法要写的数据大于可写的容量时会抛出异常。ByteBuf改进了这个设计,支持自动扩容。每次put之前会检查是否可以完全写入,如果不能,就会自动扩展ByteBuf的容量,保证put方法不会抛出异常。

  • 和ByteBuffer一样,ByteBuf也支持堆内缓冲区和堆外直接缓冲区,根据经验来说,底层IO处理线程的缓冲区使用堆外直接缓冲区,减少一次IO复制。业务消息的编解码使用堆内缓冲区,分配效率更高,而且不涉及到内核缓冲区的复制问题。

  • ByteBuf的堆内缓冲区又分为内存池缓冲区PooledByteBuf和普通内存缓冲区UnpooledHeapByteBuf。PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。在高并发的情况下推荐使用PooledByteBuf,可以节约内存的分配。在性能能够保证的情况下,可以使用UnpooledHeapByteBuf,实现比较简单

2.ByteBuf的特点

  • 可以自定义缓冲类型;
  • 通过内置的复合缓冲类型,实现零拷贝(zero-copy);
  • 不需要调用flip()来切换读/写模式;
  • 读取和写入索引分开;
  • 方法链;
  • 引用计数;
  • Pooling(池)。

3.实现机制

  • ByteBuf实际上是在一个抽象的字节数组byte[]上进行读/写操作的集合。它提供了两个指针变量用来支持读写操作:readerIndex和writerIndex。

  • 在对象初始化的时候,readerIndex和writerIndex的值为0,随着读操作和写操作的进行,writerIndex和readerIndex都会增加,不过readerIndex不能超过writerIndex,

  • 在进行读取操作之后,0到readerIndex之间的空间会被视为discard,调用ByteBuf的discardReadBytes方法,可以对这部分空间进行释放重用,类似于ByteBuffer的compact操作,对缓冲区进行压缩

  • eaderIndex到writerIndex的空间,相当于ByteBuffer的position到limit的空间,可以对其进行读取,WriterIndex到capacity的空间,则相当于ByteBuffer的limit到capacity的空间,是可以继续写入的

这里写图片描述

分析:由上图可知,ByteBuf真正可读取的内容长度是writerIndex - readerIndex。

ByteBuf的常见操作

1.readBytes

public ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) {    checkReadableBytes(length);    getBytes(readerIndex, dst, dstIndex, length);    readerIndex += length;    return this;}protected final void checkReadableBytes(int minimumReadableBytes) {    ensureAccessible();    if (minimumReadableBytes < 0) {        throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)");    }    if (readerIndex > writerIndex - minimumReadableBytes) {        throw new IndexOutOfBoundsException(String.format(                "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s",                readerIndex, minimumReadableBytes, writerIndex, this));    }}

分析:

  • 在进行读操作之前,首先对缓冲区可用的空间进行校验。如果要读取的字节长度小于0,就会抛出IllegalArgumentException异常,如果要读取的字节长度大于已写入的字节长度,会抛出IndexOutOfBoundsException异常。

  • 通过校验之后,调用getBytes方法,从当前的readerIndex开始,读取length长度的字节数据到目标dst中,由于不同的子类实现不一样,getBytes是个抽象方法,由对应的子类去实现。如果读取数据成功,readerIndex将会增加相应的length。

2.writeBytes

public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {    ensureWritable(length);    setBytes(writerIndex, src, srcIndex, length);    writerIndex += length;    return this;}public ByteBuf ensureWritable(int minWritableBytes) {    if (minWritableBytes < 0) {        throw new IllegalArgumentException(String.format(                "minWritableBytes: %d (expected: >= 0)", minWritableBytes));    }    if (minWritableBytes <= writableBytes()) {        return this;    }    if (minWritableBytes > maxCapacity - writerIndex) {        throw new IndexOutOfBoundsException(String.format(                "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",                writerIndex, minWritableBytes, maxCapacity, this));    }    // Normalize the current capacity to the power of 2.    int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes);    // Adjust to the new capacity.    capacity(newCapacity);    return this;}private int calculateNewCapacity(int minNewCapacity) {    final int maxCapacity = this.maxCapacity;    final int threshold = 1048576 * 4; // 4 MiB page    if (minNewCapacity == threshold) {        return threshold;    }    // If over threshold, do not double but just increase by threshold.    if (minNewCapacity > threshold) {        int newCapacity = minNewCapacity / threshold * threshold;        if (newCapacity > maxCapacity - threshold) {            newCapacity = maxCapacity;        } else {            newCapacity += threshold;        }        return newCapacity;    }    // Not over threshold. Double up to 4 MiB, starting from 64.    int newCapacity = 64;    while (newCapacity < minNewCapacity) {        newCapacity <<= 1;    }    return Math.min(newCapacity, maxCapacity);}//UnpooledHeapByteBuf的capacity实现public ByteBuf capacity(int newCapacity) {    ensureAccessible();    if (newCapacity < 0 || newCapacity > maxCapacity()) {        throw new IllegalArgumentException("newCapacity: " + newCapacity);    }    int oldCapacity = array.length;    if (newCapacity > oldCapacity) {        byte[] newArray = new byte[newCapacity];        System.arraycopy(array, 0, newArray, 0, array.length);        setArray(newArray);    } else if (newCapacity < oldCapacity) {        byte[] newArray = new byte[newCapacity];        int readerIndex = readerIndex();        if (readerIndex < newCapacity) {            int writerIndex = writerIndex();            if (writerIndex > newCapacity) {                writerIndex(writerIndex = newCapacity);            }            System.arraycopy(array, readerIndex, newArray, readerIndex, writerIndex - readerIndex);        } else {            setIndex(newCapacity, newCapacity);        }        setArray(newArray);    }    return this;}

分析:

  • 读操作是将源字节数组从srcIndex开始,length长度的数据写入到当前的ByteBuf中的。 一开始需要对写入数组的字节数进行校验,如果写入长度小于0,将会抛出IllegalArgumentException异常,

  • 如果写入字节数小于当前ByteBuf的可写入字节数,则通过检验。如果写入字节数大于缓冲区最大可动态扩展的容量maxCapacity,就会抛出IndexOutOfBoundsException异常,否则的话,就会通过动态扩展来满足写入需要的字节数。

  • 首先通过calculateNewCapacity计算出重新扩展后的容量,然后调用capacity方法进行扩展,不同的子类有不同实现,所以也是一个抽象方法。

  • 计算扩展容量,首先设置门阀值为4m,如果要扩展的容量等于阀值就使用阀值作为缓冲区新的容量,如果大于阀值就以4M作为步长,每次增加4M,如果扩展期间,要扩展的容量比最大可扩展容量还大的话,就以最大可扩展容量maxCapacity为新的容量。否则的话,就从64开始倍增,直到倍增之后的结果大于要扩展的容量,再把结果作为缓冲区的新容量。

  • 通过先倍增再步长来扩展容量,如果我们只是writerIndex+length的值作为缓冲区的新容量,那么再以后进行写操作的时候,每次都需要进行容量扩展,容量扩展的过程需要进行内存复制,过多内存复制会导致系统的性能下降,之所以是倍增再部长,在最初空间比较小的时候,倍增操作并不会带来太多的内存浪费,但是内存增长到一定的时候,再进行倍增的时候,就会对内存造成浪费,因此,需要设定一个阀值,到达阀值之后就通过步长的方法进行平滑的增长。

3.clear

public ByteBuf clear() {    readerIndex = writerIndex = 0;    return this;}

分析:clear操作只是把readerIndex和writerIndex设置为0,不会对存储的数据进行修改。

4.discardReadBytes

public ByteBuf discardReadBytes() {    ensureAccessible();    if (readerIndex == 0) {        return this;    }    if (readerIndex != writerIndex) {        setBytes(0, this, readerIndex, writerIndex - readerIndex);        writerIndex -= readerIndex;        adjustMarkers(readerIndex);        readerIndex = 0;    } else {        adjustMarkers(readerIndex);        writerIndex = readerIndex = 0;    }    return this;}protected final void adjustMarkers(int decrement) {    int markedReaderIndex = this.markedReaderIndex;    if (markedReaderIndex <= decrement) {        this.markedReaderIndex = 0;        int markedWriterIndex = this.markedWriterIndex;        if (markedWriterIndex <= decrement) {            this.markedWriterIndex = 0;        } else {            this.markedWriterIndex = markedWriterIndex - decrement;        }    } else {        this.markedReaderIndex = markedReaderIndex - decrement;        markedWriterIndex -= decrement;    }}

分析:

  • 可以通过discardReadByte方法去重用已经读取过的缓冲区。

  • 首先对readerIndex进行判断:

    • 如果readerIndex等于0,就说明没有读取数据,没有可以用来重用的空间,直接返回;
    • 如果readerIndex大于0且不等于writerIndex的话,说明有进行数据读取被丢弃的缓冲区,也有还没有被读取的缓冲区。调用setBytes方法进行字节数组的复制,将没被读取的数据移动到缓冲区的起始位置,重新去设置readerIndex和writerIndex,readerIndex为0,writerIndex为原writerIndex-readerIndex;同时,也需要对mark进行重新设置。
  • 首先对markedReaderIndex进行备份然后跟decrement进行比较,如果markedReaderIndex比decrement小的话,markedReaderIndex设置为0,再用markedWriterIndex跟decrement比较,如果小于的话,markedWriterIndex也设置为0,否则的话markedWriterIndex较少decrement;

  • 如果markedReaderIndex比decrement大的话,markedReaderIndex和markedReaderIndex都减去decrement就可以了。

  • 如果readerIndex等于writerIndex的话,说明没有可以进行重用的缓冲区,直接对mark重新设置就可以了,不需要内存复制。

ByteBuf功能类

1.ByteBuf的类关系图

这里写图片描述

2.AbstractDerivedByteBuf

(1)功能

ByteBuf的抽象基类,实现了包装另一个ByteBuf功能。在AbstractByteBuf的基础上提供了一下功能:

  • refCnt:获得该对象的引用计数;
  • retain:增加该对象的引用计数(无参数:+1;有参数:+指定的increment);
  • release:减少该对象的引用计数(无参数:-1;有参数:-指定的increment),当引用计数减少到0时,释放该对象。返回值为true,当且仅当引用计数变为0和该对象已释放。
  • internalNioBuffer:内部实现就是简单的调用nioBuffer(index, length);
  • nioBuffer:得到内部buffer的一个区域包装,即得到buffer的子区域作为NIO ByteBuffer,返回的ByteBuffer内容不会再受到原buffer索引或内容改变的影响。

(2)派生类

  • DuplicatedByteBuf
    简单的把所有的数据访问请求发送给内部的buffer。推荐使用ByteBuf.duplicate()来创建该对象,而不是直接调用本身的构造函数。

  • ReadOnlyByteBuf
    将原有的ByteBuf包装为制度的ByteBuf,所有的写请求都将被禁止。推荐使用Unpooled.unmodifiableBuffer(ByteBuf)来创建该对象,而不是直接调用本身的构造函数。
    对象与内部的buffer使用相同的索引标记

  • SlicedByteBuf
    仅暴露内部buffer的一个子区域,即切片。推荐使用ByteBuf.slice()或ByteBuf.slice(int, int)来创建该对象,而不是直接调用本身的构造函数。

3.AbstractReferenceCountedByteBuf

(1)定义

AbstractReferenceCountedByteBuf是ByteBuf实现对引用进行计数的基类,用来跟踪对象的分配和销毁,实现自动内存回收

(2)成员变量

  • refCntUpdater refCntUpdater是一个AtomicIntegerFieldUpdater类型的成员变量,它可以对成员变量进行原子性更新操作,达到线程安全。

  • REFCNT_FIELD_OFFSET REFCNT_FIELD_OFFSET是标识refCnt字段在AbstractReferenceCountedByteBuf的内存地址,在UnpooledDirectByteBuf

  • PooledDirectByteBuf两个子类中都会使用到这个偏移量。

  • refCnt volatile修饰保证变量的线程可见性,用来跟踪对象的引用次数

(3)对象引用计数器

  • 每调用retain方法一次,引用计数器就会加一。retain方法通过自旋对引用计数器进行加一操作,引用计数器的初始值为1,只要程序是正确执行的话,它的最小值应该为1,当申请和释放次数相等的时候,对应的ByteBuf就会被回收。

  • 当次数为0时,表明对象被错误的引用,就会抛出IllegalReferenceCountException异常,如果次数等于Integer类型的最大值,就会抛出
    IllegalReferenceCountException异常。

  • retain通过refCntUpdater的compareAndSet方法进行原子操作更新,compareAndSet会使用获取的值与期望值进行比较,如果在比较器件,有其他线程对变量进行修改,那么比较失败,会再次自旋,获取引用计数器的值再次进行比较,否则的话,就会进行加一操作,退出自旋。

  • release方法的话与retain方法类似,也是通过自旋循环进行判断和更新,不过当refCnt的值等于1的时候,表明引用计数器的申请跟释放次数一样,对象引用已经不可达了,对象应该要被垃圾收集回收掉了,调用deallocate方法释放ByteBuf对象

  • retain方法源码如下

@Override    public ByteBuf retain() {        for (;;) {            int refCnt = this.refCnt;            final int nextCnt = refCnt + 1;            if (nextCnt <= 1) {                throw new IllegalReferenceCountException(refCnt, 1);            }            if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {                break;            }        }        return this;    }
  • release方法源码如下
public final boolean release() {    for (;;) {        int refCnt = this.refCnt;        if (refCnt == 0) {            throw new IllegalReferenceCountException(0, -1);        }        if (refCntUpdater.compareAndSet(this, refCnt, refCnt - 1)) {            if (refCnt == 1) {                deallocate();                return true;            }            return false;        }    }}

ByteBuf辅助类

1.ByteBufHolder

  • 应用
    HTTP协议的请求消息和应答消息都可以携带消息体,这个消息体在NIO ByteBuffer中就是ByteBuffer对象,在Netty中就是ByteBuf对象。而不同的协议消息体中可以含有不同的协议字段和功能,因此需要对ByteBuf进行包装和抽象。

2.ByteBufAllocator

  • ByteBufAllocator是字节缓冲区分配器,

  • 根据Netty字节缓冲区的实现不同,分为两种不同的分配器PooledByteBufAllocator和UnpooledByteBufAllocator。他们提供了不同ByteBuf的分配方法。

3.CompositeByteBuf

  • CompositeByteBuf允许将多个ByteBuf的实例组装到一起,形成一个统一的视图。

  • 若某个协议POJO对象包含2部分:消息头和消息体,它们都是ByteBuf对象。当需要对消息进行编码的时候需要进行整合,如果使用JDK的话,有以下2种思路:

    • 将某个ByteBuffer复制到另一个ByteBuffer中,或者创建一个新的ByteBuffer
    • 通过List等容器,统一维护和处理

4.ByteBufUtil

  • 工具类,提供静态方法用于操作ByteBuf对象。

  • 最有用的是对字符串进行编码和解码

四种内存分配方式

1.类关系图

这里写图片描述

2.UnpooledHeapByteBuf

(1)定义

UnpooledHeapByteBuf是一个非线程池实现的在堆内存进行内存分配的字节缓冲区,在每次IO操作的都会去创建一个UnpooledHeapByteBuf对象,如果频繁地对内存进行分配或者释放会对性能造成影响。

(2)成员变量

  • ByteBufAllocator 用于内存分配
  • array 字节数组作为缓冲区,用于存储字节数据
  • ByteBuffer 用来实现Netty ByteBuf 到Nio ByteBuffer的变换

(3)动态扩展缓冲区

  • 调用capacity方法动态扩展缓冲区,首先要对扩展容量进行校验,如果新容量的大小小于0或者大于最大可扩展容量maxCapacity的话,抛出IllegalArgumentException异常。

  • 通过校验之后,如果新扩展容量比原来大的话,则创建一个新的容量为新扩展容量的字节数组缓冲区,然后调用System.arraycopy进行内存复制,将旧的数据复制到新数组中去,然后用setArray进行数组替换。动态扩展之后需要原来的视图tmpNioBuffer设置为控。

  • 如果新的容量小于当前缓冲区容量的话,不需要进行动态扩展,但是需要截取部分数据作为子缓冲区。

  • 首先对当前的readerIndex是否小于newCapacity,如果小于的话继续对writerIndex跟newCapacity进行比较,如果writerIndex大于newCapacity的话,就将writerIndex设置为newCapacity,更新完索引之后就通过System.arrayCopy内存复制将当前可读的数据复制到新的缓冲区字节数组中。

  • 如果newCapacity小于readerIndex的话,说明没有新的可读数据要复制到新的字节数组缓冲区中,只需要把writerIndex跟readerIndex都更新为newCapacity既可,最后调用setArray更换字节数组。

3.PooledByteBuf

在Netty4之后加入内存池管理,通过内存池管理比之前ByteBuf的创建性能得到了极大提高。

(1)PoolArena

  • 在内存分配中,为了能够集中管理内存的分配及释放,同时提供分配和释放内存的性能,一般都是会先预先分配一大块连续的内存,不需要重复频繁地进行内存操作,那一大块连续的内存就叫做memory Arena,而PoolArena是Netty的内存池实现类。

  • 在Netty中,PoolArena是由多个Chunk组成的,而每个Chunk则由多个Page组成。PoolArena是由Chunk和Page共同组织和管理的。

(2)PoolChunk

  • Page 可以用来分配的最小内存块单位

  • Chunk page的集合

  • PoolChunk主要负责内存块的分配及释放,chunk中的page会构建成一颗二叉树,默认情况下page的大小是8K,chunk的大小是2^11 page,即16M,构成了11层的二叉树,最下面一层的叶子节点有8192个,与page的数目一样,每一次内存的分配必须保证连续性,方便内存操作。

  • 每个节点会记录自己在Memory Area的偏移地址,当一个节点表示的内存区域被分配之后,那么该节点会被标志为已分配,该节点的所有子节点的内存请求都会忽略。每次内存分配的都是8k(2^n)大小的内存块,当需要分配大小为chunkSize/(2^k)的内存端时,为了找到可用的内存段,会从第K层左边开始寻找可用节点。

(3)PoolSubpage

  • 当对于小于一个Page的内存分配的时候,每个Page会被划分为大小相等的内存块,它的大小是根据第一次申请内存分配的内存块大小来决定的。

  • 一个Page只能分配与第一次内存内存的内存块的大小相等的内存块,如果想要想要申请大小不想等的内存块,只能在新的Page上申请内存分配了。

  • Page中的存储区域的使用情况是通过一个long数组bitmap来维护的,每一位表示一个区域的占用情况。

4.PooledDirectByteBuf

(1)创建字节缓冲区

  • 由于内存池实现,每次创建字节缓冲区的时候,不是直接new,而是从内存池中去获取,然后设置引用计数器跟读写Index,跟缓冲区最大容量返回。

  • 源码实现

static PooledHeapByteBuf newInstance(int maxCapacity) {    PooledHeapByteBuf buf = RECYCLER.get();    buf.reuse(maxCapacity);    return buf;}final void reuse(int maxCapacity) {    maxCapacity(maxCapacity);    setRefCnt(1);    setIndex0(0, 0);    discardMarks();}

(2)复制字节缓冲区实例

  • copy方法可以复制一个字节缓冲区实例,与原缓冲区独立。

  • 首先要对index和length进行合法性判断,然后调用PooledByteBufAllocator的directBuffer方法分配一个新的缓冲区。newDirectBuffer方法是一个抽象方法,对于不同的子类有不同的实现。

  • 如果是unpooled的话,会直接创建一个新的缓冲区,如果是pooled的话,它会从内存池中获取一个可用的缓冲区。

  • 源码如下

public ByteBuf copy(int index, int length) {    checkIndex(index, length);    ByteBuf copy = alloc().directBuffer(length, maxCapacity());    copy.writeBytes(this, index, length);    return copy;}public ByteBuf directBuffer(int initialCapacity, int maxCapacity) {    if (initialCapacity == 0 && maxCapacity == 0) {        return emptyBuf;    }    validate(initialCapacity, maxCapacity);    return newDirectBuffer(initialCapacity, maxCapacity);}// PooledByteBufAllocator protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {    PoolThreadCache cache = threadCache.get();    PoolArena<ByteBuffer> directArena = cache.directArena;    ByteBuf buf;    if (directArena != null) {        buf = directArena.allocate(cache, initialCapacity, maxCapacity);    } else {        if (PlatformDependent.hasUnsafe()) {            buf = new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);        } else {            buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);        }    }    return toLeakAwareBuffer(buf);}//UnpooledByteBufAllocatorprotected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {    ByteBuf buf;    if (PlatformDependent.hasUnsafe()) {        buf = new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);    } else {        buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);    }    return toLeakAwareBuffer(buf);}

5.UnpooledDirectByteBuf

(1)构造方法

 protected UnpooledDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {        super(maxCapacity);        if (alloc == null) {            throw new NullPointerException("alloc");        }        if (initialCapacity < 0) {            throw new IllegalArgumentException("initialCapacity: " + initialCapacity);        }        if (maxCapacity < 0) {            throw new IllegalArgumentException("maxCapacity: " + maxCapacity);        }        if (initialCapacity > maxCapacity) {            throw new IllegalArgumentException(String.format(                    "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));        }        this.alloc = alloc;        setByteBuffer(ByteBuffer.allocateDirect(initialCapacity));    }

(2)capacity方法源码

public ByteBuf capacity(int newCapacity) {          ensureAccessible();     // 1           if (newCapacity < 0 || newCapacity > maxCapacity()) {              throw new IllegalArgumentException("newCapacity: " + newCapacity);          }          int readerIndex = readerIndex();          int writerIndex = writerIndex();          int oldCapacity = capacity;          if (newCapacity > oldCapacity) {   // 2              ByteBuffer oldBuffer = buffer;              ByteBuffer newBuffer = allocateDirect(newCapacity);  //2.1              oldBuffer.position(0).limit(oldBuffer.capacity());          //2.2              newBuffer.position(0).limit(oldBuffer.capacity());        //2.3              newBuffer.put(oldBuffer);                                            //2.4              newBuffer.clear();                                                         //2.5              setByteBuffer(newBuffer);                                                     } else if (newCapacity < oldCapacity) { //3              ByteBuffer oldBuffer = buffer;              ByteBuffer newBuffer = allocateDirect(newCapacity);              if (readerIndex < newCapacity) {                  if (writerIndex > newCapacity) {                      writerIndex(writerIndex = newCapacity);                  }                  oldBuffer.position(readerIndex).limit(writerIndex);                  newBuffer.position(readerIndex).limit(writerIndex);                  newBuffer.put(oldBuffer);                  newBuffer.clear();              } else {                  setIndex(newCapacity, newCapacity);              }              setByteBuffer(newBuffer);          }          return this;      }  

分析:

  • <1,检测访问性,可达性,就是引用数必须大于0,否则该ByteBuf的内部空间已经被回收了(堆外内存)

  • <2,扩容操作,思路新建一个缓存区,然后将原先缓存区的数据全部写入到新的缓存区,然后释放旧的缓存区。

  • <2.1、2.2,申请一个直接缓存区,然后将原缓冲区的postion设置为0,将limit设置为capacity,处于释放状态(从缓存区读)。

  • <2.3,将新缓存区的postion,limit属性设置为0,老缓存区limit。

  • <2.4,将原缓冲区写入到新的缓存区,然后将缓存区置的position设置为0,limt设置为capacity,其实这里设置position,capacity的意义不大,因为ByteBuf并不会利用内部的ByteBuffer的limit,postion属性,而是使用readerIndex,wriateIndex。

  • <2.5,关联新的ByteBuffer,并释放原缓存区的空间。

  • <3,压缩缓存区。实现思路是新建一个缓存区,如果readerIndex大于新建的ByteBuffer的capacity,则无需将旧的缓存区内容写入到新的缓存区中。如果readerIndex小于新capacity,那需要将readerIndex 至( Math.min(writerIndex, newCapacity) )直接的内容写入到新的缓存,然后释放旧的缓存区。值得注意一点是,如果readerIndex > newCapcity,该ByteBuf的 readerIndex,writerIndex将会被设置为容量值,意味着如果不对readerIndex设置为0,或调用discardReadBytes,该缓存区是不可以使用的,

(3)setByteBuffer方法源码

private void setByteBuffer(ByteBuffer buffer) {          ByteBuffer oldBuffer = this.buffer;          if (oldBuffer != null) {              if (doNotFree) {                  doNotFree = false;              } else {                  freeDirect(oldBuffer);              }          }          this.buffer = buffer;          tmpNioBuf = null;          capacity = buffer.remaining();      }

(4)小结

UnpooledDirectByteBuf是一个聚合对象,内部维护了一个java.nio.ByteBuffer的直接对外内存空间,释放UnpooledDirectByteBuf中堆外内存的时机,就是在UnpooledDirectByteBuf被java垃圾回收的时候,应该于此同时需要释放指向的堆外内存,但堆外内存不受JVM GC的管理,所以我们只有感知到UnpooledDirectByteBuf被JVM虚拟机回收后(虚引用),手动去释放堆外内存,

规避内存泄漏

1.为什么要有引用计数器

  • Netty里四种主要的ByteBuf,

  • 其中UnpooledHeapByteBuf 底下的byte[]能够依赖JVM GC自然回收;而UnpooledDirectByteBuf底下是DirectByteBuffer,除了等JVM GC,最好也能主动进行回收;而PooledHeapByteBuf 和 PooledDirectByteBuf,则必须要主动将用完的byte[]/ByteBuffer放回池里,否则内存就要爆掉。所以,Netty ByteBuf需要在JVM的GC机制之外,有自己的引用计数器和回收过程。

  • 一下又回到了C的冰冷时代,自己malloc对象要自己free。 但和C时代又不完全一样,内有引用计数器,外有JVM的GC,情况更为复杂。

2.引用计数器常识

  • 计数器基于 AtomicIntegerFieldUpdater,因为ByteBuf对象很多,如果都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只需要一个全局的静态变量。
    所有ByteBuf的引用计数器初始值为1。

  • 调用release(),将计数器减1,等于零时, deallocate()被调用,各种回收。

  • 调用retain(),将计数器加1,即使ByteBuf在别的地方被人release()了,在本Class没喊cut之前,不要把它释放掉。

  • 由duplicate(), slice()和order(ByteOrder)所创建的ByteBuf,与原对象共享底下的buffer,也共享引用计数器,所以它们经常需要调用retain()来显示自己的存在。

  • 当引用计数器为0,底下的buffer已被回收,即使ByteBuf对象还在,对它的各种访问操作都会抛出异常。

3.谁来负责Release

  • 在C时代,我们喜欢让malloc和free成对出现,而在Netty里,因为Handler链的存在,ByteBuf经常要传递到下一个Hanlder去而不复还,所以规则变成了谁是最后使用者,谁负责释放。

  • 若出现异常情况,ByteBuf没有成功传递到下一个Hanlder,还在自己地界里的话,一定要进行释放。 谁最后谁负责

  • InBound Message
    假设每一个Handler都把消息往下传,Handler并也不知道谁是启动Netty时所设定的Handler链的最后一员,所以Netty会在Handler链的最末补一个TailHandler,如果此时消息仍然是ReferenceCounted类型就会被release掉。

  • OutBound Message
    要发送的消息通常由应用所创建,并调用 ctx.writeAndFlush(msg) 进入Handler链。在每一个Handler中的处理类似InBound Message,最后消息会来到HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。

4.内存泄漏检测

  • 内存泄漏,主要是针对池化的ByteBuf。ByteBuf对象被JVM GC掉之前,没有调用release()去把底下的DirectByteBuffer或byte[]归还到池里,会导致池越来越大。

  • 非池化的ByteBuf,即使像DirectByteBuf那样可能会用到System.gc(),但终归会被release掉的,不会出大事。



本人才疏学浅,若有错,请指出,谢谢!
如果你有更好的建议,可以留言我们一起讨论,共同进步!
衷心的感谢您能耐心的读完本篇博文!

参考链接:堆外内存的回收机制分析

原创粉丝点击