聊聊 Netty 对于内存方面的优化

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

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

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

1.使用基本类型替代包装类

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

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

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

@SuppressWarnings("UnusedDeclaration")
    private volatile long totalPendingSize;

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

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

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

2.动态内存调整

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

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;
            }
        }

3.应用层面的zero-copy

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

  • 开辟内存空间所占用的时间。
  • 将byte1内存新开辟空间的耗时。
  • 将byte2写入新开辟的内存空间耗时。

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

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

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;
    }

4.使用堆外内存

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

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

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

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

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

5.内存池化复用

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

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

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

//初始化直接内存池化工厂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;
    }

6.对jdk零拷贝的封装

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

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

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;
    }
责任编辑:赵宁宁 来源: 写代码的SharkChili
相关推荐

2020-08-03 10:53:25

存储容器虚拟机

2023-12-02 20:41:32

内存kube

2010-04-23 16:04:48

Oracle查询优化

2023-02-28 08:55:33

GatewayNetty服务

2022-01-13 09:23:48

Redis架构优化

2010-07-30 13:06:22

NFS端口

2012-02-17 09:33:52

虚拟化桌面虚拟化

2024-02-29 18:06:39

HTTP性能优化

2021-01-07 07:53:10

JavaScript内存管理

2021-01-14 08:58:12

Synchronize锁操作

2010-07-07 18:00:43

SNMP协议

2010-07-01 15:06:23

SNMP服务配置

2013-05-06 15:42:49

2015-06-24 09:45:23

容器网络容器网络优化

2009-12-30 17:30:43

EPON技术

2010-05-10 14:39:43

网络负载均衡

2022-10-28 07:27:17

Netty异步Future

2022-03-04 08:10:35

NettyIO模型Reactor

2016-08-31 13:40:03

IBM

2024-04-11 13:02:10

Rust数据类型
点赞
收藏

51CTO技术栈公众号