Netty 如何高效的使用内存

Netty 如何高效的使用内存

系统性能优化之内存篇 中提到了如何更好的使用内存,本篇以 netty 为例赏析 netty 中的内存使用技巧。

基本类型 vs 包装类

Java 中的对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。在64位虚拟机中对象头就需要占用64bit的空间,比一些基本类型(byte、short、int等)占用的空间都要大。

因此能用基本类型就不要用包装类

变量的定义

一个类在程序执行中可能会创建多个对象,如果这个变量是实例变量,则每个类对象都会有一份,而如果定义为类变量,则所有实例共享

Netty 作为网络通信框架,创建的 Channel 不可估量,需要统计每一个 channel 待发送的数据(判断写缓冲区是否处于"高水位"),这个字段会被 IO 线程消费减少,同时可能被业务线程生产增加,如果使用 AtomicLong 类型,则每个 channel 都会多一份对象头的开销。Netty 定义了一个 volatile 变量和一个静态的 AtomicLongFieldUpdater,大大节省了内存。

代码参见:io/netty/channel/ChannelOutboundBuffer.java

java 复制代码
    private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER;

    private volatile long totalPendingSize;

预估内存

定义 HashMap、List 等可能扩容的对象时指定初始容量不必多说,Netty 会根据接受到的数据动态调整(guess)下个要分配的 Buffer 大小。

代码参见:io/netty/channel/AdaptiveRecvByteBufAllocator.java(省略部分代码)

java 复制代码
    private final class HandleImpl extends MaxMessageHandle {
        
        private int nextReceiveBufferSize;

        @Override
        public int guess() {
            return nextReceiveBufferSize;
        }

        private void record(int actualReadBytes) {
            if (actualReadBytes <= SIZE_TABLE[Math.max(0, index - INDEX_DECREMENT - 1)]) {
                if (decreaseNow) {
                    index = Math.max(index - INDEX_DECREMENT, minIndex);
                    nextReceiveBufferSize = SIZE_TABLE[index];
                    decreaseNow = false;
                } else {
                    decreaseNow = true;
                }
            } else if (actualReadBytes >= nextReceiveBufferSize) {
                index = Math.min(index + INDEX_INCREMENT, maxIndex);
                nextReceiveBufferSize = SIZE_TABLE[index];
                decreaseNow = false;
            }
        }

    }

逻辑组合代替复制

当读取 channel 中的数据时,如果有新的数据来到,通过 addComponent 将新数据添加到缓冲对象里,即只是新增一个引用,无需真正复制内存。

代码参见:io/netty/handler/codec/ByteToMessageDecoder.java

java 复制代码
    /**
     * Cumulate {@link ByteBuf}s by add them to a {@link CompositeByteBuf} and so do no memory copy whenever possible.
     * Be aware that {@link CompositeByteBuf} use a more complex indexing implementation so depending on your use-case
     * and the decoder implementation this may be slower then just use the {@link #MERGE_CUMULATOR}.
     */
    public static final Cumulator COMPOSITE_CUMULATOR = new Cumulator() {
        @Override
        public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
            ByteBuf buffer;
            if (cumulation.refCnt() > 1) {
                // Expand cumulation (by replace it) when the refCnt is greater then 1 which may happen when the user
                // use slice().retain() or duplicate().retain().
                //
                // See:
                // - https://github.com/netty/netty/issues/2327
                // - https://github.com/netty/netty/issues/1764
                buffer = expandCumulation(alloc, cumulation, in.readableBytes());
                buffer.writeBytes(in);
                in.release();
            } else {
                CompositeByteBuf composite;
                if (cumulation instanceof CompositeByteBuf) {
                    composite = (CompositeByteBuf) cumulation;
                } else {
                    composite = alloc.compositeBuffer(Integer.MAX_VALUE);
                    composite.addComponent(true, cumulation);
                }
                composite.addComponent(true, in);
                buffer = composite;
            }
            return buffer;
        }
    };

零拷贝

"zero-copy"既是优化内存的手段,也是优化 IO 的手段,简单来说就是 IO 读操作需要把字节从内核态拷贝到用户态的应用程序中,写操作又需要拷贝回内核的缓冲区中。如果应用程序无需对文件进行操作,那么直接在内核态进行拷贝,就可以减少拷贝次数。

Netty 在 io/netty/channel/DefaultFileRegion.java 中也是使用了 JDK 原生的零拷贝(transferTo)实现

java 复制代码
    public long transferTo(WritableByteChannel target, long position) throws IOException {
        long count = this.count - position;
        if (count < 0 || position < 0) {
            throw new IllegalArgumentException(
                    "position out of range: " + position +
                    " (expected: 0 - " + (this.count - 1) + ')');
        }
        if (count == 0) {
            return 0L;
        }
        if (refCnt() == 0) {
            throw new IllegalReferenceCountException(0);
        }
        // Call open to make sure fc is initialized. This is a no-oop if we called it before.
        open();

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

堆外内存

堆外内存由于不受 Java GC 的影响,更加的稳定、持久,但可能由于内存原因被操作系统杀死进程。

Netty 在池化内存池 PooledByteBufAllocator 的默认实现中设置的使用堆外内存。

java 复制代码
    private static final boolean DIRECT_BUFFER_PREFERRED =
            HAS_UNSAFE && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);

io.netty.noPreferDirect 设置为 true,则使用堆内内存。

对象/内存池

io.netty.allocator.type 是一个关键的参数,它直接决定了 Netty 内部用来管理 ByteBuf 内存的分配器(Allocator)类型。简单来说,它控制着 Netty 是自己高效地管理和复用内存(池化) ,还是简单直接地向操作系统申请和释放内存(非池化)。非安卓平台默认是池化实现。

源码参见:io/netty/buffer/ByteBufUtil.java

java 复制代码
    static {
        String allocType = SystemPropertyUtil.get(
                "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
        allocType = allocType.toLowerCase(Locale.US).trim();

        ByteBufAllocator alloc;
        if ("unpooled".equals(allocType)) {
            alloc = UnpooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else if ("pooled".equals(allocType)) {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
        }

        DEFAULT_ALLOCATOR = alloc;

        THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 64 * 1024);
        logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);

        MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16 * 1024);
        logger.debug("-Dio.netty.maxThreadLocalCharBufferSize: {}", MAX_CHAR_BUFFER_SIZE);
    }

可通过 ServerBootstrap.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) 设置,在io/netty/buffer/PooledDirectByteBuf.java 创建 buffer 时会直接从池中取。

当创建一个对象的成本比较大,而且创建频率高且可复用时,使用对象池可以更好的节省资源。

Netty 自行实现了轻量级(相比 Apache Commons Pool)的对象池:io/netty/util/Recycler.java

相关推荐
佛祖让我来巡山4 天前
Netty保姆级全解析|技术背景+核心知识点+生产实战教程
netty
佛祖让我来巡山6 天前
Netty入门|从BIO到Netty:一步步看懂Java网络编程的迭代逻辑
netty·nio·bio
文慧的科技江湖13 天前
光伏储能充电系统PRD功能列表 - 慧知开源充电桩平台
开发语言·开源·netty·慧知开源充电桩平台·开源充电桩平台
不早睡不改名@22 天前
Netty源码分析---Reactor线程模型深度解析(一)
java·笔记·学习·netty
zs宝来了22 天前
Netty Reactor 模型:Boss、Worker 与 EventLoop
reactor·netty·源码解析·线程模型·eventloop
不早睡不改名@23 天前
Netty源码分析---Reactor线程模型深度解析(二)
java·网络·笔记·学习·netty
不早睡不改名@24 天前
Netty源码解析---FastThreadLocal-addToVariablesToRemove方法详解
java·网络·笔记·学习·netty
MrSYJ1 个月前
有没有人懂socketChannel中的write,read方法啊,给我讲讲
java·程序员·netty
尽兴-1 个月前
RocketMQ核心源码深度解读:架构原理与核心机制剖析
架构·rocketmq·netty·架构原理·消息持久化