Netty源码(四)Buffer与池化

前言

在java nio包中使用Buffer作为数据存放的载体,分为HeapBuffer与DirectBuffer。netty针对nio中的Buffer缺点和自身的使用特点实现自己的Buffer体系。
Netty的Buffer有如下特点:

  • 支持动态扩容
  • 读写双指针
  • 多种类型HeapBuffer、DirectBuffer、CompositeByteBuf
  • 支持引用计数
  • 支持池化

Netty Buffer

1. 支持动态扩容
nio ByteBuffer

final byte[] hb; 

nio中的Buffer是不支持动态扩容的,当你指定Buffer大小之后,后续就不能修改它的容量。比如ByteBuffer中存放数据的byte数组是由final修饰,final修饰的引用是不可变的。

UnpooledHeapByteBuf

//非 final修饰
byte[] array;

而netty中的支持动态扩容,比如UnpooledHeapByteBuf中存放的byte数组并不是和nio中一样用final修饰,当调用write类方法时候,首先检测是否超出数组容量,如果超出之后将分配一个是原来容量2倍的数组,然后将原数组中的数据拷贝到新数组中,整个过程类似ArrayList的扩容。

2. 读写双指针
nio中的Buffer使用position代表下一个读或者写的位置,所以当每次往Buffer中写完数据之后都需要调用一下flip方法,才能读取数据。

        IntBuffer intBuffer =  IntBuffer.allocate(10);
        intBuffer.put(1);
        intBuffer.put(2);
        //显示调用
        intBuffer.flip();
        while (intBuffer.hasRemaining()){
            intBuffer.get();
        }

这是很不友好的一点,因为很可能忘记调用flip而导致读取数据错误,netty基于这个缺点设计了两个指针来解决,readerIndex读索引,writerIndex写索引。

       +-------------------+------------------+------------------+
       | discardable bytes |  readable bytes  |  writable bytes  |
       |                   |     (CONTENT)    |                  |
       +-------------------+------------------+------------------+
       |                   |                  |                  |
       0      <=      readerIndex   <=   writerIndex    <=    capacity

写数据的时候移动writerIndex,读取数据的时候移动readerIndex,当readerIndex于writerIndex相等时代表当前没有数据可读。

        ByteBuf buffer = Unpooled.buffer();
        for (int i = 0; i < 10; i++) {
            buffer.writeInt(i);
        }
        //不需要显示调用,就可以进行读写操作
        while (buffer.isReadable()){
            System.out.println(buffer.readInt());
        }

3.多种类型Buffer
netty提供了三种类型的Buffer

  • HeapBuffer:分配在堆内的内存空间,它能在没有使用池化的情况下提供快速的分配和释放。
  • DirectBuffer:分配在堆外的内存空间,由于是在堆外的内存空间,在网络中会减少数据的拷贝和上下文切换效率会比较高。直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。
  • CompositeByteBuf:它为多个 ByteBuf 提供一个聚合视图。

netty相比nio中的Buffer多提供了一种Buffer类型CompositeByteBuf,复合缓冲区。

引用计数器

下面介绍一个比较重要的概念引用计数器,看一下Buffer的继承接口ReferenceCounted和Comparable

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf>

Comparable比较大小的接口这个不用分析,重点是ReferenceCounted这个接口,引用计数器。

Since Netty version 4, the life cycle of certain objects are managed by their reference counts, so that Netty can return them (or their shared resources) to an object pool (or an object allocator) as soon as it is not used anymore. Garbage collection and reference queues do not provide such efficient real-time guarantee of unreachability while reference-counting provides an alternative mechanism at the cost of slight inconvenience.

上面是官网对引用计数器的介绍,将其翻译一下

自从Netty版本4以来,某些对象的生命周期是由它们的引用计数管理的,所以当它们不再被使用时,Netty可以将它们(或它们的共享资源)返回到对象池(或对象分配器)。垃圾收集和引用队列不能提供如此高效的不可达性实时保证,而引用计数提供了一种替代机制,但会带来一些不便。

由上面的描述可以知道引用计数是为池化的对象服务的,用于管理对象的生命周期,而其中提到会带来一些不便是指,我们需要显示的去释放对象,对我们java程序员来说是比较奇怪的,因为对象的管理回收都是交给GC去做的。

ReferenceCounted 的reference count 变化流程

  • 初始化:当创建一个ReferenceCounted对象时,reference count为1;
  • retain:增加reference count;
  • release:减少reference count;
  • 回收:当reference count减为0时,对象将被显示释放。

对引用计数器的修改,是需要保证线程安全的,下面简单分析它是怎么保证线程安全的修改reference count,主要分析AbstractReferenceCountedByteBuf这个类
refCnt
初始化一个volatile修饰的refCnt,值为1,表示对象创建时引用计数器为1

private volatile int refCnt = 1;

refCntUpdater

private static final AtomicIntegerFieldUpdater<AbstractReferenceCountedByteBuf> refCntUpdater

AtomicIntegerFieldUpdater这个类代码可以线程安全的修改volatile修饰的int类型的成员变量。
retain()

    public ByteBuf retain() {
        return retain0(1);
    }

默认增加引用计数器加1
retain0()

    private ByteBuf retain0(int increment) {
        for (;;) {
            int refCnt = this.refCnt;
            //获得将要更新的值
            final int nextCnt = refCnt + increment;

            // Ensure we not resurrect (which means the refCnt was 0) and also that we encountered an overflow.
           //如果当前更新的值小于等于increment,说明refCnt为0,对象不能再被引用
            if (nextCnt <= increment) {
                throw new IllegalReferenceCountException(refCnt, increment);
            }
            //使用refCntUpdater CAS更新引用计数器的值,直到成功
            if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) {
                break;
            }
        }
        return this;
    }

retain0整体还是比较简单分为三步:

  1. 获取将要更新的值
  2. 判断当前的引用值是否为0,为0就说明对象不能再被引用
  3. CAS更新引用计算器

release()

    private boolean release0(int decrement) {
        for (;;) {
            int refCnt = this.refCnt;
            if (refCnt < decrement) {
                throw new IllegalReferenceCountException(refCnt, -decrement);
            }
           //更新引用计数器
            if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
                //表示引用计数器为0,回收资源
                if (refCnt == decrement) {
                    deallocate();
                    return true;
                }
                return false;
            }
        }
    }

release()的流程也比较简单,需要注意的是deallocate这个方法,如果是非池化的Buffer直接释放就好,比如UnpooledHeapByteBuf中直接array = null,而池化的Buffer就需要再次把内存放入池中,以便循环使用。

BufferPool

为什么需要池化

池化的技术其实比较常见,比如数据库连接池、线程池。当有资源使用比较频繁而且创建和销毁比较消耗性能的场景,就可以考虑使用池化的技术,去管理资源。
为什么netty需要引入对象池呢?《Netty实战》的作者在一次技术分享中说到,因为DirectBuffer是直接分配在堆外的,我们无法保证GC对直接内存的及时回收。最关键的是jdk中对的DirectBuffer的创建流程如下:

  1. 分配一个直接内存需要进入一个静态同步方法;
  2. 如果内存不足会进入catch块,调用system.gc进行垃圾回收,然后进行100毫秒的线程sleep,之后再进行创建操作

DirectBuffer的销毁方法也是静态同步的同步。
可以看见DirectBuffer的创建和销毁的成本都是非常高的,所有引入了BufferPool对
DirectBuffer进行管理。

怎么实现

要实现对象的池化首先要向预先申请一大块内存,netty管理内存的类是PoolArena,主要结构是一个完全平衡的二叉树像堆一样,使用这种数据结构对内存进行管理。
而Recycler是实现对象池化的核心

/**
 * Light-weight object pool based on a thread-local stack.
 *
 * @param <T> the type of the pooled object
 */
public abstract class Recycler<T> 

基于thread-local和stack实现,当对象使用的时候就从栈弹出,对象回收的就是一个入栈的过程。这里就不详细的分析Netty池化的实现了。

回收对象

        public void recycle(Object object) {
            if (object != value) {
                throw new IllegalArgumentException("object does not belong to handle");
            }
            stack.push(this);
        }

总结

netty的buffer对比nio buffer优化了很多,将nio buffer中使用比较麻烦的地方做了优化,比如支持动态扩容、读写双指针。然后再对于buffer的使用的池化的技术去管理,解决buffer创建和销毁比较耗时的问题。netty源码也是一步一步的再改进,关于引用计数器也是netty4才有的,遇到问题,然后解决,我们要学习的就是解决问题的思路。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343