ByteBuf和ByteBuffer

2023-11-10 16:28
文章标签 bytebuf bytebuffer

本文主要是介绍ByteBuf和ByteBuffer,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、背景简介

ByteBuf,顾名思义,就是字节缓冲区,是Netty中非常重要的一个组件。熟悉jdk NIO的同学应该知道ByteBuffer,正是因为jdk原生ByteBuffer使用比较复杂,某些场景下性能不是太好,netty开发团队重新设计了ByteBuf用以替代原生ByteBuffer。
 

二、ByteBuf和ByteBuffer对比

下面用图示来展示ByteBuf和ByteBuffer工作原理:

①、ByteBuffer

 ByteBuffer依靠flip()来切换模式,在读模式下调用flip()切换为写模式,在写模式下limit和capacity相等,position标识当前写的位置。在写模式下调用flip()切换为读模式,在读模式下position回到起始位置开始读,limit回到position位置表示能读到多少数据,capacity不变表示缓存区容量大小。

capacity:在读/写模式下都是固定的,就是缓冲区容量大小。

position:读/写位置指针,表示当前读(写)到什么位置。

limit:在写模式下表示最多能写入多少数据,此时和capacity相同。在读模式下表示最多能读多少数据,此时它的值等于缓存区中实际数据量的大小。
 

②、ByteBuf

ByteBuf主要是通过readerIndex 和 writerIndex两个指针进行数据的读和写,整个ByteBuf被这两个指针最多分成三个部分,分别是可丢弃部分,可读部分和可写部分

刚初始化的时候,整个缓冲区还没有数据,读写指针都指向0,所有的内容都是可写部分,此时还没有可读部分和可丢弃部分,如下:

 当写完N个字节数据后,读指针仍然是0,因为还没有开始进行读事件,写指针向后移动了N个字节的位置,如下:

 当开始读数据并且读取M个字节数据之后(M<N)写指针位置不变,读指针后移动了M个字节的位置,如下:

 当可丢弃部分数据被清空之后,readerindex重新回到起始位置,writerindex的位置为writerindex的值减去之前的readerindex,也就是M,相关图示如下:

 调用clear之后,writerindex和readerinde全部复位为0。它不会清除缓冲区内容(例如,用填充0),而只是清除两个指针。更改的读写指针的值,每个位置上原本的字节内容并没有发生改变,只是变成了可写状态而已。另请注意,此操作的语义不同于Buffer.clear()。

三、源码

明白了ByteBuf工作原理之后,ByteBuf相关的api就很好理解了,在此附上netty官方api文档,以供参阅:

https://netty.io/4.1/api/overview-summary.html。

我们在这里看下netty扩容相关源码逻辑。

扩容肯定是在写入数据的时候会由相关逻辑判断,我们随便进入一个写入字节的api方法。
 

public abstract ByteBuf writeBytes(byte[] src);

进入到其抽象子类AbstractByteBuf中。

 
  1. @Override

  2. public ByteBuf writeBytes(byte[] src) {

  3. writeBytes(src, 0, src.length);

  4. return this;

  5. }

  6. @Override

  7. public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {

  8. ensureAccessible();

  9. ensureWritable(length);

  10. setBytes(writerIndex, src, srcIndex, length);

  11. writerIndex += length;

  12. return this;

  13. }

首先ensureAccessible进行安全校验,每种尝试访问缓冲区内容的方法都应调用此方法,以检查缓冲区是否已释放。然后ensureWritable判断是否可写,扩容相关逻辑就在这里进行判断,如果缓冲区可写执行setBytes进行数据写入,然后writerindex向后移动length的位置,最后将ByteBuf对象进行返回。我们重点看ensureWritable。

 
  1. @Override

  2. public ByteBuf ensureWritable(int minWritableBytes) {

  3. if (minWritableBytes < 0) {

  4. throw new IllegalArgumentException(String.format(

  5. "minWritableBytes: %d (expected: >= 0)", minWritableBytes));

  6. }

  7. ensureWritable0(minWritableBytes);

  8. return this;

  9. }

直接进入ensureWritable0(minWritableBytes)方法中,此时minWritableBytes就是我们计划需要申请的内存大小空间。

 
  1. private void ensureWritable0(int minWritableBytes) {

  2. // 安全检查,保证写入之前是可访问的

  3. //ensureAccessible();

  4. // 可写,不必扩容

  5. if (minWritableBytes <= writableBytes()) {

  6. return;

  7. }

  8. //下标越界

  9. if (minWritableBytes > maxCapacity - writerIndex) {

  10. throw new IndexOutOfBoundsException(String.format(

  11. "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",

  12. writerIndex, minWritableBytes, maxCapacity, this));

  13. }

  14. //达到临界条件,开始执行扩容逻辑

  15. // 计算新的容量,实际上为当前容量扩容至2的幂次方大小(具体是多少需要进行后续判断和计算)

  16. int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity);

  17. // 扩容后的容量

  18. capacity(newCapacity);

  19. }

可以看到真正开辟内存空间新容量逻辑处理的是 alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity)执行的,进入到方法里面。

来到其实现类AbstractByteBufAllocator的calculateNewCapacity方法。

 
  1. @Override

  2. public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {

  3. if (minNewCapacity < 0) {

  4. throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expectd: 0+)");

  5. }

  6. if (minNewCapacity > maxCapacity) {

  7. throw new IllegalArgumentException(String.format(

  8. "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",

  9. minNewCapacity, maxCapacity));

  10. }

  11. // 扩容的阈值,4兆字节大小

  12. final int threshold = 1048576 * 4;

  13. if (minNewCapacity == threshold) {

  14. return threshold;

  15. }

  16. //如果计划一共需要的内存容量大小大于阈值,则需要和最大容量j进行比较

  17. if (minNewCapacity > threshold) {

  18. int newCapacity = minNewCapacity / threshold * threshold;

  19. if (newCapacity + threshold > maxCapacity) {

  20. newCapacity = maxCapacity;

  21. } else {

  22. newCapacity += threshold;

  23. }

  24. return newCapacity;

  25. }

  26. //如果计划一共需要的内存容量大小小于阈值,则以64为基数进行倍增

  27. int newCapacity = 64;

  28. while (newCapacity < minNewCapacity) {

  29. newCapacity <<= 1;

  30. }

  31. return Math.min(newCapacity, maxCapacity);

  32. }

minNewCapacity是我们计划一共需要的内存容量大小,maxCapacity是最大缓冲区容量大小。首先判断minNewCapacity 是否小于零或者minNewCapacity 是否大于maxCapacity,满足任一都抛出异常信息,然后判断我们计划一共需要的内存容量大小minNewCapacity 是否等于了阈值4M:

①、如果等于了阈值,新容量大小就是阈值4M。

②、如果计划一共需要的内存容量大小大于阈值,则maxCapacity和minNewCapacity 相对于阈值的整数倍再加上一个阈值进行大小判断,如果大于maxCapacity,则新容量最大就是maxCapacity,返回maxCapacity,如果小于maxCapacity,则相当于按照阈值的2倍进行扩容。

③、如果计划一共需要的内存容量大小小于阈值,则以64为基数只要小于我们计划需要的内存容量大小,就2倍扩容,最后选取循环后的扩容值和最大值两个值其中的较小者。

至此扩容就完成了,总结来说就是在扩容过程中有一个扩容需要容量的一个阈值4M,如果我们需要的内存空间等于这个阈值,那么扩容后的容量就是阈值大小,如果我们需要的内存容量大小大于阈值或者小于阈值,其扩容逻辑判断和扩容后返回的容量大小是不同的。但是最终扩容后的容量大小总是2的幂次方大小并且不会比maxCapacity大。
 

4、ByteBuf主要的继承关系

从内存分配的角度看,ByteBuf可以分为两类

(1)堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被JVM自动收回;缺点就是如果进行Socket的I/O读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Chanenel中,性能会有一定程度的下降。

(2)直接内存(DirectByteBuf) 字节缓冲区:非堆内存,它在堆外进行内存分配,相比于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从Socket Channel中读取时,由于少了一次内存复制,速度比堆内存快。

正式因为各有利弊,所以Netty提供了多种ByteBuf供开发者使用,经验表明,ByteBuf的最佳实践是在I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf,这样组合可以达到性能最优。

从内存回收角度看,ByteBuf也可以分为两类:基于对象池的ByteBuf和普通ByteBuf。两者的主要区别就是基于对象池的ByteBuf可以重用ByteBuf对象,它自己维护了一个内存池,可以循环利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC。测试表名使用内存池后的Netty在高负载、大并发的冲击下内存和GC更加平稳。

这篇关于ByteBuf和ByteBuffer的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/383868

相关文章

ByteBuffer详解

文章目录 1. ByteBuffer是抽象类,他的主要实现类为2. 获取方式3. 核心结构4. 核心API5. 字符串操作 1. ByteBuffer是抽象类,他的主要实现类为 HeapByteBuffer 堆ByteBuffer JVM内的堆内存 —> 读写操作 效率低 会受到GC影响MappedByteBuffer(DirectByteBuffer) OS内存 —

Netty ByteBuf 释放详解:内存管理与最佳实践

Netty ByteBuf 释放详解:内存管理与最佳实践 在Netty中(学习netty请参考:🔗深入浅出Netty:高性能网络应用框架的原理与实践),管理ByteBuf的内存是至关重要的(学习ByteBuf请参考:🔗Netty ByteBuf 详解:高性能数据缓冲区的全面介绍)。未能正确释放ByteBuf可能会导致内存泄漏,进而影响应用的性能和稳定性。本文将详细介绍如何正确地释放ByteB

27.ByteBuf零拷贝-分解合并

slice方法 netty中对于数据零拷贝的体现之一。 零拷贝就是减少数据复制。 slice就是切片,对原始ByteBuf进行切片成多个ByteBuf,切片后的ByteBuf并没有发生内存复制。 也就是将一个大的ByteBuf分片成几个小的ByteBuf,分片的过程中不会发生数据的拷贝。 切片后的ByteBuf还是使用的原始ByteBuf的内存。 切片后的ByteBuf维护独立的rea

26.ByteBuf组成和内存释放源码

ByteBuf中有什么? 1.capacity 容量 2.max capacity 最大容量 3.读指针(在ByteBuf的一开始) 4.写指针(在ByteBuf的一开始) 读指针与写指针之间就是 还未读取的部分(可读部分)。 读指针读过的部分叫做废弃的部分。 ByteBuf由四部分组成 可扩容部分(容量capacity与最大容量max capacity之间的部分)可写部分(写指针

【Netty】ByteBuffer原理与使用

Buffer则用来缓冲读写数据,常见的buffer有: ByteBuffer MappedByBuffer DirectByteBuffer HeapByteBuffer hortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer CharBuffer 有一个普通文本文件data.txt,内容为: 12345

Netty中的ByteBuf使用介绍

ByteBuf有三类: 堆缓存区:JVM堆内存分配直接缓冲区:有计算机内存分配,JVM只是保留分配内存的地址信息,相对于堆内存方式较为昂贵;复合缓冲区:复合缓冲区CompositeByteBuf,它为多个ByteBuf 提供一个聚合视图。比如HTTP 协议,分为消息头和消息体,这两部分可能由应用程序的不同模块产生,各有各的 ByteBuf,将会在消息被发送的时候组装为一个ByteBuf,此时可以

ByteBuf相关方法

将ByteBuf转为十六进制 ByteBuf buf; System.out.println(ByteBufUtil.hexDump(buf));

[netty核心类]--缓冲区ByteBuf

本文主要包括以下内容: 1)ByteBuf的三种类型:heapBuffer(堆缓冲区)、directBuffer(直接缓冲区)以及Composite Buffer(复合缓冲区)。 2)ByteBuf的工作原理。 3)ByteBuf与JDK中ByteBuffer的区别以及对比 4)ByteBuf的引用计数器实现类AbstractReferenceCountedByteBuf分析。 5)Unpoo

netty源码解解析(4.0)-25 ByteBuf内存池:PoolArena-PoolChunk

PoolArena实现了用于高效分配和释放内存,并尽可能减少内存碎片的内存池,这个内存管理实现使用PageRun/PoolSubpage算法。分析代码之前,先熟悉一些重要的概念: page: 页,一个页是可分配的最小的内存块单元,页的大小:pageSize = 1 << n (n <= 12)。chunk: 块,块是多个页的集合。chunkSize是块中所有page的pageSize之和。T

Java NIO学习笔记之二-图解ByteBuffer

Java NIO学习笔记之二-图解ByteBuffer ByteBuffer前前后后看过好几次了,实际使用也用了一些,总觉得条理不够清晰。 《程序员的思维修炼》一本书讲过,主动学习,要比单纯看资料效果来的好,所以干脆写个详细点的文章来记录一下。 概述 ByteBuffer是NIO里用得最多的Buffer,它包含两个实现方式:HeapByteBuffer是基于Java堆的实现,而DirectB