您当前的位置:首页 > 电脑百科 > 站长技术 > 服务器

Netty中的缓冲区为什么比原生NIO更高效

时间:2022-01-07 09:59:57  来源:  作者:互联网高级架构师

Netty自己的ByteBuf

ByteBuf是为了解决ByteBuffer的问题和满足网络应用程序开发人员的日常需求而设计的.

JDK中ByteBuffer的缺点:

  1. 无法动态扩容 长度是固定的,不能动态扩展和收缩,当数据大于ByteBuffer容量时,会发生索引越界异常.
  2. API使用复杂 读写的时候需要手动调用flip()和rewind()等方法,使用时需要非常谨慎的使用这些api,否则容易出现错误.

ByteBuf做了哪些增强?

  1. API操作便捷性
  2. 动态扩容
  3. 多种ByteBuf实现
  4. 内存复用机制
  5. 零拷贝机制

ByteBuf的操作

三个重要属性:

  1. capacity容量
  2. readerIndex读取位置
  3. writerIndex写入位置

提供了两个指针变量来支持顺序读和写操作,分别是readerIndex和writeInDex,也就把缓冲区分成了三个部分:

0[ --已读可丢弃区域-- ]reaerIndex[ --可读区域-- ]writerIndex[ --待写区域-- ]capacity

常用方法定义:

  • 随机访问索引getByte
  • 顺序读read*
  • 顺序写write*
  • 清除已读内容discardReadBytes
  • 清除缓冲区clear
  • 搜索操作
  • 标记和重置
  • 引用计数和释放

我们可以对这些api做一些测试,如下:

package io.netty.example.echo;

import JAVA.util.Arrays;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

/**
 * @author daniel
 * @version 1.0.0
 * @date 2021/12/20
 */
public class ApiTest {

    public static void main(String[] args) {
        //1.创建一个非池化的ByteBuf,大小为10字节
        ByteBuf buf = Unpooled.buffer(10);
        System.out.println("原始ByteBuf为:" + buf.toString());
        System.out.println("1.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
        System.out.println();

        //2.写入一段内容
        byte[] bytes = {1,2,3,4,5};
        buf.writeBytes(bytes);
        System.out.println("写入的bytes为:" + Arrays.toString(bytes));
        System.out.println("写入一段内容后ByteBuf为:" + buf);
        System.out.println("2.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
        System.out.println();

        //3.读取一段内容
        byte b1 = buf.readByte();
        byte b2 = buf.readByte();
        System.out.println("读取的bytes为:" + Arrays.toString(new byte[]{b1, b2}));
        System.out.println("读取一段内容后ByteBuf为:" + buf);
        System.out.println("3.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
        System.out.println();

        //4.将读取的内容丢弃
        buf.discardReadBytes();
        System.out.println("丢弃已读取的内容后ByteBuf为:" + buf);
        System.out.println("4.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
        System.out.println();

        //5.清空读写指针
        buf.clear();
        System.out.println("清空读写指针后ByteBuf为:" + buf);
        System.out.println("5.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
        System.out.println();

        //6.再次写入一段内容,比第一段内容少
        byte[] bytes2 = {1,2,3};
        buf.writeBytes(bytes2);
        System.out.println("再写入的bytes2为:" + Arrays.toString(bytes2));
        System.out.println("再写入一段内容后ByteBuf为:" + buf);
        System.out.println("6.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
        System.out.println();

        //7.将ByteBuf清空
        buf.setZero(0, buf.capacity());
        System.out.println("内容清空后ByteBuf为:" + buf);
        System.out.println("7.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
        System.out.println();

        //8.再次写入一段超过容量的内容
        byte[] bytes3 = {1,2,3,4,5,6,7,8,9,10,11};
        buf.writeBytes(bytes3);
        System.out.println("写入超量的bytes3为:" + Arrays.toString(bytes3));
        System.out.println("写入超量内容后ByteBuf为:" + buf);
        System.out.println("8.ByteBuf中的内容为:" + Arrays.toString(buf.array()));
        System.out.println();
    }
}

从这些api的使用中就可以体会到ByteBuf比ByteBuffer的强大之处,我们可以深入研究一下它在写入超量数据时的扩容机制,也就是buf.writeBytes(byte[])方法


ByteBuf动态扩容

容量默认值为256字节,最大值为Integer.MAX_VALUE,也就是2GB

实际调用
AbstractByteBuf.writeBytes,如下:

AbstractByteBuf.writeBytes

@Override
public ByteBuf writeBytes(byte[] src) {
    writeBytes(src, 0, src.length);
    return this;
}

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

@Override
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
    ensureWritable(length); //检查是否有足够的可写空间,是否需要扩容
    setBytes(writerIndex, src, srcIndex, length);
    writerIndex += length;
    return this;
}

AbstractByteBuf.ensureWritable(length);

@Override
public ByteBuf ensureWritable(int minWritableBytes) {
    ensureWritable0(checkPositiveOrZero(minWritableBytes, "minWritableBytes"));
    return this;
}

AbstractByteBuf.ensureWritable0(checkPositiveOrZero(minWritableBytes, "minWritableBytes"));

final void ensureWritable0(int minWritableBytes) {
    final int writerIndex = writerIndex(); //获取当前写下标
    final int targetCapacity = writerIndex + minWritableBytes; //计算最少需要的容量
    // using non-short-circuit & to reduce branching - this is a hot path and targetCapacity should rarely overflow
    if (targetCapacity >= 0 & targetCapacity <= capacity()) { //判断当前容量是否够用
        ensureAccessible(); //检查ByteBuf的引用计数,如果为0则不允许继续操作
        return;
    }
    if (checkBounds && (targetCapacity < 0 || targetCapacity > maxCapacity)) { //判断需要的容量是否是合法值,不合法为true直接抛出越界异常
        ensureAccessible();//检查ByteBuf的引用计数,如果为0则不允许继续操作
        throw new IndexOutOfBoundsException(String.format(
                "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
                writerIndex, minWritableBytes, maxCapacity, this));
    }

    // Normalize the target capacity to the power of 2.(标准化为2的次幂)
    final int fastWritable = maxFastWritableBytes();
    int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
            : alloc().calculateNewCapacity(targetCapacity, maxCapacity); //计算扩容后容量(只要扩容最小64)

    // Adjust to the new capacity.
    capacity(newCapacity); //设置新的容量
}

alloc().calculateNewCapacity(targetCapacity, maxCapacity) -> AbstractByteBufAllocator

@Override
public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
    checkPositiveOrZero(minNewCapacity, "minNewCapacity"); //最小所需容量
    if (minNewCapacity > maxCapacity) { //判断最小所需容量是否合法
        throw new IllegalArgumentException(String.format(
                "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
                minNewCapacity, maxCapacity));
    }
    final int threshold = CALCULATE_THRESHOLD; // 4 MiB page 阈值超过4M以其他方式计算

    if (minNewCapacity == threshold) { //等于4M直接返回4M
        return threshold;
    }

    // If over threshold, do not double but just increase by threshold.
    if (minNewCapacity > threshold) { //大于4M,不需要加倍,只需要扩大阈值即可
        int newCapacity = minNewCapacity / threshold * threshold;
        if (newCapacity > maxCapacity - threshold) {
            newCapacity = maxCapacity;
        } else {
            newCapacity += threshold;
        }
        return newCapacity;
    }

    // 64 <= newCapacity is a power of 2 <= threshold
    final int newCapacity = MathUtil.findNextPositivePowerOfTwo(Math.max(minNewCapacity, 64)); //计算不少于所需容量的最小的2次幂的值
    return Math.min(newCapacity, maxCapacity); //取容量所允许的最大值和计算的2次幂的最小值,当然在这儿就是newCapacity=64
}

总结一下就是最小所需容量是否等于阈值,如果是直接返回阈值此后直接扩大阈值,否则以64为最小2次幂为基础每次扩大二倍直到阈值.


选择合适的ByteBuf实现

netty针对ByteBuf提供了8中具体的实现方式,如下:

堆内/堆外

是否池化

访问方式

具体实现类

备注

heap堆内

unpool

safe

UnpooledHeapByteBuf

数组实现

heap堆内

unpool

unsafe

UnpooledUnsafeHeapByteBuf

Unsafe类直接操作内存

heap堆内

pool

safe

PooledHeapByteBuf

 

heap堆内

pool

unsafe

PooledUnsafeHeapByteBuf

~

direct堆外

unpool

safe

UnpooledDirectByteBuf

NIO DirectByteBuffer

direct堆外

unpool

unsafe

UnpooleUnsafedDirectByteBuf

~

direct堆外

pool

safe

PooledDirectByteBuf

~

direct堆外

pool

unsafe

PooledUnsafeDirectByteBuf

~

在使用时,都是通过ByteBufAllocator分配器进行申请,同时分配器具有内存管理的功能。

在这儿堆内和堆外没有什么区别,对api的使用时一样的,仅仅是通过Unpooled申请的不一样.

那个safe和unsafe有什么区别呢?

以UnpooledHeapByteBuf和UnpooledUnsafeHeapByteBuf中的getByte(int index)方法为例进行分析

UnpooledHeapByteBuf

@Override
public byte getByte(int index) {
    ensureAccessible();
    return _getByte(index); //真正的获取字节的方法
}

@Override
protected byte _getByte(int index) {
    return HeapByteBufUtil.getByte(array, index);  //通过HeapByteBufUtil工具类获取数据
}

HeapByteBufUtil

static byte getByte(byte[] memory, int index) {
    return memory[index];
}

UnpooledHeapByteBuf从堆内数组中获取数据,这是安全的

UnpooledUnsafeHeapByteBuf

 @Override
public byte getByte(int index) {
    checkIndex(index);
    return _getByte(index);
}

@Override
protected byte _getByte(int index) {
    return UnsafeByteBufUtil.getByte(array, index);
}

PlatformDependent0

static byte getByte(byte[] data, int index) {
    return UNSAFE.getByte(data, BYTE_ARRAY_BASE_OFFSET + index);
}

UnpooledUnsafeHeapByteBuf是通过UNSAFE来操作内存的


现在我们来研究一下Unsafe

Unsafe的实现

Unsafe意味着不安全的操作,但是更底层的操作会带来性能提升和特殊功能,Netty中会尽力使用unsafe以提升系统性能

Java语言很重要的特性就是一次编译到处运行,所以它针对底层的内存或者其他操作做了很多封装,而unsafe提供了一系列我们操作底层的方法,可能会导致不兼容或不可知的异常.

比如:

  • 返回一些低级的内存信息
    • addressSize
    • pageSize
  • 提供用于操作对象及其字段的方法
    • allocateInstance
    • objectFieldOffset
  • 提供用于操作类及其静态字段的方法
    • staticFieldOffset
    • defineClass
    • defineAnonymousClass
    • ensureClassInitialized
  • 低级的同步原语
    • monitorEnter
    • tryMonitorEnter
    • monitorExit
    • compareAndSwapInt
    • putOrderedInt
  • 直接访问内存的方法 allocateMomery copyMemory freeMemory getAddress getInt putInt
  • 操作数组
    • arrayBaseoffset
    • arrayIndexScale

既然这些东西都是jdk封装好的,而是netty也是直接使用的,所以我们无论在使用safe还是unsafe的时候都是无感知的,我们无需关系底层的操作逻辑,因为api都是一样的,只是实现不一样


是否还有一个疑问,池化和非池化是什么意思?

池化和非池化

比如在使用Unpooled.buffer(10)申请一个缓存区的时候,默认非池化申请的一个缓冲区.

池化和非池化的区别主要是申请内存缓存空间以及缓存空间的使用上,体现为内存复用.

  • 在申请内存缓存空间方面:
    • pool:池化申请的时候会申请一个比当前所需内存空间更大的内存空间,这就好比一个快递柜,为此netty提供了buf分配管理器专门用来处理这种事情,来创建或复用ByteBuf.
    • unpool:非池化申请只会申请特定大小能够使用的内存缓存空间,使用完之后立刻释放,这就像直接把快递放到你的手中,你所在的位置就是开辟的内存空间.
  • 在缓存空间使用方面:
    • pool:池化申请的内存空间有一定扩容容积,也就是这个快递柜可以存放多个快递,只需要找到对应的方格即可存放,同样buf分配管理器来复用已经创建好的内存空间,在创建ByteBuf的时候已经开辟3中大小的内存块 normal:16MN small:8KB tiny:512B
    • unpool:毫无疑问,非池化的方式必然是每次都会再去开辟内存空间的.

理论如此,netty中是如何做到内存复用的?

在netty中每一个EventLoopThread由PooledBytebufAllocator内存分配器实力维护了一个线程变量叫做PoolThreadCache,在这个变量中维护了3种规格的MemoryRegionCache数组用作内存缓存,MemoryRegionCache内部是链表,队列里面存Chunk.

首先内存内存分配器会寻找合适的ByteBuf对象进行复用;

之后从内存数组中找到合适的内存空间进行复用;

PoolChunk里面维护了内存引用,内存复用的做法就是把ByteBuf的memory指向chunk的memory.

如果没有找到对应的缓存空间,则直接向内存申请unpool的缓存空间.

netty中默认(池化)也是这样做的,这也是netty性能高效的一个原因,但是就像example例子一样,如果我们自己创建的话,netty推荐我们使用unpool.

面试官:Netty中的缓冲区为什么比原生NIO更高效

 

==需要注意的是即使创建了可复用的ByteBuf,但是使用过后一直没有被release,也就是没有被回收也是不能被复用的,这是应用设计时应该注意的.==


说了半天的废话,总算是要说到零拷贝机制了

零拷贝机制

Netty的零拷贝机制是一种应用层的表现,和底层JVM/操作系统内存机制并无过多关联,你可认为netty就是一个软件,我们在用这个软件来创造另一个软件.

  1. CompositeByteBuf,将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝 什么意思呢?这是一个虚拟的ByteBuf,这个ByteBuf并不是一个,而是一个复合缓冲区,有多个独立的ByteBuf CompositeByteBuf compositeByteBuf = Unpooled.CompositeBuffer(); ByteBuf byteBuf = compositeByteBuf.addComponents(true,buffer1,buffer2); 复制代码
  2. wrapedBuffer()方法,将byte[]数组包装成ByteBuf对象 什么意思呢?这也是一个虚拟的ByteBuf,这个新创建的ByteBuf只是通过memory对此字节数组做了一个引用,避免了复制带来的性能损耗. ByteBuf byteBuf = Unpooled.wrAppedBuffer(new byte[]{1,2,3,4,5}); 复制代码
  3. slice()方法,将一个ByteBuf对象切分成多个ByteBuf对象 什么意思呢?这还是一个虚拟的ByteBuf,只不过拆分出去的ByteBuf中的memory引用的只是拆分出去的字节位置,并且会以unwarp保留一个对原ByteBuf的引用. ByteBuf byteBuf = Unpooled.wrappedBuffer("hello".getBytes()); ByteBuf newByteBuf = byteBuf.slice(1,2); 复制代码

ByteBuf的零拷贝机制也是Netty高性能的一个原因.

作者:梧桐小站
链接:
https://juejin.cn/post/7049754668653608997
来源:稀土掘金



Tags:Netty   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
Netty自己的ByteBufByteBuf是为了解决ByteBuffer的问题和满足网络应用程序开发人员的日常需求而设计的.JDK中ByteBuffer的缺点: 无法动态扩容 长度是固定的,不能动态扩展和...【详细内容】
2022-01-07  Tags: Netty  点击:(0)  评论:(0)  加入收藏
前言在实现TCP长连接功能中,客户端断线重连是一个很常见的问题,当我们使用netty实现断线重连时,是否考虑过如下几个问题: 如何监听到客户端和服务端连接断开 ? 如何实现断线后重...【详细内容】
2021-12-24  Tags: Netty  点击:(19)  评论:(0)  加入收藏
这篇文章对于排查使用了 netty 引发的堆外内存泄露问题,有一定的通用性,希望对你有所启发 背景最近在做一个基于 websocket 的长连中间件,服务端使用实现了 socket.io 协议(基于...【详细内容】
2021-12-16  Tags: Netty  点击:(31)  评论:(0)  加入收藏
简介在之前的文章中,我们提到了在netty的客户端通过使用Http2FrameCodec和Http2MultiplexHandler可以支持多路复用,也就是说在一个连接的channel基础上创建多个子channel,通过...【详细内容】
2021-12-14  Tags: Netty  点击:(12)  评论:(0)  加入收藏
本系列为 Netty 学习笔记,本篇介绍总结Java NIO 网络编程。Netty 作为一个异步的、事件驱动的网络应用程序框架,也是基于NIO的客户、服务器端的编程框架。其对 Java NIO 底层...【详细内容】
2021-12-07  Tags: Netty  点击:(20)  评论:(0)  加入收藏
想要阅读Netty源码的同学,建议从GitHub上把源码拉下来,方便写注释、Debug调试哦~点我去下载! 先来看一个简单的Echo服务端程序,监听本地的9999端口,有客户端接入时控制台输出一句...【详细内容】
2021-10-22  Tags: Netty  点击:(46)  评论:(0)  加入收藏
相信很多人知道石中剑这个典故,在此典故中,天命注定的亚瑟很容易的就拔出了这把石中剑,但是由于资历不被其他人认可,所以他颇费了一番周折才成为了真正意义上的英格兰全境之王,亚...【详细内容】
2021-07-22  Tags: Netty  点击:(103)  评论:(0)  加入收藏
家纯 阿里技术 一 什么是 Netty? 能做什么? Netty 是一个致力于创建高性能网络应用程序的成熟的 IO 框架。 相比较与直接使用底层的 Java IO API,你不需要先成为网络专家就...【详细内容】
2021-06-23  Tags: Netty  点击:(138)  评论:(0)  加入收藏
客户端代码package com.huanfeng.test;import java.io.BufferedReader;import java.io.InputStreamReader;import java.net.URL;import java.net.URLConnection;import java...【详细内容】
2021-05-26  Tags: Netty  点击:(146)  评论:(0)  加入收藏
前言:作为一个刚踏入职场的实习生,我很幸运参加了某个政府项目,并且在项目中负责一个核心模块功能的开发,而不是从头到尾对数据库的crud。虽然我一直心里抱怨我的工作范围根本...【详细内容】
2021-05-24  Tags: Netty  点击:(237)  评论:(0)  加入收藏
▌简易百科推荐
Netty自己的ByteBufByteBuf是为了解决ByteBuffer的问题和满足网络应用程序开发人员的日常需求而设计的.JDK中ByteBuffer的缺点: 无法动态扩容 长度是固定的,不能动态扩展和...【详细内容】
2022-01-07  互联网高级架构师    Tags:Netty   点击:(0)  评论:(0)  加入收藏
服务器或者租用机柜的时候,有时会听到1U、2U、4U或者42U等类似这样子的名词。而这些名词又代表什么意思呢?机架式服务器的外形看来不像计算机,而像交换机,有1U(1U=1.75英寸)、2U、...【详细内容】
2022-01-06  硬件十万个为什么    Tags:服务器   点击:(3)  评论:(0)  加入收藏
当你点击 Ubuntu 网站上的下载按钮时,它会给你几个选项。其中两个分别是 Ubuntu 桌面版和 Ubuntu 服务器版。这可能会让新用户感到困惑。为什么会有两个(实际上是四个)?应该下载...【详细内容】
2022-01-05  硬核老王  Linux中国  Tags:Ubuntu   点击:(4)  评论:(0)  加入收藏
1.首先在windows系统中建一个vbs文件,文件内容例如:set os=createobject("wscript.shell")dowscript.sleep 2000(注解:这里是多长时间上传一次)os.run "C:\file.bat",0(注解:调用批...【详细内容】
2022-01-04  DIQI    Tags:linux服务器   点击:(4)  评论:(0)  加入收藏
一、背景介绍Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。二、安装$ wget http://nginx.org/download/nginx-1.14.2.tar.gz#...【详细内容】
2022-01-04  程序员涛哥    Tags:nginx   点击:(5)  评论:(0)  加入收藏
安装:yum install openldap openldap-servers openldap-clients拷贝数据库配置文件cp /usr/share/openldap-servers/DB_CONFIG.example /var/lib/ldap/DB_CONFIGchown ldap:...【详细内容】
2021-12-31  ethnicity    Tags:CentOS7   点击:(7)  评论:(0)  加入收藏
安装Nginx在Mac上有一个很好用的包管理插件,名为homebrew。 具体的安装可以自行去搜索下。下面就借助Homebrew来安装Nginx。首先是拉取Nginx$ brew tap home/nginx执行安装$...【详细内容】
2021-12-31  lee哥的服务器开发    Tags:Nginx   点击:(13)  评论:(0)  加入收藏
在生产环境中为了保证网络的更高可用性,我们一般都会将网络做bond 。也称为双网卡绑定。先看看我们bond 的模式:  bond0: 平衡轮循环策略,有自动备援,不过需要交换机支持 。 ...【详细内容】
2021-12-29  忆梦如风    Tags:linux-centos   点击:(11)  评论:(0)  加入收藏
阿里云镜像源地址及安装网站地址https://developer.aliyun.com/mirror/centos?spm=a2c6h.13651102.0.0.3e221b111kK44P更新源之前把之前的国外的镜像先备份一下 切换到yumcd...【详细内容】
2021-12-27  干程序那些事    Tags:CentOS7镜像   点击:(11)  评论:(0)  加入收藏
前言在实现TCP长连接功能中,客户端断线重连是一个很常见的问题,当我们使用netty实现断线重连时,是否考虑过如下几个问题: 如何监听到客户端和服务端连接断开 ? 如何实现断线后重...【详细内容】
2021-12-24  程序猿阿嘴  CSDN  Tags:Netty   点击:(19)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条