netty4.0源码分析之PooledByteBufAllocator

 

概述

netty 内存管理的高性能主要依赖于两个关键点:

  • 内存的池化管理
  • 使用堆外直接内存(Direct Memory)

从netty 4开始,netty加入了内存池管理,采用内存池管理比普通的new ByteBuf性能提高了数十倍。
       首先介绍PoolChunk, 该类主要负责内存块的分配与回收,首先来看看两个重要的术语:
          page: 可以分配的最小的内存块单位。
          chunk: 一堆page的集合。
       下面一张图直观的表述了PoolChunk是如何管理内存的:
       
       上图中是一个默认大小的chunk, 由2048个page组成了一个chunk,一个page的大小为8192, chunk之上有11层节点,最后一层节点数与page数量相等。每次内存分配需要保证内存的连续性,这样才能简单的操作分配到的内存,因此这里构造了一颗完整的平衡二叉树,所有子节点的管理的内存也属于其父节点。如果想获取一个8K的内存,则只需在第11层找一个可用节点即可,而如果需要16K的数据,则需要在第10层找一个可用节点,因为需要两个第11层节点。如果一个节点存在一个已经被分配的子节点,则该节点不能被分配,例如需要16K内存,这个时候id=2048的节点已经被分配,id=2049的节点未分配,就不能直接分配1024这个节点,因为这个节点下的内存只有8K了。
       通过上面这个树结构,可以看到每次内存分配都是8K*(2^n), 比如需要24K内存时,实际上会申请到一块32K的内存。为了分配一个大小为chunkSize/(2^i)的内存段,需要在深度为i的层从左开始查找可用节点。如想分配16K的内存,chunkSize = 16M( 2048个page * 8K ), 则i=10, 需要从第10层找一个空闲的节点分配内存。

       负责内存分配的PoolChunk类,它最小的分配单位为page, 而默认的page size为8K。在实际的应用中,会存在很多小块内存的分配,如果小块内存也占用一个page明显很浪费,针对这种情况,可以将8K的page拆成更小的块,这已经超出chunk的管理范围了,这个时候就出现了PoolSubpage, 其实PoolSubpage做的事情和PoolChunk做的事情类似,只是PoolSubpage管理的是更小的一段内存。
       
       如上图,PoolSubpage将chunk中的一个page再次划分,分成相同大小的N份,这里暂且叫Element,通过对每一个Element的标记与清理标记来进行内存的分配与释放。

       介绍了PoolChunk以及针对page的更细粒度的PoolSubpage,其实在chunk的上层还有一个管理类:PoolChunkList,PoolChunkList负责管理多个chunk的生命周期,在此基础上对内存分配进行进一步的优化。
       PoolChunkList主要是为了提高内存分配的效率,每个list中包含多个chunk,而多个list又可以形成一个大的link list,在进行内存分配时,先从比较靠前的list中分配内存,这样分配到的几率更大。在高峰期申请过多的内存后,随着流量下降慢慢的释放掉多余内存,形成一个良性的循环。下图是上述三个类的层次结构:
       
 
已经讲到了内存池中的几个重要的类:
       1、PoolChunk:维护一段连续内存,并负责内存块分配与回收,其中比较重要的两个概念:page:可分配的最小内存块单位;chunk:page的集合;
       2、PoolSubpage:将page分为更小的块进行维护;
       3、PoolChunkList:维护多个PoolChunk的生命周期。
       多个PoolChunkList也会形成一个list,方便内存的管理。最终由PoolArena对这一系列类进行管理,PoolArena本身是一个抽象类,其子类为HeapArena和DirectArena,对应堆内存(heap buffer)和堆外内存(direct buffer),除了操作的内存(byte[]和ByteBuffer)不同外两个类完全一致。

内存池内存分配流程:
       1、ByteBufAllocator 准备申请一块内存;
       2、尝试从PoolThreadCache中获取可用内存,如果成功则完成此次分配,否则继续往下走,注意后面的内存分配都会加锁;
       3、如果是小块(可配置该值)内存分配,则尝试从PoolArena中缓存的PoolSubpage中获取内存,如果成功则完成此次分配;
       4、如果是普通大小的内存分配,则从PoolChunkList中查找可用PoolChunk并进行内存分配,如果没有可用的PoolChunk则创建一个并加入到PoolChunkList中,完成此次内存分配;
       5、如果是大块(大于一个chunk的大小)内存分配,则直接分配内存而不用内存池的方式;
       6、内存使用完成后进行释放,释放的时候首先判断是否和分配的时候是同一个线程,如果是则尝试将其放入PoolThreadCache,这块内存将会在下一次同一个线程申请内存时使用,即前面的步骤2;
       7、如果不是同一个线程,则回收至chunk中,此时chunk中的内存使用率会发生变化,可能导致该chunk在不同的PoolChunkList中移动,或者整个chunk回收(chunk在q000上,且其分配的所有内存被释放);同时如果释放的是小块内存(与步骤3中描述的内存相同),会尝试将小块内存前置到PoolArena中,这里操作成功了,步骤3的操作中才可能成功。
       在PoolThreadCache中分了tinySubPageHeapCaches、smallSubPageHeapCaches、normalSubPageHeapCaches三个数组,对应于tiny\small\normal在内存分配上的不同(tiny和small使用subpage,normal使用page)。
       到此,netty内存池相关介绍已经完,netty就是实现了两个比较经典的分配策略,buddy allocation(见PoolChunk)和jemalloc(有一定改动,PooledByteBufAllocator+PoolArena+PoolChunk+PoolThreadCache),所以如果想了解更新的信息,可以按照上面两个关键词搜索,或则看转载的原文。
 

netty内存池可调优参数

 

参数名说明默认值
io.netty.allocator.pageSizepage的大小8192
io.netty.allocator.maxOrder一个chunk的大小=pageSize << maxOrder11
io.netty.allocator.numHeapArenasheap arena的个数min(cpu核数,maxMemory/chunkSize/6),一般来说会=cpu核数
io.netty.allocator.numDirectArenasdirect arena的个数min(cpu核数,directMemory/chunkSize/6),一般来说会=cpu核数
io.netty.allocator.tinyCacheSizePoolThreadCache中tiny cache每个MemoryRegionCache中的Entry个数512
io.netty.allocator.smallCacheSizePoolThreadCache中small cache每个MemoryRegionCache中的Entry个数256
io.netty.allocator.normalCacheSizePoolThreadCache中normal cache每个MemoryRegionCache中的Entry个数64
io.netty.allocator.maxCachedBufferCapacityPoolThreadCache中normal cache数组长度32 * 1024
io.netty.allocator.cacheTrimIntervalPoolThreadCache中的cache收缩阈值,每隔该值次数,会进行一次收缩8192
io.netty.allocator.typeallocator类型,如果不使用内存池,则设置为unpooledpooled
io.netty.noUnsafe是否关闭direct bufferfalse
io.netty.leakDetectionLevel内存泄露检测级别SIMPLE

使用PooledByteBufAllocator

netty 基于两个维度:池化/非池化、Heap Memory/Direct Memory 的组合来确定最终使用的内存管理策略。对 netty 应用首先要能确定netty 到底采用了哪种内存管理策略,才能对各种情况下的性能表现有预期。根据 ByteBufUtil 代码:

 

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

需要在 netty 应用启动时,设置 JVM参数 -Dio.netty.allocator.type=pooled 设置池化管理策略,而根据 PlatformDependent 中的代码片段:

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

 ByteBufAllocator接口定义了默认的实现类

/**
 * Implementations are responsible to allocate buffers. Implementations of this interface are expected to be
 * thread-safe.
 */
public interface ByteBufAllocator {

    ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
}

只要没有设置 -Dio.netty.noPreferDirect=true 并且运行在标准 Oracle JVM(sun.misc.Unsafe存在)中,就会优先使用 Direct Memory,当然还有一个前提是分配了一定数量的Direct Memory 

转载自:http://blog.csdn.net/youaremoon/article/details/47910971
              http://blog.csdn.net/youaremoon/article/details/47984409
              http://blog.csdn.net/youaremoon/article/details/48085591
              http://blog.csdn.net/youaremoon/article/details/48184429
              http://blog.csdn.net/youaremoon/article/details/50042373
              http://blog.csdn.net/youaremoon/article/details/50054387

https://www.jianshu.com/p/ce7c6f5cb5f6

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值