Netty如何高效接收网络数据?聊透ByteBuffer动态自适应扩缩 七

r660926
发布于 2022-7-28 17:53
浏览
0收藏

5. 使用堆外内存为ByteBuffer分配内存


AdaptiveRecvByteBufAllocator类只是负责动态调整ByteBuffer的容量,而具体为ByteBuffer申请内存空间的是由PooledByteBufAllocator负责。

 

5.1 类名前缀Pooled的来历


在我们使用Java进行日常开发过程中,在为对象分配内存空间的时候我们都会选择在JVM堆中为对象分配内存,这样做对我们Java开发者特别的友好,我们只管使用就好而不必过多关心这块申请的内存如何回收,因为JVM堆完全受Java虚拟机控制管理,Java虚拟机会帮助我们回收不再使用的内存。

 

但是JVM在进行垃圾回收时候的stop the world会对我们应用程序的性能造成一定的影响。

 

除此之外我们在?《聊聊Netty那些事儿之从内核角度看IO模型》一文中介绍IO模型的时候提到,当数据达到网卡时,网卡会通过DMA的方式将数据拷贝到内核空间中,这是第一次拷贝。当用户线程在用户空间发起系统IO调用时,CPU会将内核空间的数据再次拷贝到用户空间。这是第二次拷贝

 

于此不同的是当我们在JVM中发起IO调用时,比如我们使用JVM堆内存读取Socket接收缓冲区中的数据时,会多一次内存拷贝,CPU在第二次拷贝中将数据从内核空间拷贝到用户空间时,此时的用户空间站在JVM角度是堆外内存,所以还需要将堆外内存中的数据拷贝到堆内内存中。这就是第三次内存拷贝

 

同理当我们在JVM中发起IO调用向Socket发送缓冲区写入数据时,JVM会将IO数据先拷贝堆外内存,然后才能发起系统IO调用。

 

那为什么操作系统不直接使用JVM的堆内内存进行IO操作呢?

 

因为JVM的内存布局和操作系统分配的内存是不一样的,操作系统不可能按照JVM规范来读写数据,所以就需要第三次拷贝中间做个转换将堆外内存中的数据拷贝到JVM堆中。

 
所以基于上述内容,在使用JVM堆内内存时会产生以下两点性能影响:

 

1.JVM在垃圾回收堆内内存时,会发生stop the world导致应用程序卡顿。


2.在进行IO操作的时候,会多产生一次由堆外内存到堆内内存的拷贝。


基于以上两点使用JVM堆内内存对性能造成的影响,于是对性能有卓越追求的Netty采用堆外内存也就是DirectBuffer来为ByteBuffer分配内存空间。

 

采用堆外内存为ByteBuffer分配内存的好处就是:

 

•堆外内存直接受操作系统的管理,不会受JVM的管理,所以JVM垃圾回收对应用程序的性能影响就没有了。

 

•网络数据到达之后直接在堆外内存上接收,进程读取网络数据时直接在堆外内存中读取,所以就避免了第三次内存拷贝


所以Netty在进行 I/O 操作时都是使用的堆外内存,可以避免数据从 JVM 堆内存到堆外内存的拷贝。但是由于堆外内存不受JVM的管理,所以就需要额外关注对内存的使用和释放,稍有不慎就会造成内存泄露,于是Netty就引入了内存池堆外内存进行统一管理。

 

PooledByteBufAllocator类的这个前缀Pooled就是内存池的意思,这个类会使用Netty的内存池为ByteBuffer分配堆外内存

 

5.2 PooledByteBufAllocator的创建


创建时机


在服务端NioServerSocketChannel的配置类NioServerSocketChannelConfig以及客户端NioSocketChannel的配置类NioSocketChannelConfig实例化的时候会触发

PooledByteBufAllocator的创建。

public class DefaultChannelConfig implements ChannelConfig {
    //PooledByteBufAllocator
    private volatile ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;

    ..........省略......
}

创建出来的PooledByteBufAllocator实例保存在DefaultChannelConfig类中的ByteBufAllocator allocator字段中。

 

创建过程

public interface ByteBufAllocator {

    ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
    
    ..................省略............
}
public final class ByteBufUtil {

    static final ByteBufAllocator DEFAULT_ALLOCATOR;

    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;
        
        ...................省略..................
    }
}

从ByteBufUtil类的初始化过程我们可以看出,在为ByteBuffer分配内存的时候是否使用内存池在Netty中是可以配置的。

 

•通过系统变量-D io.netty.allocator.type 可以配置是否使用内存池为ByteBuffer分配内存。默认情况下是需要使用内存池的。但是在安卓系统中默认是不使用内存池的。

 

•通过PooledByteBufAllocator.DEFAULT获取内存池ByteBuffer分配器。

   public static final PooledByteBufAllocator DEFAULT =
            new PooledByteBufAllocator(PlatformDependent.directBufferPreferred());

由于本文的主线是介绍Sub Reactor处理OP_READ事件的完整过程,所以这里只介绍主线相关的内容,这里只是简单介绍下在接收数据的时候为什么会用PooledByteBufAllocator来为ByteBuffer分配内存。而内存池的架构设计比较复杂,所以笔者后面会单独写一篇关于Netty内存管理的文章。
 
总结


本文介绍了Sub Reactor线程在处理OP_READ事件的整个过程。并深入剖析了AdaptiveRecvByteBufAllocator类动态调整ByteBuffer容量的原理。

 

同时也介绍了Netty为什么会使用堆外内存来为ByteBuffer分配内存,并由此引出了Netty的内存池分配器PooledByteBufAllocator 。

 

在介绍AdaptiveRecvByteBufAllocator类和PooledByteBufAllocator一起组合实现动态地为ByteBuffer分配容量的时候,笔者不禁想起了多年前看过的《Effective Java》中第16条 复合优先于继承

 

Netty在这里也遵循了这条军规,首先两个类设计的都是单一的功能。

 

•AdaptiveRecvByteBufAllocator类只负责动态的调整ByteBuffer容量,并不管具体的内存分配。

 

•PooledByteBufAllocator类负责具体的内存分配,用内存池的方式。


这样设计的就比较灵活,具体内存分配的工作交给具体的ByteBufAllocator,可以使用内存池的分配方式PooledByteBufAllocator,也可以不使用内存池的分配方式UnpooledByteBufAllocator。具体的内存可以采用JVM堆内内存(HeapBuffer),也可以使用堆外内存(DirectBuffer)。

 

AdaptiveRecvByteBufAllocator只需要关注调整它们的容量工作就可以了,而并不需要关注它们具体的内存分配方式。

 

最后通过io.netty.channel.RecvByteBufAllocator.Handle#allocate方法灵活组合不同的内存分配方式。这也是装饰模式的一种应用。

byteBuf = allocHandle.allocate(allocator);

标签
已于2022-7-28 17:53:13修改
收藏
回复
举报
回复
    相关推荐