首页 > 其他分享 >京东面试:说说你对ByteBuf的理解

京东面试:说说你对ByteBuf的理解

时间:2023-04-21 11:34:47浏览次数:39  
标签:20 System 面试 ByteBuf println 京东 buf out


你好,我是田哥

一位朋友面试京东,被面试官按在地上各种摩擦!尤其是关于Netty的ByteBuf问了问题。于是决定分享一波,欢迎加我微信(tj0120622)一起探讨技术。

可能你会觉得字节面试没遇到过这个问题,这里主要是这位朋友写了自己对Dubbo源码有深入研究,于是背面试官问到了Netty的内容。

正文

在Netty中,还有另外一个比较常见的对象ByteBuf,它其实等同于Java Nio中的ByteBuffer,但是ByteBuf对Nio中的ByteBuffer的功能做了很作增强,下面我们来简单了解一下ByteBuf。

ByteBuf从名字上可以看出是缓冲区,主要是用于进行信息承载和交流的 。

Netty的数据读写都是以ByteBuf为单位进行交互的。

ByteBuf类定义:

public abstract class ByteBuf implements ReferenceCounted, Comparable<ByteBuf> {
    .....
}

可以看出,ByteBuf其实是一个抽象类,是Netty中Buffer的基础类,有很多种实现类,请看下图(截图效果只能看到部分,感兴趣的自己可以去翻翻源码):

京东面试:说说你对ByteBuf的理解_大数据


下面这段代码演示了ByteBuf的创建以及内容的打印,这里显示出了和普通ByteBuffer最大的区别之一,就是ByteBuf可以自动扩容,默认长度是256,如果内容长度超过阈值时,会自动触发扩容。

ByteBuf入门案例演示

package com.tian.netty;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;
import io.netty.util.internal.StringUtil;

/**
 * @author tianwc
 * @公众号 Java后端技术全栈
 * @description ByteBuf 演示 
 */
public class ByteBufExample {


    public static void main(String[] args) {
        //可自动扩容
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        log(buf);
        StringBuilder sb = new StringBuilder();
        //演示的时候,可以把循环的值扩大,就能看到扩容效果
        //0到31数字拼接
        for (int i = 0; i < 32; i++) {
            sb.append(" - " + i);
        }
        buf.writeBytes(sb.toString().getBytes());
        log(buf);
    }

    //输出Buf相关信息
    private static void log(ByteBuf buf) {
        StringBuilder builder = new StringBuilder()
                .append(" read index:").append(buf.readerIndex())//获取读索引
                .append(" write index:").append(buf.writerIndex())//获取写索引
                .append(" capacity:").append(buf.capacity())//获取容量
                .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        ByteBufUtil.appendPrettyHexDump(builder, buf);
        System.out.println(builder.toString());
    }
}

输出结果:

============before==============
 read index:0 write index:0 capacity:256

============after==============
 read index:0 write index:150 capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 20 2d 20 30 20 2d 20 31 20 2d 20 32 20 2d 20 33 | - 0 - 1 - 2 - 3|
|00000010| 20 2d 20 34 20 2d 20 35 20 2d 20 36 20 2d 20 37 | - 4 - 5 - 6 - 7|
|00000020| 20 2d 20 38 20 2d 20 39 20 2d 20 31 30 20 2d 20 | - 8 - 9 - 10 - |
|00000030| 31 31 20 2d 20 31 32 20 2d 20 31 33 20 2d 20 31 |11 - 12 - 13 - 1|
|00000040| 34 20 2d 20 31 35 20 2d 20 31 36 20 2d 20 31 37 |4 - 15 - 16 - 17|
|00000050| 20 2d 20 31 38 20 2d 20 31 39 20 2d 20 32 30 20 | - 18 - 19 - 20 |
|00000060| 2d 20 32 31 20 2d 20 32 32 20 2d 20 32 33 20 2d |- 21 - 22 - 23 -|
|00000070| 20 32 34 20 2d 20 32 35 20 2d 20 32 36 20 2d 20 | 24 - 25 - 26 - |
|00000080| 32 37 20 2d 20 32 38 20 2d 20 32 39 20 2d 20 33 |27 - 28 - 29 - 3|
|00000090| 30 20 2d 20 33 31                               |0 - 31          |
+--------+-------------------------------------------------+----------------+

上面输出的数据是不是很像我们抓包的数据格式。

============before==============
 read index:0 write index:0 capacity:256

没有写入数据之前:读索引是0,写索引是0,容量是256。

============after==============
 read index:0 write index:150 capacity:256

写入数据后:读索引是0,写索引是150,容量是256。说明我们想buffer中写入了150个字节。

如果我们对上面for循环进行修改:

public static void main(String[] args) {
        //可自动扩容
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        System.out.println("============before==============");
        log(buf);
        StringBuilder sb = new StringBuilder();
        //演示的时候,可以把循环的值扩大,就能看到扩容效果
        for (int i = 0; i < 320; i++) {
            sb.append(" - " + i);
        }
        buf.writeBytes(sb.toString().getBytes());
        System.out.println("============after==============");
        log(buf);
}

输出:

============before==============
 read index:0 write index:0 capacity:256

============after==============
 read index:0 write index:1810 capacity:2048
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 20 2d 20 30 20 2d 20 31 20 2d 20 32 20 2d 20 33 | - 0 - 1 - 2 - 3|
|00000010| 20 2d 20 34 20 2d 20 35 20 2d 20 36 20 2d 20 37 | - 4 - 5 - 6 - 7|
|00000020| 20 2d 20 38 20 2d 20 39 20 2d 20 31 30 20 2d 20 | - 8 - 9 - 10 - |
|00000030| 31 31 20 2d 20 31 32 20 2d 20 31 33 20 2d 20 31 |11 - 12 - 13 - 1|
|00000040| 34 20 2d 20 31 35 20 2d 20 31 36 20 2d 20 31 37 |4 - 15 - 16 - 17|
|00000050| 20 2d 20 31 38 20 2d 20 31 39 20 2d 20 32 30 20 | - 18 - 19 - 20 |
|00000060| 2d 20 32 31 20 2d 20 32 32 20 2d 20 32 33 20 2d |- 21 - 22 - 23 -|
|00000070| 20 32 34 20 2d 20 32 35 20 2d 20 32 36 20 2d 20 | 24 - 25 - 26 - |

重点看:

============after==============
 read index:0 write index:1810 capacity:2048

可以看出,写入的数据是1810字节,容量编程了2048。

由此我们可推算,在此过程中做了扩容

//容量默认大小
static final int DEFAULT_INITIAL_CAPACITY = 256;
//最大容量
static final int DEFAULT_MAX_CAPACITY = Integer.MAX_VALUE;

下面来看看扩容规则:

如果写入后数据大小未超过512字节,则选择下一个16的整数倍进行扩容。比如写入数据大小未12字节,则扩容后的capacity容量是16.

如果写入后数据大小超过512个字节,则选择下一个2的n次幂。比如写入大小是512字节,则扩容后的capacity是2的10次幂,也是就是1024(因为2的9次幂为512,长度已经不够了)。

扩容不能超过max capacity ,否则会报错。

ByteBuf创建的方法有两种

第一种,创建基于堆内存的ByteBuf,也就是由JVM管理内存。比如:

ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);

第二种,创建基于直接内存(堆外内存)的ByteBuf(默认情况下用的是这种)。

Java中的内存分为两个部分,一部分是JVM内存,另外一部分是不需要jvm管理的直接内存,也被称为堆外内存。堆外内存就是把内存对象分配在JVM堆意外的内存区域,这部分内存不是虚拟机管理,而是由操作系统来管理,这样可以减少垃圾回收对应用程序的影响。

比如:

ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);

直接内存的好处是读写性能会高一些,如果数据存放在堆中,此时需要把Java堆空间的数据发送到远程服务器,首先需要把堆内部的数据拷贝到直接内存(堆外内存),然后再发送。如果是把数据直接存储到堆外内存中,发送的时候就少了一个复制步骤。

但是它也有缺点,由于缺少了JVM的内存管理,所以需要我们自己来维护堆外内存,防止内存溢出。

第三种:池化技术,池化技术的目的就是重复使用资源。我们在上面的演示例子中,创建ByteBuf是:

ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();

我们可以通过输出来看看:

public class ByteBufExample {
    public static void main(String[] args) { 
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        System.out.println(buf);
    }
}

输出:

PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)

所以,上面这种方式创建ByteBuf,默认就是PooledUnsafeDirectByteBuf池化技术来创建的。

类关系图:

京东面试:说说你对ByteBuf的理解_编程语言_02


我们在IDEA上VM options中设置参数(unpooled非池化、pooled池化):

-Dio.netty.allocator.type=unpooled

再运行:

public class ByteBufExample { 
    public static void main(String[] args) { 
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        System.out.println(buf);
    }
}

输出:

UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(ridx: 0, widx: 0, cap: 256)

结果表明我们使用的是非池化ByteBuf。

探索

上面我们对ByteBuf有个简单的了解,我们现在来探索ByteBuf是怎样存储数据的。

请看下面这张图:

京东面试:说说你对ByteBuf的理解_编程语言_03


从这个图中可以看到ByteBuf其实是一个字节容器,该容器中包含三个部分:

  • 废弃字节:Byte中废弃的字节,简单理解为已经被读取过的字节,可以通过discardReadBytes()方法进行丢弃,并释放这部分空间。
  • 可读字节:可以被读取的字节空间,由读指针和写指针进行划分,两个指针中间的字节空间即为可以被读取的字节大小。计算:可读字节 = WriteIndex - ReadIndex。当WriteIndex等于ReadIndex时,ByteBuf不可读。
  • 可写字节:,可以被写入的字节空间,每写入一个字节,WriteIndex+1,直到WriteIndex等于容量Capacity时,ByteBuf
  • 可扩容字节,表示ByteBuf最多还能扩容多少容量。

在ByteBuf中,有两个指针:

  • readerIndex: 读指针,每读取一个字节,readerIndex自增加1。ByteBuf里面总共有witeIndex-readerIndex个字节可读,当readerIndex和writeIndex相等的时候,ByteBuf不可读
  • writeIndex: 写指针,每写入一个字节,writeIndex自增加1,直到增加到capacity后,可以触发扩容后继续写入。

ByteBuf中还有一个maxCapacity最大容量,默认的值是Integer.MAX_VALUE,当ByteBuf写入数据时,如果容量不足时,会触发扩容,直到capacity扩容到maxCapacity。

说了这一堆,可能你还是不太明白,我们用一个案例演示:

/**
 * @author tianwc
 * @公众号 Java后端技术全栈
 * @description ByteBuf 演示 
 */
public class ByteBufExample {

    public static void main(String[] args) {
        ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
        buf.writeBytes(new byte[]{1, 2, 3, 4}); //写入四个字节
        log(buf);
        buf.readByte();//读取一个字节
        log(buf);
        buf.writeByte(5); //写入一个字节
        log(buf);
        buf.writeInt(6); //写入一个int类型,也是4个字节
        log(buf);
    }

    //输出Buf相关信息
    private static void log(ByteBuf buf) {
        StringBuilder builder = new StringBuilder()
                .append(" read index:").append(buf.readerIndex())//获取读索引
                .append(" write index:").append(buf.writerIndex())//获取写索引
                .append(" capacity:").append(buf.capacity())//获取容量
                .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        //转换成16进制,美化
        ByteBufUtil.appendPrettyHexDump(builder, buf);
        System.out.println(builder.toString());
    }
}

运行结果:

京东面试:说说你对ByteBuf的理解_java_04


问题1

在上面的代码中:

buf.writeInt(6);

看似我们之加入了一个6,但是其实是四个字节:

00 00 00 06

也就是说整形的6,在buffer中保存的是四个字节,如果读取不全,那就不能输出6了,这也就是传说中的网络数据传输拆包和粘包问题,这个我们后面再专门分享。

问题2

上面我们数据被读取后,就不存在了,怎么?

如果想重复读取哪些已经读完的数据,这里提供了两个方法来实现标记和重置。

/**
 * @author tianwc
 * @公众号 Java后端技术全栈
 * @description ByteBuf 演示 
 */
public class ByteBufExample {

    public static void main(String[] args) {

        ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
        buf.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7});
        log(buf);
        buf.markReaderIndex(); //标记读取的索引位置
        System.out.println(buf.readInt());
        log(buf);
        buf.resetReaderIndex();//重置到标记位
        System.out.println(buf.readInt());
        log(buf); 
    }

    //输出Buf相关信息
    private static void log(ByteBuf buf) {
        StringBuilder builder = new StringBuilder()
                .append(" read index:").append(buf.readerIndex())//获取读索引
                .append(" write index:").append(buf.writerIndex())//获取写索引
                .append(" capacity:").append(buf.capacity())//获取容量
                .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        //转换成16进制,美化
        ByteBufUtil.appendPrettyHexDump(builder, buf);
        System.out.println(builder.toString());
    }
}

结果输出:

read index:0 write index:7 capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07                            |.......         |
+--------+-------------------------------------------------+----------------+
16909060
 read index:4 write index:7 capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 05 06 07                                        |...             |
+--------+-------------------------------------------------+----------------+
16909060
 read index:4 write index:7 capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 05 06 07                                        |...             |
+--------+-------------------------------------------------+----------------+

另外,如果想不改变读指针位置来获得数据,在ByteBuf中提供了 get 开头的方法,这个方法基于索引位置读取,并且允许重复读取的功能。

零拷贝

我之前有分享过零拷贝的内容:网易面试:说说零拷贝,成功上岸!

需要说明一下,ByteBuf的零拷贝机制和我们之前提到的操作系统层面的零拷贝不同,操作系统层面的零拷贝,是我们要把一个文件发送到远程服务器时,需要从内核空间拷贝到用户空间,再从用户空间拷贝到内核空间的网卡缓冲区发送,导致拷贝次数增加。

而ByteBuf中的零拷贝思想也是相同,都是减少数据复制提升性能。如图3-2所示,假设有一个原始ByteBuf,我们想对这个ByteBuf其中的两个部分的数据进行操作。按照正常的思路,我们会创建两个新的ByteBuf,然后把原始ByteBuf中的部分数据拷贝到两个新的ByteBuf中,但是这种会涉及到数据拷贝,在并发量较大的情况下,会影响到性能。

京东面试:说说你对ByteBuf的理解_大数据_05


ByteBuf中提供了一个slice方法,这个方法可以在不做数据拷贝的情况下对原始ByteBuf进行拆分,使用方法如下:

public class ByteBufExample {

    public static void main(String[] args) {
        ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();//可自动扩容
        buf.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
        log(buf);
        ByteBuf bb1 = buf.slice(0, 5);
        ByteBuf bb2 = buf.slice(5, 5);
        log(bb1);
        log(bb2);
        System.out.println("修改原始数据");
        buf.setByte(2, 5); //修改原始buf数据
        log(bb1);//再打印bb1的结果,发现数据发生了变化
    }

    //输出Buf相关信息
    private static void log(ByteBuf buf) {
        StringBuilder builder = new StringBuilder()
                .append(" read index:").append(buf.readerIndex())//获取读索引
                .append(" write index:").append(buf.writerIndex())//获取写索引
                .append(" capacity:").append(buf.capacity())//获取容量
                .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        //转换成16进制,美化
        ByteBufUtil.appendPrettyHexDump(builder, buf);
        System.out.println(builder.toString());
    }
}

运行结果:

京东面试:说说你对ByteBuf的理解_大数据_06


在上面的代码和运行结果,我们可以看出,通过slice对原始buf进行切片,每个分片是5个字节。

为了证明slice是没有数据拷贝,我们通过修改原始buf的索引2所在的值,然后再打印第一个分片bb1, 可以发现bb1的结果发生了变化。说明两个分片和原始buf指向的数据是同一个。

Unpooled

在前面的案例中我们经常用到Unpooled工具类,它是同了非池化的ByteBuf的创建、组合、复制等操作。

假设有一个协议数据,它有头部和消息体组成,这两个部分分别放在两个ByteBuf中我们希望把header和body合并成一个ByteBuf,通常的做法是:

ByteBuf allBuf=Unpooled.buffer(header.readableBytes()+body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);

在这个过程中,我们把header和body拷贝到了新的allBuf中,这个过程在无形中增加了两次数据拷贝操 作。那有没有更高效的方法减少拷贝次数来达到相同目的呢?

在Netty中,提供了一个CompositeByteBuf组件,它提供了这个功能。

public class ByteBufExample {

   public static void main(String[] args) {
        ByteBuf header = ByteBufAllocator.DEFAULT.buffer();//可自动扩容
        header.writeCharSequence("header", CharsetUtil.UTF_8);
        ByteBuf body = ByteBufAllocator.DEFAULT.buffer();
        body.writeCharSequence("body", CharsetUtil.UTF_8);
        CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
        //其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf的 writeIndex.
        //默认是false,也就是writeIndex=0,这样的话我们不可能从compositeByteBuf中读取到数据。
        compositeByteBuf.addComponents(true, header, body);
        log(compositeByteBuf);
    }

    //输出Buf相关信息
    private static void log(ByteBuf buf) {
        StringBuilder builder = new StringBuilder()
                .append(" read index:").append(buf.readerIndex())//获取读索引
                .append(" write index:").append(buf.writerIndex())//获取写索引
                .append(" capacity:").append(buf.capacity())//获取容量
                .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        //转换成16进制,美化
        ByteBufUtil.appendPrettyHexDump(builder, buf);
        System.out.println(builder.toString());
    }
}

运行结果:

京东面试:说说你对ByteBuf的理解_大数据_07


之所以CompositeByteBuf能够实现零拷贝,是因为在组合header和body时,并没有对这两个数据进行复制,而是通过CompositeByteBuf构建了一个逻辑整体,里面仍然是两个真实对象,也就是有一个指针指向了同一个对象,所以这里类似于浅拷贝的实现。

京东面试:说说你对ByteBuf的理解_jvm_08


wrappedBuffer

Unpooled工具类中,提供了一个wrappedBuffer()方法,来实现CompositeByteBuf零拷贝功能。使 用方法如下。

public class ByteBufExample {

    public static void main(String[] args) {
        ByteBuf header = ByteBufAllocator.DEFAULT.buffer();//可自动扩容
        header.writeBytes(new byte[]{1, 2, 3, 4, 5});

        ByteBuf body = ByteBufAllocator.DEFAULT.buffer();
        body.writeBytes(new byte[]{6, 7, 8, 9, 10});
        ByteBuf all = Unpooled.wrappedBuffer(header, body);
        log(all);tln(buf.readInt());
        log(buf);
    }

    //输出Buf相关信息
    private static void log(ByteBuf buf) {
        StringBuilder builder = new StringBuilder()
                .append(" read index:").append(buf.readerIndex())//获取读索引
                .append(" write index:").append(buf.writerIndex())//获取写索引
                .append(" capacity:").append(buf.capacity())//获取容量
                .append(StringUtil.NEWLINE);
        //把ByteBuf中的内容,dump到StringBuilder中
        //转换成16进制,美化
        ByteBufUtil.appendPrettyHexDump(builder, buf);
        System.out.println(builder.toString());
    }
}

输出结果:

京东面试:说说你对ByteBuf的理解_大数据_09


copiedBuffer

copiedBuffer,和wrappedBuffer最大的区别是,该方法会实现数据复制,感兴趣的自己搞搞,这里就不贴代码了。

ByteBuf使用API

代码案例:

package com.tian.netty;

//作者: 有梦想的肥宅
public class ByteBufDemo {
    public static void main(String[] args) throws InterruptedException {
        //1、把消息内容通过Netty自带的缓存工具类转换成ByteBuf对象
        byte[] msg = "【有梦想的肥宅】".getBytes(StandardCharsets.UTF_8);
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer(msg.length);
        byteBuf.writeBytes(msg);

        //2、操作容量相关API
        System.out.println("==========A、开始操作容量相关的API==========");
        System.out.println("1、输出ByteBuf容量capacity:" + byteBuf.capacity());
        System.out.println("2、输出ByteBuf最大容量maxCapacity:" + byteBuf.maxCapacity());
        System.out.println("3、输出ByteBuf当前可读字节数readableBytes:" + byteBuf.readableBytes());
        System.out.println("4、输出ByteBuf当前是否可读isReadable:" + byteBuf.isReadable());
        System.out.println("5、输出ByteBuf当前可写字节数writableBytes:" + byteBuf.writableBytes());
        System.out.println("6、输出ByteBuf当前是否可写isWritable:" + byteBuf.isWritable());
        System.out.println("7、输出ByteBuf可写的最大字节数maxWritableBytes:" + byteBuf.maxWritableBytes());
        System.out.println();

        //3、操作读写指针相关API
        System.out.println("==========B、开始操作读写指针相关API==========");
        System.out.println("1、输出ByteBuf读指针readerIndex:" + byteBuf.readerIndex());
        System.out.println("2、输出ByteBuf写指针writerIndex:" + byteBuf.writerIndex());
        System.out.println("3、开始调用markReaderIndex()方法保存读指针:" + byteBuf.markReaderIndex());
        System.out.println("4、开始调用resetReaderIndex()方法恢复读指针【实现重复读】:" + byteBuf.resetReaderIndex());
        System.out.println("5、开始调用markWriterIndex()方法保存写指针:" + byteBuf.markWriterIndex());
        System.out.println("6、开始调用resetWriterIndex()方法恢复写指针【实现重复写】:" + byteBuf.resetWriterIndex());
        System.out.println();

        //4、操作读写相关API
        System.out.println("==========C、开始操作读写相关API==========");
        System.out.println("1、开始调用writeBytes()方法写入数据:" + byteBuf.writeBytes("开始写入消息".getBytes(StandardCharsets.UTF_8)));
        byte[] readBytes = new byte[byteBuf.readableBytes()];//新建一个容量为byteBuf可读长度的字节数组
        byteBuf.readBytes(readBytes);//从ByteBuf中读取数据
        System.out.println("2、开始调用readBytes()方法从ByteBuf中读出数据:" + new String(readBytes, StandardCharsets.UTF_8));
        //PS:Netty使用了堆外内存,而堆外内存不能被JVM的垃圾回收器回收,所以需要我们手动回收【手动释放内存】
        //PS:ByteBuf是通过引用计数的方式管理的,所以需要调用release()方法把引用计数设置为0,才能直接回收内存
        System.out.println("3、开始调用retain()方法增加引用计数:" + byteBuf.retain());
        System.out.println("4、开始多次调用release()方法直至内存释放:");
        System.out.println("    4.1 释放引用前:byteBuf的状态:" + byteBuf);
        System.out.println("    4.2 当前引用计数:" + byteBuf.refCnt());
        System.out.println("    4.3 开始释放引用计数:");
        int i = 1;
        while (byteBuf.refCnt() > 0) {
            System.out.println("        第" + i + "次释放引用次数结果:" + byteBuf.release());
            i++;
        }
        System.out.println("    4.4 释放引用后:byteBuf的状态:" + byteBuf);
        System.out.println();

        //5、操作复制相关API
        System.out.println("==========D、开始操作复制相关API==========");
        byte[] msgN = "快乐肥肥".getBytes(StandardCharsets.UTF_8);
        ByteBuf byteBufN = ByteBufAllocator.DEFAULT.heapBuffer(msgN.length);
        byteBufN.writeBytes(msgN);
        System.out.println("1、输出原对象:" + byteBufN);
        ByteBuf slice = byteBufN.slice();
        System.out.println("2、调用slice()方法复制对象:" + slice);
        System.out.println("    2.1 调用slice()方法有以下特点:");
        System.out.println("        2.1.1 最大容量为原byteBuf的可读容量【新对象的maxCapacity = 原对象的readableBytes()】");
        System.out.println("        2.1.2 底层内存和引用计数与原始的byteBuf共享,但读写指针不同");
        System.out.println("        2.1.3 不复制数据,只通过改变读写指针来改变读写行为");
        System.out.println("        2.1.4 不改变原byteBuf的引用计数,当原byteBuf调用release()方法时,slice()出来的对象也会被释放");
        ByteBuf duplicate = byteBufN.duplicate();
        System.out.println("3、调用duplicate()方法复制对象:" + duplicate);
        System.out.println("    3.1 调用duplicate()方法有以下特点:");
        System.out.println("        3.1.1 最大容量、数据内容、指针位置都和原来的byteBuf一样【整个新的byteBuf都和原byteBuf共享】");
        System.out.println("        3.1.2 底层内存和引用计数与原始的byteBuf共享,但读写指针不同");
        System.out.println("        3.1.3 不复制数据,只通过改变读写指针来改变读写行为");
        System.out.println("        3.1.4 不改变原byteBuf的引用计数,当原byteBuf调用release()方法时,slice()出来的对象也会被释放");
        ByteBuf copy = byteBufN.copy();
        System.out.println("4、调用copy()方法复制对象:" + copy);
        System.out.println("    4.1 调用copy()方法有以下特点:");
        System.out.println("        4.1.1 直接复制一个新的对象出来,包括指针位置、底层对应的数据等【往copy()方法复制出来的对象内写数据,不影响原来的byteBuf】");
        System.out.println("        4.1.2 当原byteBuf调用release()方法时,copy()出来的对象不会被释放");
        byteBufN.release();
        System.out.println("5、release()方法后,其余对象状态如下:");
        System.out.println("    5.1 原对象:" + byteBufN);
        System.out.println("    5.2 slice()方法复制的对象:" + slice);
        System.out.println("    5.3 duplicate()方法复制的对象:" + duplicate);
        System.out.println("    5.4 copy()方法复制的对象:" + copy);
    }
}

输出结果:

==========A、开始操作容量相关的API==========
1、输出ByteBuf容量capacity:24
2、输出ByteBuf最大容量maxCapacity:2147483647
3、输出ByteBuf当前可读字节数readableBytes:24
4、输出ByteBuf当前是否可读isReadable:true
5、输出ByteBuf当前可写字节数writableBytes:0
6、输出ByteBuf当前是否可写isWritable:false
7、输出ByteBuf可写的最大字节数maxWritableBytes:2147483623

==========B、开始操作读写指针相关API==========
1、输出ByteBuf读指针readerIndex:0
2、输出ByteBuf写指针writerIndex:24
3、开始调用markReaderIndex()方法保存读指针:PooledUnsafeHeapByteBuf(ridx: 0, widx: 24, cap: 24)
4、开始调用resetReaderIndex()方法恢复读指针【实现重复读】:PooledUnsafeHeapByteBuf(ridx: 0, widx: 24, cap: 24)
5、开始调用markWriterIndex()方法保存写指针:PooledUnsafeHeapByteBuf(ridx: 0, widx: 24, cap: 24)
6、开始调用resetWriterIndex()方法恢复写指针【实现重复写】:PooledUnsafeHeapByteBuf(ridx: 0, widx: 24, cap: 24)

==========C、开始操作读写相关API==========
1、开始调用writeBytes()方法写入数据:PooledUnsafeHeapByteBuf(ridx: 0, widx: 42, cap: 64)
2、开始调用readBytes()方法从ByteBuf中读出数据:【有梦想的肥宅】开始写入消息
3、开始调用retain()方法增加引用计数:PooledUnsafeHeapByteBuf(ridx: 42, widx: 42, cap: 64)
4、开始多次调用release()方法直至内存释放:
    4.1 释放引用前:byteBuf的状态:PooledUnsafeHeapByteBuf(ridx: 42, widx: 42, cap: 64)
    4.2 当前引用计数:2
    4.3 开始释放引用计数:
        第1次释放引用次数结果:false
        第2次释放引用次数结果:true
    4.4 释放引用后:byteBuf的状态:PooledUnsafeHeapByteBuf(freed)

==========D、开始操作复制相关API==========
1、输出原对象:PooledUnsafeHeapByteBuf(ridx: 0, widx: 12, cap: 12)
2、调用slice()方法复制对象:UnpooledSlicedByteBuf(ridx: 0, widx: 12, cap: 12/12, unwrapped: PooledUnsafeHeapByteBuf(ridx: 0, widx: 12, cap: 12))
    2.1 调用slice()方法有以下特点:
        2.1.1 最大容量为原byteBuf的可读容量【新对象的maxCapacity = 原对象的readableBytes()】
        2.1.2 底层内存和引用计数与原始的byteBuf共享,但读写指针不同
        2.1.3 不复制数据,只通过改变读写指针来改变读写行为
        2.1.4 不改变原byteBuf的引用计数,当原byteBuf调用release()方法时,slice()出来的对象也会被释放
3、调用duplicate()方法复制对象:UnpooledDuplicatedByteBuf(ridx: 0, widx: 12, cap: 12, unwrapped: PooledUnsafeHeapByteBuf(ridx: 0, widx: 12, cap: 12))
    3.1 调用duplicate()方法有以下特点:
        3.1.1 最大容量、数据内容、指针位置都和原来的byteBuf一样【整个新的byteBuf都和原byteBuf共享】
        3.1.2 底层内存和引用计数与原始的byteBuf共享,但读写指针不同
        3.1.3 不复制数据,只通过改变读写指针来改变读写行为
        3.1.4 不改变原byteBuf的引用计数,当原byteBuf调用release()方法时,slice()出来的对象也会被释放
4、调用copy()方法复制对象:PooledUnsafeHeapByteBuf(ridx: 0, widx: 12, cap: 12)
    4.1 调用copy()方法有以下特点:
        4.1.1 直接复制一个新的对象出来,包括指针位置、底层对应的数据等【往copy()方法复制出来的对象内写数据,不影响原来的byteBuf】
        4.1.2 当原byteBuf调用release()方法时,copy()出来的对象不会被释放
5、release()方法后,其余对象状态如下:
    5.1 原对象:PooledUnsafeHeapByteBuf(freed)
    5.2 slice()方法复制的对象:UnpooledSlicedByteBuf(freed)
    5.3 duplicate()方法复制的对象:UnpooledDuplicatedByteBuf(freed)
    5.4 copy()方法复制的对象:PooledUnsafeHeapByteBuf(ridx: 0, widx: 12, cap: 12)

内存释放

针对不同的ByteBuf创建,内存释放的方法不同。

  • UnpooledHeapByteBuf,使用JVM内存,只需要等待GC回收即可
  • UnpooledDirectByteBuf,使用对外内存,需要特殊方法来回收内存
  • PooledByteBuf和它的之类使用了池化机制,需要更复杂的规则来回收内存

如果ByteBuf是使用堆外内存来创建,那么尽量手动释放内存,那怎么释放呢?

Netty采用了引用计数方法来控制内存回收,每个ByteBuf都实现了ReferenceCounted接口。

  • 每个ByteBuf对象的初始计数为1
  • 调用release方法时,计数器减一,如果计数器为0,ByteBuf被回收
  • 调用retain方法时,计数器加一,表示调用者没用完之前,其他handler即时调用了release也不会造成回收。
  • 当计数器为0时,底层内存会被回收,这时即使ByteBuf对象还存在,但是它的各个方法都无法正常使用

优点

  • 通过内置的复合缓冲区类型实现了透明的零拷贝。
  • 容量可以按需增长。
  • 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法。
  • 读和写使用了不同的索引。
  • 支持引用计数。
  • 支持池化。
  • 所有的网络通信都会涉及到字节序列的移动。

总结

本文分享了ByteBuf入门案例、创建方式、存储数据结构、零拷贝、API使用以及内存释放。我们分析了Netty对二进制数据的抽象ByteBuf的结构,本质上他的原理是:饮用一段内存,这段内存可以是堆内的,也可以是堆外的,然后用引用计数来控制争端内存是都需要被释放,使用读写指针来控制ByteBuf的读写,可以理解为外观模式的一种使用。

好了,今天就分享到这里。

需要《面试小抄》的小伙伴,后台回复77获取我的面试小抄第一版。

目前,第三版《面试小抄》已完成:我的第三版《面试小抄》


标签:20,System,面试,ByteBuf,println,京东,buf,out
From: https://blog.51cto.com/u_11702014/6212354

相关文章

  • Java技术_基础技术(0003)_类执行顺序详解+实例(阿里面试题)+详细讲解+流程图
    一、总体原则列出执行顺序的原则(这里本人出了简化,比较明了。可能有漏的,请帮忙补充,但应付该实例足以):  ==父类先于子类;  ==静态先于非静态;  ==变量和块先于构造方法;  ==变量声明先于执行(变量赋值、块执行);(这一点是根据数据在内存中是如何存储的得出的,基本类型、对象、......
  • 扎实打牢数据结构算法根基,从此不怕算法面试系列之week01 02-09 测试算法时间复杂度性
    1、数组生成器测试算法性能肯定不能自己手动声明创建数组了,在现代计算机上,对于O(n)级别的算法,都需要10W级别以上的数据才能看到性能,我们肯定不能手动声明10W个元素的数组吧?所以,创建数组生成器。这里,自己创建一个数组生成器——ArrayGenerator。packagecom.mosesmin.datastruc......
  • 程序员面试金典---11
    魔术索引思路:直接代码functionfindMagicIndex(nums){leti=0if(!nums.length){return-1}else{while(i<=nums.length&&i!==nums[i]){i++}letmin=i>nums.length?-1:i......
  • 年薪50W京东软件测试工程师的成长路 —— 我们都曾一样迷茫
    ​和朋友谈到软件测试行业的发展问题,其实软件测试现在已经不知不觉发生了非常大的变化,前几年的软件测试行业还是一个风口,人才缺口巨大,随着不断地转行人员以及毕业的大学生疯狂地涌入软件测试行业,目前软件测试行业“缺口”已经基本饱和。当然,我说的是最基础的功能测试的岗位需求已......
  • 15 个必须知道的 Java 面试问题(2年工作经验)
    【Java核心】1)Whatisthepurposeofserialization?2)WhatisthedifferencebetweenJDKandJRE?3)Whatisthedifferencebetweenequalsand==?4)WhenwillyouuseComparatorandComparableinterfaces?5)Whatisthewait/notifymechanism?6)......
  • 24道Python面试练习题
    1.简述函数式编程答:在函数式编程中,函数是基本单位,变量只是一个名称,而不是一个存储单元。除了匿名函数外,Python还使用fliter(),map(),reduce(),apply()函数来支持函数式编程。2.什么是匿名函数,匿名函数有什么局限性答:匿名函数,也就是lambda函数,通常用在函数体比较简单的函数上。......
  • 面试题
    好的,以下是前端面试可能会问到的具体问题:HTML、CSS、JavaScript的基础知识什么是盒模型?有哪些盒模型?CSS选择器的优先级是怎样的?什么是闭包?如何使用闭包?JavaScript中的数据类型有哪些?如何判断数据类型?HTML5中新增了哪些标签和属性?如何使用?前端框架(如React、Vue)的......
  • Java架构师面试题
    Java架构师面试题以下是一些Java架构师面试题的示例:请谈谈你对Java多线程的理解,并介绍一下Java线程池的使用场景。请谈谈你在项目中遇到的最困难的问题是什么,以及你是如何解决的?请谈谈你对Spring框架的理解,并介绍一下Spring的核心模块和作用。请简述一下你对RESTfulAPI的认......
  • 一天吃透JVM面试八股文
    什么是JVM?JVM,全称JavaVirtualMachine(Java虚拟机),是通过在实际的计算机上仿真模拟各种计算机功能来实现的。由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆和一个存储方法域等组成。JVM屏蔽了与操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行的目标......
  • #yyds干货盘点# LeetCode面试题:搜索旋转排序数组 II
    1.简述:已知存在一个按非降序排列的整数数组nums,数组中的值不必互不相同。在传递给函数之前,nums在预先未知的某个下标k(0<=k<nums.length)上进行了旋转,使数组变为[nums[k],nums[k+1],...,nums[n-1],nums[0],nums[1],...,nums[k-1]](下标从0开始计数)。例如,[0,1,2......