吃透Netty源码系列十五之ByteBuf和引用计数

  • Post author:
  • Post category:其他




ByteBuf简单介绍

其实是一个接口,但是有很多方法,可以理解成一个缓冲区数组,有一些操作的索引,比如读索引

readerIndex

表示从这个索引开始读,写索引

writerIndex

表示从这个索引开始写,还有缓冲区容量

capacity

等等。先看下类结构:

在这里插入图片描述

有一个引用计数的接口,因为缓冲区用完了需要释放,这里用引用计数来管理。

其实缓冲区大致是这么个样子:

在这里插入图片描述



一些特定的操作



discardReadBytes

把已读的区域数据给丢弃掉,其实就是把这部分区域给回收了,把读写索引都往前移,可写区域就大了:

在这里插入图片描述



clear

这个并不是清除数据,只是重置读写索引到

0

,可写区域又变大了:

在这里插入图片描述

其实方法有很多,一些不太好用图表示,后面会慢慢讲解的。



AbstractByteBuf

这个抽象字节缓冲区对

ByteBuf

的接口方法做了一些基本的实现。

在这里插入图片描述

具体的实现都比较好理解,就不多说了,但是有一个要注意,就是他里面有个泄露检测器,应该是用来帮助检测内存泄露的,后面会说:

在这里插入图片描述



ReferenceCounted引用计数接口

这个就是为了更好的管理好内存用的一种比较简单的方法,简单的原理就是如果一个对象有引用,那就计数器

+1

,如果释放了引用,那就计数器

-1

,如果发现计数器是

0

了,那就执行回收。一般的堆内的内存可以由GC来回收,但是如果是堆外的话,就要自己手动来释放啦,不然会造成内存泄露的。

touch

方法就是辅助调试用的,另外就是引用计数增加

retain

和减少

release



在这里插入图片描述



AbstractReferenceCountedByteBuf

这个字节缓冲区没干别的什么主要就是实现了引用计数器接口:

在这里插入图片描述

我们来看看源码,其实都是调用了

ReferenceCountUpdater

的方法。

public abstract class AbstractReferenceCountedByteBuf extends AbstractByteBuf {
    private static final long REFCNT_FIELD_OFFSET =//refCnt属性的内存偏移地址
            ReferenceCountUpdater.getUnsafeOffset(AbstractReferenceCountedByteBuf.class, "refCnt");
    private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> AIF_UPDATER =//原子更新器
            AtomicIntegerFieldUpdater.newUpdater(AbstractReferenceCountedByteBuf.class, "refCnt");
//引用更新器
    private static final ReferenceCountUpdater<AbstractReferenceCountedByteBuf> updater =
            new ReferenceCountUpdater<AbstractReferenceCountedByteBuf>() {
        @Override
        protected AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> updater() {
            return AIF_UPDATER;
        }
        @Override
        protected long unsafeOffset() {
            return REFCNT_FIELD_OFFSET;
        }
    };

    // Value might not equal "real" reference count, all access should be via the updater 初始值
    @SuppressWarnings("unused")
    private volatile int refCnt = updater.initialValue();

    protected AbstractReferenceCountedByteBuf(int maxCapacity) {
        super(maxCapacity);
    }

    @Override
    boolean isAccessible() {
        // Try to do non-volatile read for performance as the ensureAccessible() is racy anyway and only provide
        // a best-effort guard.
        return updater.isLiveNonVolatile(this);//是否还能用,释放了就不能用了
    }

    @Override
    public int refCnt() {
        return updater.refCnt(this);//获取真实引用计数
    }

    /** 直接设置真实引用计数
     * An unsafe operation intended for use by a subclass that sets the reference count of the buffer directly
     */
    protected final void setRefCnt(int refCnt) {
        updater.setRefCnt(this, refCnt);
    }

    /** 重新设置真实计数
     * An unsafe operation intended for use by a subclass that resets the reference count of the buffer to 1
     */
    protected final void resetRefCnt() {
        updater.resetRefCnt(this);
    }
//真实计数+1
    @Override
    public ByteBuf retain() {
        return updater.retain(this);
    }
//真实计数+increment
    @Override
    public ByteBuf retain(int increment) {
        return updater.retain(this, increment);
    }
//获取当前对象
    @Override
    public ByteBuf touch() {
        return this;
    }

    @Override
    public ByteBuf touch(Object hint) {
        return this;
    }
//外部可以调用的尝试释放资源,内部是用引用更新器来判断的
    @Override
    public boolean release() {
        return handleRelease(updater.release(this));
    }

    @Override
    public boolean release(int decrement) {
        return handleRelease(updater.release(this, decrement));
    }
//真正返回才去释放
    private boolean handleRelease(boolean result) {
        if (result) {
            deallocate();
        }
        return result;
    }

    /** 一旦真实计数为0就释放资源
     * Called once {@link #refCnt()} is equals 0.
     */
    protected abstract void deallocate();
}



ReferenceCountUpdater

具体引用计数怎么实现的,主要是这个类。但是他并不是用普通的那种引用一次计数器加

1

,释放一次减

1

,而是用了奇数和偶数,如果还存在引用那么引用数是偶数,否则是奇数。同时引用一次会加

2

,释放一次也减

2

,获取真实的计数是引用计数无符号右移

1

位,看起来好像很奇怪,不过基本都是位操作和直接比较操作性能应该会提高点。比如我们初始的时候真实引用计数

=1

,但是内部引用计数

=2

。如果有一次释放就内部引用计数

-2

,两次就内部引用计数

-4

,当然引用的时候也一样,你会发现

只要有引用,内部引用计数值就是偶数

。我们举个例子,我引用了

3

次,内部引用计数

=6

,获取真实引用计数刚好

6>>>1=3

,如果释放了

3

次,前2次会将内部引用计数

=2

,但是最后一次如果发现内部引用计数

=2

的话,就会设置成

1



这样内部引用计数刚好是奇数,真实引用计数刚好是

1>>>1=0

,就可以释放内存了

。说了那么多,还是看源码吧,里面有体现。



realRefCnt

这个就是获取真实的计数,是引用计数

>>>1

,同时前面会判断引用计数是否是偶数,偶数才有引用,奇数就直接返回

0

了,这里开始并不是直接用&判断奇偶,而是直接用是否等于,这个比位操作更加快,可见netty在这提高性能方面真的做到了细节中的细节了,因为大部分真实的计数可能就是

1

或者

2

,所以前面两个只要直接判断相等即可:

//获得真实计数 引用计数是奇数就返回0,说明已经释放了 偶数就无符号右移1 返回
    private static int realRefCnt(int rawCnt) {
        return rawCnt != 2 && rawCnt != 4 && (rawCnt & 1) != 0 ? 0 : rawCnt >>> 1;
    }



toLiveRealRefCnt

这个主要是在释放的时候内部用的,如果真实计数已经是0了,再释放就会报错,避免重复释放。

 /** 获取真实计数,如果真实引用已经是0了,就抛异常
     * Like {@link #realRefCnt(int)} but throws if refCnt == 0
     */
    private static int toLiveRealRefCnt(int rawCnt, int decrement) {
        if (rawCnt == 2 || rawCnt == 4 || (rawCnt & 1) == 0) {
            return rawCnt >>> 1;//偶数就无符号右移1
        }
        // odd rawCnt => already deallocated 奇数已经释放的
        throw new IllegalReferenceCountException(0, -decrement);
    }



refCnt

获取真实计数,但是不会抛异常。

//获取真实计数
    public final int refCnt(T instance) {
        return realRefCnt(updater().get(instance));
    }



nonVolatileRawCnt

这个可以获取内部的引用计数,不是真实的。

//可以根据偏移量获得引用计数,不是真实的计数
    private int nonVolatileRawCnt(T instance) {
        // TODO: Once we compile against later versions of Java we can replace the Unsafe usage here by varhandles.
        final long offset = unsafeOffset();
        return offset != -1 ? PlatformDependent.getInt(instance, offset) : updater().get(instance);
    }



isLiveNonVolatile

这个就是判断是否还存在引用,即内部的引用是否是偶数,是的话表示还有引用计数,返回

true

,不是就表示释放了,返回

false

,最后也是先判断是否相等来优化。

//是否是还有真实计数
    public final boolean isLiveNonVolatile(T instance) {
        final long offset = unsafeOffset();
        final int rawCnt = offset != -1 ? PlatformDependent.getInt(instance, offset) : updater().get(instance);

        // The "real" ref count is > 0 if the rawCnt is even.偶数的话真实计数>0
        return rawCnt == 2 || rawCnt == 4 || rawCnt == 6 || rawCnt == 8 || (rawCnt & 1) == 0;
    }



setRefCnt

直接设置真实引用计数,可以看到如果正数就会乘以2,负数就直接是

1

,也就是说没设置成功。但是这里要注意

refCnt << 1

可能会是负数,溢出了,比如

1173741824<<1 =-1947483648

  public final void setRefCnt(T instance, int refCnt) {
        updater().set(instance, refCnt > 0 ? refCnt << 1 : 1); // overflow OK here
    }



retain

这几个方法都是增加引用的,里面会判断溢出的问题。

//真实计数+1,即引用计数+2
    public final T retain(T instance) {
        return retain0(instance, 1, 2);
    }
//increment为正的才可以,但是rawIncrement 可能是负的,溢出了,后面会处理
    public final T retain(T instance, int increment) {
        // all changes to the raw count are 2x the "real" change - overflow is OK
        int rawIncrement = checkPositive(increment, "increment") << 1;
        return retain0(instance, increment, rawIncrement);
    }

    // rawIncrement == increment << 1 增量=真实增量x2
    private T retain0(T instance, final int increment, final int rawIncrement) {
        int oldRef = updater().getAndAdd(instance, rawIncrement);
        if (oldRef != 2 && oldRef != 4 && (oldRef & 1) != 0) {//如果老的是奇数的话 说明已经释放了
            throw new IllegalReferenceCountException(0, increment);
        }
        // don't pass 0! 经过0就说明有溢出了,要处理掉
        if ((oldRef <= 0 && oldRef + rawIncrement >= 0)//比如setRefCnt的时候设置了负数进去,oldRef =-1173741824,increment=1003741824 rawIncrement=2007483648
                || (oldRef >= 0 && oldRef + rawIncrement < oldRef)) {//比如setRefCnt的时候设置了正数进去,oldRef =2,increment=1103741824 rawIncrement=-2087483648
            // overflow case 溢出了
            updater().getAndAdd(instance, -rawIncrement);//改回来
            throw new IllegalReferenceCountException(realRefCnt(oldRef), increment);
        }
        return instance;
    }



release

释放的时候会先调用

nonVolatileRawCnt

获得引用计数,然后判断引用计数是否是

2

或者减的值就是真实引用计数值,是的话就可以尝试直接设置的方法

tryFinalRelease0

,如果失败会去尝试释放方法

retryRelease0

,这个是自旋,直到成功为止。如果不是的话就普通的引用计数器值的修改即可

nonFinalRelease0

。当然这里可能会对引用已经是

1

的再进行释放,这样就会在

retryRelease0

中的

toLiveRealRefCnt

检测报异常,避免了重复释放,而且里面修改值都是原子操作,线程安全的。

//减少计数1,返回是否真正释放
    public final boolean release(T instance) {
        int rawCnt = nonVolatileRawCnt(instance);//获取引用计数 如果引用计数rawCnt == 2 说明真实计数是1,就可以直接尝试最终释放,否则就真实计数减1,这个就算已经释放也不会报错
        return rawCnt == 2 ? tryFinalRelease0(instance, 2) || retryRelease0(instance, 1)
                : nonFinalRelease0(instance, 1, rawCnt, toLiveRealRefCnt(rawCnt, 1));
    }
//减少计数decrement,返回是否真正释放
    public final boolean release(T instance, int decrement) {
        int rawCnt = nonVolatileRawCnt(instance);//获取引用计数
        int realCnt = toLiveRealRefCnt(rawCnt, checkPositive(decrement, "decrement"));//获取真实计数
        return decrement == realCnt ? tryFinalRelease0(instance, rawCnt) || retryRelease0(instance, decrement)
                : nonFinalRelease0(instance, decrement, rawCnt, realCnt);//
    }
//尝试最终释放 如果引用计数是2的话,就直接设为1,释放内存,否则就失败
    private boolean tryFinalRelease0(T instance, int expectRawCnt) {
        return updater().compareAndSet(instance, expectRawCnt, 1); // any odd number will work
    }

    private boolean nonFinalRelease0(T instance, int decrement, int rawCnt, int realCnt) {
        if (decrement < realCnt //更新引用计数
                // all changes to the raw count are 2x the "real" change - overflow is OK
                && updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) {
            return false;
        }
        return retryRelease0(instance, decrement);
    }
//自旋设置引用计数或者尝试释放
    private boolean retryRelease0(T instance, int decrement) {
        for (;;) {
            int rawCnt = updater().get(instance), realCnt = toLiveRealRefCnt(rawCnt, decrement);
            if (decrement == realCnt) {//真实的计数和要减去的计数一样的话
                if (tryFinalRelease0(instance, rawCnt)) {//尝试最终释放
                    return true;
                }
            } else if (decrement < realCnt) {//真实计数大于减去的计数,还不能释放,只是减去decrement
                // all changes to the raw count are 2x the "real" change
                if (updater().compareAndSet(instance, rawCnt, rawCnt - (decrement << 1))) {
                    return false;
                }
            } else {
                throw new IllegalReferenceCountException(realCnt, -decrement);
            }
            Thread.yield(); // this benefits throughput under high contention 提示释放CPU,增加吞吐量
        }
    }



总结

今天主要先讲下这个引用计数缓冲区的实现,后面很多我们常用的缓冲区都是这个的子类:

在这里插入图片描述

其他的后面再讲吧,现在我们知道了很多缓冲区是实现引用计数接口的就行了,主要还是要释放内存,特别是堆外的内存。

好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。



版权声明:本文为wangwei19871103原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。