Bootstrap

《Netty in Action》中文版—第五章 ByteBuf

http://ifeve.com/netty-in-action-5/


《Netty in Action》中文版—第五章 ByteBuf

本文翻译自《Netty in Action》第五章  

作者:Norman Maurer, Marvin Allen Wolfthal   译者:桃小胖

本章包含

  • ByteBuf—Netty的数据容器
  • API详情
  • 用例
  • 内存分配

正如我们前面提到的,网络数据的基础单位总是字节。Java NIO用ByteBuffer做它的字节容器,但是这个类使用起来过于复杂,有时候还非常麻烦。

Netty用ByteBuf替代了ByteBuffer,这个强大的实现突破了JDK API的限制,为网络开发者提供了更好的API。

在这一章我们会展现同ByteBuffer相比,ByteBuf的出色功能和灵活性。这也让你从大体上更好地理解Netty数据处理的方式,并且为第六章ChannelPipeline和ChannelHandler的讨论做好准备。

5.1 ByteBuf API

Netty通过两个组件来提供它的数据处理(data handling)服务——抽象类ByteBuf和接口ByteBufHolder。

这里是一些ByteBuf API的优点:

  • 可扩展用户定义的buffer类型
  • 一个内置的composite buffer类型实现了透明的零拷贝
  • 根据需求扩展(处理)能力(类似JDK StringBuilder)
  • 读写模式切换不需要调用ByteBuffer的flip()方法
  • 读和写采用不同的索引
  • 支持方法链(method chaining)
  • 支持引用计数(reference counting)
  • 支持对象池(Pooling)

还有其他一些类用于管理ByteBuf实例的分配,对容器和容器中的数据执行一系列操作。我们会在学习ByteBuf和ByteBufHolder的时候详细研究这些特性。

5.2 ByteBuf类——Netty的数据容器

因为所有的网络通信都包含了字节序列的运动,所以一个高效易用的数据结构显然是必须的。Netty的ByteBuf满足甚至超越了这些需求。让我们首先来看下ByteBuf是如果用索引来简化数据存取的。

5.2.1 如何工作

ByteBuf包含两种不同的索引:一个用于读,另一个用于写。当你从一个ByteBuf读数据的时候,它的readerIndex按读取的字节数量递增。同样地,当你往一个ByteBuf中写数据时,它的writerIndex递增。图5.1 是一个空ByteBuf的布局和状态图。

5.1 一个16字节的ByteBuf,索引为0

f5-1

为了理解这两个索引之间的关系,考虑这么一个情况,当你一直读字节直到readerIndex碰到了writerIndex(即,它们的值相等)时,会发生什么情况。在那时,你已经到达了可读数据的尾部。试图继续读数据会触发一个IndexOutOfBoundException,就像当你试图存取一个数组尾部之外的数据时一样。

ByteBuf类中以read或者write开头的方法会增加其相应的索引值,而以set或者get开头的操作则不会。后者这些方法(以set或者get开头)在一个相对索引值(a relative value)上操作,这个相对索引值是作为输入参数被传入这些方法的。

一个ByteBuf的最大容量可以被指定,并且试图将写索引移出这个容量值的范围会触发一个异常(默认限制是Integer.MAX_VALUE)。

5.2.2 ByteBuf使用模式

用Netty的时候,你会发现一些ByteBuf常见的使用模式。在我们进一步了解它们的过程中,把图5.1——一个用不同索引控制读写的字节数组——牢记在脑子里会很有帮助。

堆内存字节缓冲区(HEAP BUFFERS)

最常用的ByteBuf模式把数据存储在JVM的堆空间。这个模式被称为字节数组(backing array),在不用对象池的情况下,提供快速分配和再分配(的功能)。如5.1所示,这个方式非常适合你处理遗留数据(legacy data)。

代码清单5.1 字节数组

l5-1 注意:如果hasArray()返回false,试图存取一个字节数组(比如调用array()方法)会触发UnsupportedOperationException。这个模式(HEAP BUFFERS)和JDK的ByteBuffer使用类似。

直接内存字节缓冲区(DIRECT BUFFERS)

Direct buffer是另一种ByteBuf的模式。我们总是期望分配给对象创建的内存来自堆,但其实不一定是——ByteBuffer类和NIO一起在1.4版本被引入JDK,允许JVM的实现通过本地调用(native calls)来分配内存。这是为了避免在每个本地I/O操作的调用之前(或者之后)拷贝buffer的内容到一个intermediate buffer(或者从intermediate buffer拷贝到buffer)

ByteBuffer的Javadoc说的很明确,“direct buffer的内容会位于常规(可被)垃圾收集的堆之外”。这解释了为什么direct buffer很适合用于网络数据传输。如果你的数据放在一个堆分配的buffer里,在被送往socket发送之前,JVM实际上在内部会拷贝你的heap buffer到一个direct buffer。

Direct buffer主要的缺点是它们分配和释放的开销比heap buffer更大。如果你用的是遗留代码,你可能还会碰上另一个问题:因为数据不在堆上,你不得不做一次拷贝,如下所示。

代码清单5.2 Direct buffer数据存取

l5-2

显然,这比用一个字节数组更费事,所以,如果你事先知道容器里的数据会通过一个数组存取,你也许会更倾向于用堆内存。

复合字节缓冲区(COMPOSITE BUFFERS)

第三种也是最后一种模式采用composite buffer,是多个ByteBuf实例的统一视图(an aggregated view of multiple ByteBufs)。用这个模式,你可以按需增加或者删除ByteBuf实例,这是JDK ByteBuffer完全没有的一个特性。

Netty用ByteBuf的子类CompositeByteBuf实现了这种模式,把多个buffer以一个单独的,融合的buffer的形式展现出来。

警告:一个CompositeByteBuf中的ByteBuf实例可能同时包含direct和非direct分配,如果只有一个ByteBuf实例,那么在CompositeByteBuf上调用hasArray()会返回那个ByteBuf的hasArray()值;否则,会返回false。

为了说明composite buffer的用法,我们假设有一条通过HTTP传输的消息,包含header和body两部分。这两部分由不同的应用程序模块生成,在消息被发送时被合并到一起。应用程序可以选择为多条消息采用同一个body。这种情况下,应用程序会为每条消息创建一个新的header。

因为我们不想为每条消息都分配两个buffer,用CompositeByteBuf最适合不过。它在采用通用ByteBuf API的同时,省掉了不必要的拷贝。图5.2 是最后产生的消息布局。

图5.2 CompositeByteBuf存放一个header和一个body

f5-2

下面这段代码说明了用JDK的ByteBuffer是如何实现这个需求的。一个包含两个ByteBuffer的数组被创建用存放消息的header和body,第三个ByteBuffer用来存放所有数据的一份拷贝。

代码清单5.3 用ByteBuffer实现Composite buffer模式

l5-3

分配和拷贝操作,还有数组管理,让这个版本的实现看起来低效和别扭。下面这段代码用CompositeByteBuf实现。

代码清单5.4 用CompositeByteBuf实现Composite buffer模式

l5-4

CompositeByteBuf可能不允许从一个字节数组获取数据,那么从一个CompositeByteBuf获取数据就类似direct buffer模式,如下所示。

代码清单5.5 从一个CompositeByteBuf存取数据

l5-5

注意,Netty优化了使用CompositeByteBuf的socket I/O操作,消除了所有可能的JDK buffer性能和内存使用的代价。这个优化在Netty的内部核心代码,所以没有通过API暴露出来,但是你应该意识到这个优化的效果。

COMPOSITEBYTEBUF API 除了继承自ByteBuf的方法以外,CompositeByteBuf还提供了大量额外的功能。请参考Netty Javadoc来获取完整的API清单。

5.3 字节级操作

ByteBuf提供了很多基本的读写操作之外的方法来修改它的数据。在下面几个小节我们会讨论其中最重要的几个方法。

5.3.1 随机数据获取索引

和一个普通的Java字节数组一样,ByteBuf 索引是从0开始的:第一个字节的索引是0,最后一个总是capacity()-1。从下面这段代码可以看到,存储机制的封装,让迭代(iterate)一个ByteBuf的内容变得非常简单。

代码清单5.6 获取数据

l5-6

注意,获取数据采用的方法用一个索引作为一个输入参数,并不改变readerIndex或者writerIndex的值。如果需要,它们的值可以通过调用readerIndex(index)或者writerIndex(index)手动修改。

5.3.2 连续数据获取索引

ByteBuf同时有读和写的索引,但是JDK的ByteBuffer只有一个,所以你得调用flip()方法在读写模式之间做切换。从图5.3可见,一个ByteBuf被它的两个索引分成了三个区域。

图5.3 ByteBuf内部划分

f5-3

5.3.3 可丢弃字节(discardable bytes)

图5.3中标记为discardable 字节的分段包含已经被读取的字节。通过调用discardReadBytes(),这部分字节可以被丢弃,同时这部分空间被回收。这个分段的初始大小是0,存在readerIndex中,并且随着读操作的执行而增加(get操作不移动readerIndex)。

图5.4是在图5.3中的buffer上调用discardReadBytes()的结果。你可以看到,discardable字节分段的空间现在已经可被写了。注意,discardReadBytes()被调用之后,不能保证writable分段的内容(是已经被丢弃的那些字节)

图5.4 丢弃已读字节之后的ByteBuf

f5-4

虽然你可能忍不住经常要去调用discardReadBytes()来最大化writable分段,请注意这样做很有可能造成内存拷贝,因为readable字节(图中标为CONTENT的部分)要被移动到buffer的头部。我们建议只在真正需要的时候才调用discardReadBytes();比如,当内存是稀缺品时。

5.3.4 可读字节(readable bytes)

ByteBuf的readable字节分段存储真正的数据,一个新分配的,重新包装的(wrapped),或者刚拷贝的buffer的readerIndex默认值是0。任何以read或者skip开头的方法会按当前readerIndex值读取或者跳过数据,然后按读取的字节数增加readerIndex的值。

如果其中某个调用方法的输入参数中包括一个目标ByteBuf做为写入对象,而且输入参数没有目标索引(a destination index),那么这个目标buffer的writerIndex也会增加(因为写入了这个目标buffer);例如,

readBytes(ByteBuf dest);

当试图从一个没有readable字节的buffer从读取数据,IndexOutOfBoundsException抛出。

这段代码说明了如何读所有的readable字节。

代码清单5.7 读所有的数据

l5-7

5.3.5 可写字节(writable bytes)

writable字节分段是一块包含未定义内容,待写入的内存。一个新分配的buffer的writerIndex默认值是0。任何以write开头的操作方法会从当前的writerIndex开始写数据,按写入字节的数量来增加writerIndex的值。如果写操作的源也是一个ByteBuf,并且源buffer的索引没有指定,那么源buffer的readerIndex增加同样的值。这个调用看起来像这样:

writeBytes(ByteBuf dest);

当向一个目标buffer写数据,并且超出它的容量时,IndexOutOfBoundsException抛出。

下面这段代码是一个用随机整数填满一个buffer直到空间用完的例子。writable()方法被用来判断buffer中是否还有足够的空间。

代码清单5.8 写数据

l5-8

5.3.6 索引管理

JDK的InputStream定义了方法mark(int readlimit) 和reset()。这两个方法分别用来标记stream的当前位置为一个指定值,或重置当前stream到这个指定位置。

同样地,你可以通过调用markReaderIndex(),markWriterIndex(), resetReaderIndex()和resetWriterIndex()设置和重置ByteBuf readerIndex和writerIndex。这些方法和InputStream的类似,除了它们没有readlimit参数来指定何时标记会失效。

你也可以通过readerIndex(int)或者writerIndex(int)移动索引到指定位置。试图设置一个索引到一个无效的位置会导致IndexOutOfBoundException。

你可以通过调用clear()同时将readerIndex和writerIndex置为0。注意这不会清掉内存里的东西。图5.5(和图5.3一样)说明了clear()是如何工作的。

图5. 5 调用clear()之前

f5-5

跟之前看到的一样,ByteBuf包括三个分段。图5.6是clear()调用之后的ByteBuf。

图5.6 调用clear()之后

f5-6

调用clear()比discardReadBytes()开销小得多,因为它只是重置了索引,没有拷贝任何内存。

5.3.7 索引操作

有好几种方法可以获得ByteBuf中某个值的索引。最简单的用法是indexOf()方法。更多复杂的搜索操作可以通过执行那些用ByteBufProcessor做为输入参数的方法实现。ByteBufProcessor这个接口就定义了一个方法,

boolean process(byte value);

这个方法用来报告是否这个输入值正在被搜索过程中。

ByteBufProcessor针对常见的搜索值定义了很多便捷的方法。假设你的应用需要和所谓的Flash sockets整合,这类socket包含NULL结尾的内容。调用buffer的

forEachByte(ByteBufProcessor.FIND_NUL);

能更加简单有效地处理Flash数据,因为在处理过程中,执行的边界检查更少了。

下面的代码是一个搜索回车字符(\r)的例子。

代码清单5.9 用ByteBufProcessor来寻找\r

l5-9

5.3.8 派生的buffer

一个派生的buffer将ByteBuf的内容以一种特定的方式呈现。

  • duplicate()
  • slice()
  • slice(int, int)
  • Unpooled.unmodifiableBuffer(…)
  • order(ByteOrder)
  • readSlice(int)

每个方法都返回一个新的ByteBuf实例,包含该实例自己的reader,writer和标记索引。和JDK ByteBuffer一样,内部的存储是和源ByteBuf共享的。这让派生buffer的创建开销不是不大,但是也意味着如果你改变了派生buffer的内容,你同时也在改变源buffer实例(source instance)的内容,所以使用时要小心。

BYTEBUF COPYING  如果你需要的是一个现存buffer的拷贝,那么用copy()或者copy(int, int)。和一个派生的buffer不同,这些调用返回的ByteBuf有一份独立(包含独立的内容和索引)的数据拷贝。

下面的代码说明了如何用slice(int, int)来操作子ByteBuf(slice)

代码清单5.10 分割一个ByteBuf

l5-10

现在我们来看下一个ByteBuf的分段(segment)拷贝和子段有什么不同。

代码清单5.11 拷贝一个ByteBuf

l5-11 两种用法是类似的,除了改变原始ByteBuf的一个子段或者一个拷贝的效果是不同的。只要有可能,尽量用slice()来避免拷贝内存的开销。

5.3.9 读/写 操作

像我们提到过的,有两类读/写操作:

  • get()和set()操作从一个指定的索引开始操作,不改变索引的值
  • read()和write()操作从一个指定的索引开始操作,随着读/写的字节数调整索引值

表5.1 列出了最常用的get()方法。参考API文档获取完整的列表。

表5.1 get()操作

方法名称描述
getBoolean(int)返回指定索引位置的boolean值
getByte(int)返回指定索引位置的byte值
getUnsignedByte(int)返回指定索引位置的无符号byte值(返回类型是short)
getMedium(int)返回指定索引位置的24位(medium)值
getUnsignedMedium(int)返回指定索引位置的无符号24位(medium)值
getInt(int)返回指定索引位置的int值
getUnsignedInt(int)返回指定索引位置的无符号int值(返回类型是long)
getLong(int)返回指定索引位置的long值
getShort(int)返回指定索引位置的short值
getUnsignedShort(int)返回指定索引位置的无符号short值(返回类型是int)
getBytes(int, …)传送buffer数据到指定索引位置开始的目标位置

这些操作基本上都有一个对应的set()方法。这些方法列在表5.2

表5.2 set()操作

方法名称描述
setBoolean(int, boolean)在指定索引位置设置boolean值
setByte(int index, int value)在指定索引位置设置Byte值
setMedium(int index, int value)在指定索引位置设置24位(medium)值
setInt(int index, int value)在指定索引位置设置int值
setLong(int index, long value)在指定索引位置设置long值
setShort(int index, int value)在指定索引位置设置short值

下面的代码是get()和set()方法的用法,可以看到它们并不改变读和写的索引。

代码清单5.12 get()和set()的用法

l5-12

现在让我们来看下read()操作,它作用于当前的readerIndex或者writeIndex。这些read()方法像读取stream一样从ByteBuf读数据。 表5.3列出了最常用的方法。

表5.3 read()操作

方法名称描述
readBoolean()返回当前readerIndex位置的boolean值, 然后readerIndex加1
readByte()返回当前readerIndex位置的byte值, 然后readerIndex加1
readUnsignedByte()返回当前readerIndex位置的无符号byte值(返回类型是short), 然后readerIndex加1
readMedium()返回当前readerIndex位置的24位(medium)值, 然后readerIndex加3
readUnsignedMedium()返回当前readerIndex位置的无符号24位(medium)值, 然后readerIndex加3
readInt()返回当前readerIndex位置的int值, 然后readerIndex加4
readUnsignedInt()返回当前readerIndex位置的无符号int值(返回类型是long), 然后readerIndex加4
readLong()返回当前readerIndex位置的long值, 然后readerIndex加8
readShort()返回当前readerIndex位置的short值, 然后readerIndex加2
readUnsignedShort()返回当前readerIndex位置的无符号short值(返回类型是int), 然后readerIndex加2
readBytes(ByteBuf | byte[]

destination,

int dstIndex [,int length])

从readerIndex位置(如果有指定,长度是length字节)开始,传送当前ByteBuf数据到目标ByteBuf或者byte[]。当前ByteBuf的readerIndex值根据已传送的字节数增长

几乎每一个read()方法都有一个对应的write()方法,用来往一个ByteBuf添加数据。注意,列在表5.4中的这些方法的输入参数,都是待写入ByteBuf的值,而不是索引值。

表5.4 write()操作

方法名称描述
writeBoolean(boolean)在当前writerIndex位置写入boolean值,然后writerIndex加1
writeByte(int)在当前writerIndex位置写入byte值,然后writerIndex加1
writeMedium(int)在当前writerIndex位置写入medium值,然后writerIndex加3
writeInt(int)在当前writerIndex位置写入int值,然后writerIndex加4
writeLong(long)在当前writerIndex位置写入long值,然后writerIndex加8
writeShort(int)在当前writerIndex位置写入short值,然后writerIndex加2
writeBytes(source ByteBuf |

byte[] [,int srcIndex

,int length])

从当前writerIndex位置开始,从指定的源ByteBuf或者byte[]传送数据到当前ByteBuf。如果输入参数中提供了srcIndex和length,那么从srcIndex开始读出length长的字节。当前ByteBuf的writerIndex值根据已写入的字节数增长。

代码5.13是这些方法的示例。

代码清单5.13 ByteBuf上的read()和write()操作

l5-13

5.3.10 更多操作

表5.5列出了ByteBuf提供的其他一些有用的操作方法。

表5.5 其他有用的操作

方法名称描述
isReadable()如果至少一字节大小的数据可读,返回true
isWritable()如果至少一字节大小的数据可写,返回true
readableBytes()返回可读的字节数
writableBytes()返回可写的字节数
capacity()返回ByteBuf可容纳的字节数。调用该方法后,ByteBuf会试着扩展容量直到到达maxCapacity()
maxCapacity()返回ByteBuf可容纳的最大字节数
hasArray()如果ByteBuf有一个字节数组,返回true
Array()如果ByteBuf有一个字节数组,返回该数组;否则,抛出异常

UnsupportedOperationException

5.4 ByteBufHolder接口

我们经常会发现,除了数据负载,我们还需要存储各种数据属性。一个HTTP响应就是一个很好的例子;除了以字节形式展现的消息体(content),还有状态码(status code), cookie等等。

Netty提供了ByteBufHolder来应对这样一种常见的情况。ByteBufHolder还为Netty的一些高级特性提供了支持,比如buffer池,一个ByteBuf可用从一个池中取出,然后在需要时被自动释放(回池中)。

ByteBufHolder只有一些少量的方法用来获取底层数据和引用计数。表5.6列出了这些方法(没有列出从ReferenceCounted继承的方法)。

表5. 6 ByteBufHolder操作

方法名称描述
content()返回ByteBufHolder的ByteBuff数据
copy()返回ByteBufHolder的深拷贝(deep copy), 包括一份和源ByteBuf数据独立的(unshared)拷贝
duplicate()返回ByteBufHolder的浅拷贝(shallow copy),包括一份和源ByteBuf数据共享的(shared)拷贝

如果你想实现一个消息对象,把消息负载(payload)存储在一个ByteBuf中,那么ByteBufHolder是个不错的选择。

5.5 ByteBuf分配

在这一节我们会说明管理ByteBuf实例的几种方法。

5.5.1 按需分配:接口ByteBufAllocator

为了减少分配和释放内存的开销,Netty用接口ByteBufAllocator实现了对象池,可以用来分配我们提到过的所有模式的ByteBuf实例。对象池的使用和具体应用有关,并不影响ByteBuf API的用法。

表5.7列出了ByteBufAllocator的一些操作

表5.7 ByteBufAllocator方法

方法名称描述
buffer()

buffer(int initialCapacity);

buffer(int initialCapacity, int maxCapacity);

返回heap或者direct ByteBuf
heapBuffer()

heapBuffer(int initialCapacity)

heapBuffer(int initialCapacity, int

maxCapacity)

返回heap ByteBuf
directBuffer()

directBuffer(int initialCapacity)

directBuffer(int initialCapacity, int

maxCapacity)

返回direct ByteBuf
compositeBuffer()

compositeBuffer(int maxNumComponents);

compositeDirectBuffer()

compositeDirectBuffer(int maxNumComponents);

compositeHeapBuffer()

compositeHeapBuffer(int maxNumComponents);

返回CoompositeByteBuf,可以根据指定的

component数量来扩展heap或者direct buffer

ioBuffer()返回一个用于socket I/O操作的ByteBuf

你可以从一个Channel(每个Channel实例都不同)或者从ChannelHandler绑定的ChannelHandlerContext中获取

ByteBufAllocator的一个引用。下面的代码说明了这两种方法。

代码清单5.14 获取一个ByteBufAllocator引用

l5-14

Netty提供了ByteBufAllocator的两种实现:PoolByteBufAllocator和UnpooledByteBufAllocator。前者将ByteBuf实例放入池中,提高了性能,将内存碎片减少到最小。这个实现采用了一种内存分配的高效策略,称为jemalloc。它已经被好几种现代操作系统所采用。后者则没有把ByteBuf放入池中,每次被调用时,返回一个新的ByteBuf实例。

虽然Netty默认使用PoolByteBufAllocator,但是改变默认设置很简单,只要通过ChannelConfig API或者在Bootstrap应用程序的时候指定一个不同的allocator就可以了。更多详情可以参考第八章。

5.5.2 Unpooled buffers

有些时候,你不能获取到ByteBufAllocator的引用。在这种情况下,Netty有一个被称为Unpooled的工具类,提供了一些静态的辅助方法(helper methods)来创建unpooled ByteBuf实例。表5.8列出了其中最重要的几个方法。

表5.8 Unpooled方法

方法名称描述
buffer()

buffer(int initialCapacity)

buffer(int initialCapacity, int maxCapacity)

返回heap ByteBuf
directBuffer()

directBuffer(int initialCapacity)

directBuffer(int initialCapacity, int

maxCapacity)

返回direct ByteBuf
wrappedBuffer()返回 wrapped ByteBuf
copiedBuffer()返回copied ByteBuf

Unpooled类让ByteBuf也同样适用于不需要其他的Netty组件的、无网络操作的项目,这些项目可以从这个高性能的、可扩展的buffer API中获益。

 5.5.3 类ByteBufUtil

ByteBufUtil提供了一些操作ByteBuf的静态辅助方法。因为这个ByteBufUtil API是通用的,并且和内存池并无关联,所以这些辅助方法是在buffer类之外实现的(译者注:从ByteBufUtil Javadoc可见,它是继承自Object的)。

这些静态方法中最有价值的也许就是hexdump(), 它将一个ByteBuf内容以十六进制字符串的方式打印出来。这在各种情况下都很有用,比如为了调试,记录ByteBuf的内容。一个以十六进制表示的记录日志通常比直接用字节表示的更有用。此外,十六进制表示的版本很容易转换回原字节表示的形式。

另一个有用的方法是boolean equals(ByteBuf, ByteBuf),用来判断两个ByteBuf实例是否相等。如果你实现了你自己的ByteBuf子类,你也许还能发现一些其他有用的ByteBufUtil方法。

5.6 引用计数(Reference counting)

引用计数是一项通过释放不再被其他对象引用的对象的资源,来优化内存使用和性能的技术。Netty在第四版为ByteBu和ByteBufHolderf引入了引用计数,它们都实现了ReferenceCounted接口。

引用计数背后的思想不是特别复杂;它主要参与追踪某个特定对象活跃的引用数量。一个ReferenceCounted实现的实例通常从1开始计数。只要引用计数大于0,这个对象就保证不会被释放。当活跃的引用计数减少到0,对象就会被释放。注意,虽然释放的确切含义根据具体实现有所不同,但最起码,一个已经被释放的对象不能再被使用。

引用计数对对象池的实现很关键,比如让PoolByteBufAllocator减小了内存分配的开销。示例在下面两段代码里。

代码清单5.15 引用计数

l5-15

代码清单5.16 释放包含引用计数的对象

l5-16

如果包含引用计数的对象已经被释放,当试图获取这个对象时会抛出引用越界的异常IllegalReferenceCountException。

要注意的是,某个类可以定义它独有的释放-计数规则。比如说,我们可以想象有这么一个类,不管当前值是多少,它的release()方法总是把引用计数器设为0,因此可以即刻让所有活跃的引用失效。

谁来负责释放?通常来说,最后一次获取对象的代码应该负责释放它。在第六章,我们会解释这个概念和ChannelHandler以及ChannelPipeline之间的关联。

5.7 小结

这一章致力于介绍基于ByteBuf的Netty数据容器。我们首先解释了ByteBuf相比JDK ByteBuffer的一些优势。我们还强调了几种不同模式的API,并指出了它们最适合哪些使用场景。

下面是我们提到过的本章要点:

  • 读写分离的索引用来控制数据存取
  • 不同的内存管理策略——字节数组和direct buffer
  • 代表了多个ByteBuf统一视图的composite buffer
  • 数据存取的方法:搜索,分割,和拷贝
  • read,write,get和set APIs
  • ByteBufAllocator对象池和引用计数

在下一章,我们会把重点放在为你的数据处理逻辑提供了传输媒介的ChannelHandler。因为ChannelHandler大量使用了ByteBuf,你会看到整个Netty结构的这些重要部分将会聚集到一起。


;