从Netty的ByteBuf中学习高并发场景下的内存优化艺术

写在文章开头

Netty通过巧妙的内存使用技巧尽可能节约内存空间,进而减少javaFull gcSTW的时间,由此间接的提升了程序的性能,本文也将直接从源码的角度分析一下Netty对于内存方面的使用技巧,希望对你有所启发。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的技术人,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

同时也非常欢迎你star我的开源项目mini-redis:github.com/shark-ctrl/...

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。

详解Netty中的内存的优化思路

使用基本类型替代包装类

内存空间算是宝贵的系统资源,为了提升CPU加载数据效率以及节约内存空间,对于某些常见的基本数据类型,Netty都是能省则省,最直接的落地方案就是使用基本类型替代包装类。

这其中totalPendingSize这个变量,它用于记录那些待处理的数据,为了节约内存空间,记录大小的类型是long而非Long,通过这种方式避免了创建java对象(java对象包含对象头的信息,相比基本类型更占用内存空间):

对此我们也给出这个变量的定义:

java 复制代码
@SuppressWarnings("UnusedDeclaration")
    private volatile long totalPendingSize;

又因为该字段需要保证线程安全,所以Netty设计者在此基础上又将其设置为AtomicLong原子类型,通过static关键字加以修饰,使所有实例共享一个变量,从而避免没必要的创建开销和并发安全:

对此我们也给出源码示例,即位于ChannelOutboundBuffer变量定义的位置:

arduino 复制代码
//通过AtomicLongFieldUpdater修饰totalPendingSize
  private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER =
            AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");

动态内存调整

除上述内存使用技巧以外,netty在进行内存分配时也用到的动态调整的使用技巧,该设计理念比较简单,按照空间与分配思想:后续使用的内存大小大概率是等同于本次使用的空间大小,所以Netty在调用record进行内存分配时,如果发现缩小空间依然可以满足要求,则进行缩容,反之进行扩容,由此得到一个尽可能节约内存空间且能满足业务要求的数值:

ini 复制代码
private void record(int actualReadBytes) {
            //若实际需要的空间 <= 预缩小达到的尺寸,则对nextReceiveBufferSize进行缩减
            if (actualReadBytes <= SIZE_TABLE[max(0, index - INDEX_DECREMENT)]) {
                if (decreaseNow) {
                    index = max(index - INDEX_DECREMENT, minIndex);
                    nextReceiveBufferSize = SIZE_TABLE[index];
                    decreaseNow = false;
                } else {
                    decreaseNow = true;
                }
            } else if (actualReadBytes >= nextReceiveBufferSize) {//如果所需空间大于nextReceiveBufferSize,则进行扩容
                index = min(index + INDEX_INCREMENT, maxIndex);
                nextReceiveBufferSize = SIZE_TABLE[index];
                decreaseNow = false;
            }
        }

应用层面的zero-copy

内存拷贝也是存在一定的时间开销,例如我们现在有一个字符串的数据需要将byte1byte2拼接起来才能得到,按照传统的实现思路,我们需要开发一个足够容纳byte1byte2的内存空间,然后将byte1byte2一并写入,这种做法有着如下耗时点:

  1. 开辟内存空间所占用的时间。
  2. byte1内存新开辟空间的耗时。
  3. byte2写入新开辟的内存空间耗时。

Netty则不是这样做,它的设计思路是直接将两个数组,逻辑上组合,即通过一个数组指向这两个引用,从逻辑上视为一个整体,而不是物理操作上的组合:

对此我们给出CompositeByteBufaddComponent0方法,可以看到对于需要组合的数据buffer,它会通过addComp方法将这个ByteBuf 存到CompositeByteBuf底层的数组中,由此保证数据逻辑上的一致:

java 复制代码
private int addComponent0(boolean increaseWriterIndex, int cIndex, ByteBuf buffer) {
        assert buffer != null;
        boolean wasAdded = false;
        try {
            checkComponentIndex(cIndex);

            //将其包装为Component 
            Component c = newComponent(ensureAccessible(buffer), 0);
            int readableBytes = c.length();

           //......
   //添加到CompositeByteBuf底层的components数组中,通过逻辑完成组合
            addComp(cIndex, c);
           //......
            return cIndex;
        } finally {
          //......
        }
    }

//添加到components数组中保证逻辑上的一致
private void addComp(int i, Component c) {
        //......
        components[i] = c;
    }

使用堆外内存

将数据存放在JVM非堆内存空间,通过减少没必要的GC确保操作和执行性能的高效,这也是Netty中对于内存方面的优化,这其中最经典的就是PooledHeapByteBuf,它直接操作的就是堆外内存的数据:

对此我们也给处PooledDirectByteBuf 获取直接内存的源码实现:

csharp 复制代码
//从内存池中获取直接内存空间返回给用户使用
  static PooledDirectByteBuf newInstance(int maxCapacity) {
        PooledDirectByteBuf buf = RECYCLER.get();
        buf.reuse(maxCapacity);
        return buf;
    }

需要补充的是,这种做法也存在的一定的风险:

  1. 创建速度慢。
  2. 存放在非堆内存空间,使用不当可能造成内存泄漏。

内存池化复用

上文的堆内存就是PooledHeapByteBuf即池化过的内存,通过池化:

  1. 保证对象复用,减小没必要的创建开销。
  2. 提升程序并发执行性能。

对此我们给出相应的源码实现:

java 复制代码
//初始化直接内存池化工厂RECYCLER 
private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(
            new ObjectCreator<PooledDirectByteBuf>() {
        @Override
        public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
            return new PooledDirectByteBuf(handle, 0);
        }
    });


//从内存池中获取直接内存空间返回给用户使用
  static PooledDirectByteBuf newInstance(int maxCapacity) {
    //从内存池中获取直接内存空间
        PooledDirectByteBuf buf = RECYCLER.get();
        buf.reuse(maxCapacity);
        return buf;
    }

对jdk零拷贝的封装

我们在上述所讲的零复制更多强调的是应用层面上的零复制,也就是通过减少应用层面上数据的拷贝提升程序的执行效率。实际上Netty也有基于操作系统层面的零拷贝实现,这其中最典型的实现就是DefaultFileRegiontransferTo函数,它底层调用JDK自带的NIO零拷贝方法transferTo实现当前文件数据通过sendfile调用传输到socket通道中,由此避免数据传输时多次切态、内核缓冲区和用户缓冲区来回拷贝的开销:

对此我们也给出DefaultFileRegion类中transferTo的源码,可以看到其底层就是将JDK默认的NIO零拷贝方法进行封装,将DefaultFileRegion封装的FileChannel 的文件数据拷贝到target的文件通道中,其底层就用到内核函数sendfile

arduino 复制代码
private FileChannel file;

 @Override
    public long transferTo(WritableByteChannel target, long position) throws IOException {
        //......

        long written = file.transferTo(this.position + position, count, target);
        if (written > 0) {
            transferred += written;
        } else if (written == 0) {
           //......
        }
        return written;
    }

小结

自此,本文从netty源码的角度深入剖析和netty那些对于内存方面的优化和使用技巧,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家mini-redis 的作者,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili

同时也非常欢迎你star我的开源项目mini-redis:github.com/shark-ctrl/...

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 "加群" 即可和笔者和笔者的朋友们进行深入交流。

参考

【Netty】Netty零拷贝原理:blog.csdn.net/agonie20121...

原来 8 张图,就可以搞懂「零拷贝」了:www.cnblogs.com/xiaolincodi...

本文使用 markdown.com.cn 排版

相关推荐
菜鸟谢12 分钟前
c# 文件系统
后端
写bug写bug29 分钟前
Java并发编程:什么是线程组?它有什么作用?
java·后端
Andya_net36 分钟前
SpringBoot | 构建客户树及其关联关系的设计思路和实践Demo
java·spring boot·后端
南囝coding1 小时前
关于我的第一个产品!
前端·后端·产品
北漂老男孩2 小时前
Spring Boot 自动配置深度解析:从源码结构到设计哲学
java·spring boot·后端
陈明勇2 小时前
MCP 实战:用 Go 语言开发一个查询 IP 信息的 MCP 服务器
人工智能·后端·mcp
小咕聊编程2 小时前
【含文档+PPT+源码】基于SpringBoot+Vue的移动台账管理系统
java·spring boot·后端
景天科技苑2 小时前
【Rust结构体】Rust结构体详解:从基础到高级应用
开发语言·后端·rust·结构体·关联函数·rust结构体·结构体方法
-曾牛2 小时前
Spring Boot常用注解详解:实例与核心概念
java·spring boot·后端·spring·java-ee·个人开发·spring boot 注解
得物技术3 小时前
得物业务参数配置中心架构综述
后端·架构