netty(十一)源码分析之ByteBuf 三

4.操作索引

与索引相关的操作主要涉及设置读写索引、mark和rest等。我们看下ByteBuf的markReaderIndex()方法:

public ByteBuf markReaderIndex() {
        markedReaderIndex = readerIndex;
        return this;
    }

5.重用缓冲区

前面介绍功能的时候已经简单讲解了如何通过discardReadBytes和discardSomeReadBytes方法重用已经读取过的缓冲区,下面结合discardReadBytes方法的实现进行分析。

首先对读索引进行判断,如果为0说明没有可重用的缓冲区,直接返回。如果读索引大于0且读索引不等于写索引,说明缓冲区中既有已经读取过的被丢弃的缓冲区,也有尚未读取的可读缓冲区。调用setBytes(0,this,readerIndex,writerIndex-readerIndex)方法进行字节数组复制。将尚未读取的字节数组复制到缓冲区的起始位置,然后重新设置读写索引,读索引设置为0,写索引设置为之前的写索引减去读索引(重用的缓冲区长度)。

    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;
    }
在设置读写索引的同时,需要同时调整markedReaderIndex和markedWriterIndex。

首先对备份的markedReaderIndex和需要减少的decrement进行判断,如果小于需要减少的值,则将markedReaderIndex设置为0.注意,无论是markedReaderIndex还是markedwriterIndex,它的取值都不能小于0.如果markedWriterIndex也小于需要减少的值,则markedWriterIndex置为0,否则,markedWriterIndex减去decrement之后的值就是新的markedWriterIndex。

    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;
        }
    }
如果需要减小的值小于markedReaderIndex,则也一定小于markedWriterIndex,markedReaderIndex和markedWriterIndex的新值就是减去decrement之后的取值。

6.skipBytes

在解码的时候,有时候需要丢弃非法的数据报,或者跳跃过不需要读取的字节或字节数组,此时,使用skipBytes方法就非常方便。它可以忽略指定长度的字节数组,读操作时直接跳过这些数据读取后面的可读缓冲区,详细代码如下:

public ByteBuf skipBytes(int length) {
        checkReadableBytes(length);

        int newReaderIndex = readerIndex + length;
        if (newReaderIndex > writerIndex) {
            throw new IndexOutOfBoundsException(String.format(
                    "length: %d (expected: readerIndex(%d) + length <= writerIndex(%d))",
                    length, readerIndex, writerIndex));
        }
        readerIndex = newReaderIndex;
        return this;
    }


AbastractReferenceCountedByteBuf源码分析

从类的名字就可以看出该类主要是对引用进行计数,类似于JVM内存回收的对象引用计数器,用于跟踪对象的分配和销毁,做自动内存回收。
下面通过源码看它的具体实现。
1.成员变量
private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater =
            AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");

    private static final long REFCNT_FIELD_OFFSET;

    static {
        long refCntFieldOffset = -1;
        try {
            if (PlatformDependent.hasUnsafe()) {
                refCntFieldOffset = PlatformDependent.objectFieldOffset(
                        AbstractReferenceCountedByteBuf.class.getDeclaredField("refCnt"));
            }
        } catch (Throwable t) {
            // Ignored
        }

        REFCNT_FIELD_OFFSET = refCntFieldOffset;
    }

    @SuppressWarnings("FieldMayBeFinal")
    private volatile int refCnt = 1;
首先看第一个字段refCntUpdater,它是AtomicIntegerFieldUpdater类型变量,通过原子的方式对成员变量进行更新等操作,以实现线程安全,清除锁。第二个字段是REFCNT_FIELD_OFFSET,它用于标示refCnt字段在AbstractReferenceCountedByteBuf中的内存地址。该内存地址的获取是JDK实现强相关的,如果使用SUN的JDK,它通过sum.mic.Unsafe的objectFieldObject接口来获得,ByteBuf的实现子类UnpooledUnsafeDirectByteBuf和PooledUnsafeDirectByteBuf会使用到这个偏移量。
最后定义了一个volatile修饰的refCnt字段用于跟踪对象的引用次数,使用volatile是为了解决多线程并发访问的可见性问题。

2.对象引用计数器
每调用一次retain方法,引用计数器就会加一,由于可能存在多线程并发调用的场景,所以它的累加必须是线程安全的。
public ByteBuf retain() {
        for (;;) {
            int refCnt = this.refCnt;
            if (refCnt == 0) {
                throw new IllegalReferenceCountException(0, 1);
            }
            if (refCnt == Integer.MAX_VALUE) {
                throw new IllegalReferenceCountException(Integer.MAX_VALUE, 1);
            }
            if (refCntUpdater.compareAndSet(this, refCnt, refCnt + 1)) {
                break;
            }
        }
        return this;
    }
通过自旋对引用计数器进行加一操作,由于引用计数器的初始值为1,如果申请和释放操作能够保证正常使用,则它的最小值为1.当被释放和被申请的次数相等时,就调用回收方法回收当前的ByteBuf对象。如果为0,说明对象被意外、错误地引用,抛出IllegalReferenceCountException。如果引用计数达到整形的最大值,抛出越界的异常IllegalReferenceCountException。最后通过compareAndSet进行原子更新。

下面看下释放引用计数器代码,
与retain类似,它也是在一个自旋循环里面进行判断和更新的。需要注意的是:当refCnt==1时意味着申请和释放相等,说明对象引用已经不可达,该对象需要被释放和垃圾回收掉,则通过调用deallocate方法来释放ByteBuf对象。
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;
            }
        }
    }

UnpooledHeapByteBuf源码分析

UnpooledHeapByteBuf是基于堆内存进行内存分配的字节缓冲区,它没有基于对象池技术实现,这就意味着每次I/O的读写都会创建一个新的UnpooledHeapByteBuf,频繁进行大块内存的分配和回收对性能会造成一定影响,但是相比于堆外内存的申请和释放,它的成本还是会低一些。

相比于PooledHeapByteBuf,UnpooledHeapByteBuf的实现原理更加简单,也不容易出现内存管理方面的问题,因此在满足性能的情况下,推荐使用UnpooledHeapByteBuf。
我们看下UnpooledHeapByteBuf的代码实现:
1.成员变量
首先看下UnpooledHeapByteBuf的成员变量定义:
    private final ByteBufAllocator alloc;
    private byte[] array;
    private ByteBuffer tmpNioBuf;
首先,它聚合了一个ByteBufAllocator,用于UnpooledHeapByteBuf的内存分配,紧接着定义了一个byte数组作为缓冲区,最后定义了一个ByteBuffer类型的tmpNioBuf变量用于实现Netty ByteBuf到JDK NIO ByteBuffer的转换。

事实上,如果使用JDK的ByteBuffer替换byte数组也是可行的,直接使用byte数组的根本原因就是提升性能和更加便捷地进行位操作。JDK的ByteBuffer底层实现也是byte数组。

2.动态扩展缓冲区
我们之前说过AbstractByteBuf的自动扩张,再一起看下UnpooledHeapByteBuf中的自动扩展。
判断新的容量值是否大于当前的缓冲区容量,如果大于则需要进行动态扩展,通过byte[] newArray = new byte[newCapacity]创建新的缓冲区字节数组,然后通过System.arraycopy进行内存复制,将旧的字节数组复制到新创建的字节数组中,然后调用setArray替换旧的字节数组。需要注意的是,当动态扩展完成后,需要将原来的视图tmpNioBuf设置为空。
    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;
    }
    private void setArray(byte[] initialArray) {
        array = initialArray;
        tmpNioBuf = null;
    }

4.转换成JDK ByteBuffer

熟悉JDK NIO ByteBuffer的读者可能会想到转换非常简单,因为ByteBuf基于byte数组实现。NIO的ByteBuffer提供了wrap方法,可以将byte数组转换成ByteBuffer对象。
我们看一下UnpooledHeapByteBuf的实现。
我们发现,它还调用了ByteBuffer的slice方法。由于每次调用nioBuffer都会创建一个新的ByteBuffer,因此此处的slice方法起不到重用缓冲区内容的效果,只能保证读写索引的独立性。
    public ByteBuffer nioBuffer(int index, int length) {
        ensureAccessible();
        return ByteBuffer.wrap(array, index, length).slice();
    }
    public static ByteBuffer wrap(byte[] array,
                                    int offset, int length)
    {
        try {
            return new HeapByteBuffer(array, offset, length);
        } catch (IllegalArgumentException x) {
            throw new IndexOutOfBoundsException();
        }
    }
由于UnpooledDerectByteBuf与UnpooledHeapByteBuf的实现原理相同,不同之处就是它内部缓冲区由java.nio.DirectByteBuffer实现。


PooledByteBuf内存池原理分析

由于ByteBuf内存池的实现涉及到的类和数据结构较多,限于篇幅,我们仅从设计原理角度来分析内存池的实现。
1.poolArena
Arena本身是指一块区域,在内存管理中,Memory Arena是指内存中的一大块连续的区域,PoolArena就是Netty的内存池实现类。
为了集中管理内存的分配和释放,同时提高分配和释放内存时候的性能,很多框架和应用都会通过预先申请一大块内存,然后通过提供相应的分配和释放接口来使用内存。这样一来,对内存的管理就被集中到几个类或函数中,由于不再频繁使用系统调用来申请和释放内存,应用或者系统的性能也会大大提高。在这种设计思路下,预先申请的那一大块内存就被称为Memory Arena。
不同的框架,Memory Arena的实现不同,Netty的PoolArena是由多个Chunk组成的大块内存区域,而每个Chunk则由一个或者多个Page组成,因此,对内存的组织和管理也就主要集中在如何管理和组织Chunk和Page了。PoolArena中的内存Chunk定义如下所示:
    final PooledByteBufAllocator parent;

    private final int pageSize;
    private final int maxOrder;
    private final int pageShifts;
    private final int chunkSize;
    private final int subpageOverflowMask;

    private final PoolSubpage<T>[] tinySubpagePools;
    private final PoolSubpage<T>[] smallSubpagePools;

    private final PoolChunkList<T> q050;
    private final PoolChunkList<T> q025;
    private final PoolChunkList<T> q000;
    private final PoolChunkList<T> qInit;
    private final PoolChunkList<T> q075;
    private final PoolChunkList<T> q100;
2.PoolChunk
Chunk主要用来组织和管理多个Page的内存分配和释放,在Netty中,Chunk中的Page被构建成一棵二叉树。假设一个Chunk由16个Page组成,那么这些Page将会被按照下图所示的形式组织起来。
Page的大小是4个字节,Chunk的大小是64个字节(4*16)。整棵树有5层,第一层(也就是叶子节点所在的层)用来分配所有Page的内存,第4层用来分配2个Page的内存,依此类推。
每个节点都记录了自己在整个Memory Arena中的偏移地址,当一个节点代表的内存区域被分配出去之后,这个节点就会被标记为已分配,自这个节点以下的所有节点在后面的内存分配请求中都会被忽略。举例来说,当我们请求一个16字节的存储区域时,上面这个树中的第三层中的4个节点中的一个就会被标记为已分配,这就表示整个Memory Arena总有16个节点被分配出去了,新的分配请求只能从剩下的3个节点及其子树中寻找合适的节点。

对树的遍历采用深度优先的算法,但是在选择哪个子节点继续遍历时则是随机的,并不像通常的深度优先算法中那样总是访问左边的子节点。

3.PoolSubpage

对于小于一个Page的内存,Netty在Page中完成分配。每个Page会被切分成大小相等的多个存储块,存储块的大小由第一次申请的内存块大小决定。假如一个Page是8个字节,如果第一次申请的块大小是4个字节,那么这个Page就包含2个存储块;如果第一次申请的是8个字节,那么这个Page就被分成1个存储块。
一个Page只能用于分配第一次申请时大小相同的内存,比如,一个4字节的Page,如果第一次分配了1字节的内存,那么后面这个Page只能继续分配1字节的内存,如果有一个申请2字节的内存请求,就需要在一个新的Page中进行分配。
Page中存储区域的使用状态通过一个long数组来维护,数组中每个long的每一位表示一个块存储区域的占用情况:0表示未占用,1表示已占用。对于一个4字节的page来说,如果这个Page用来分配1个字节的存储区域,那么long数组中就只有一个long类型元素,这个数值的低四位用来指示各个存储区域的占用情况。对于一个128字节的Page来说,如果这个Page也是用来分配1个字节的存储区域,那么long数组中就会包含2个元素,总共128位,每一位代表一个区域的占用情况。
    final PoolChunk<T> chunk;
    final int memoryMapIdx;
    final int runOffset;
    final int pageSize;
    final long[] bitmap;

    PoolSubpage<T> prev;
    PoolSubpage<T> next;

    boolean doNotDestroy;
    int elemSize;
    int maxNumElems;
    int nextAvail;
    int bitmapLength;
    int numAvail;
4.内存回收策略
无论是Chunk还是Page,都通过状态位来标识内存是否可用,不同之处是Chunk通过在二叉树上对节点进行标识实现,Page通过维护块的使用状态标识来实现。
对于使用者来说,不需要关心内存池的实现细节,也不需要与这些类库打交道,只需要按照API说明正常使用即可。











  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值