死磕 Netty 之内存篇:再探 Netty 池化内存分配管理

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。

本文已收录到我的技术网站:www.skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经


本篇文章我们再来探索 Netty 的池化内存分配管理机制,其实在文章 深挖 Netty 高性能内存管理 已经讲述过一次了,但是那篇文章是基于 jemalloc3 版本,而这篇内容则是基于 jemalloc4 版本(包括后面的源码部分),也是大明哥在写这个系列之前的最新版本 4.1.77.Final

Netty 从 4.1.45 版本开始,基于 jemalloc4.x 算法对内存模块进行重构,两者差别比较大。

内存规格

在文章 深挖 Netty 高性能内存管理 我们知道了基于 jemalloc3 的 Netty将整个内存划分为:Tiny、Small、Normal 和 Huge 四类。其中 Tiny 为 0 ~ 512 B 之间的内存块,Small 为 512B ~ 8KB 之间的内存块,Normal 为 8KB ~ 16M 之间的内存块,Huge 则是大于 16M 的,如下图:

而基于 jemalloc4 的 Netty 则将 Tiny 去掉了,保留了 Small、Normal、Huge,内存划分如下:

  • Small:[0-28K]
  • Normal:(28K - 16M]
  • Huge:> 16M

整体架构

下图是基于 jemalloc3 的 Netty 的内存池架构图:

关于这图的详情,大明哥就不再阐述了,主要是用它来跟基于 jemalloc4 的 Netty 内存结构图对比:

从这个图可以看出,Netty 根据内存模型抽象出来了一些组件:PoolArenaPoolChunkListPoolChunkPoolSubpagePoolThreadCache

下面大明哥在这篇文章就这些组件做一个简单的概括,然后每个组件利用一篇文章来详细介绍,最后用内存分配和释放将所有组件进行一个概括总结,让你彻底掌握 Netty 的内存模块!!

PoolArena 数据结构

PoolArena 是外部申请内存的主要入口,Netty 借据 jemalloc 中 Arena 的设计思想,采用固定数量的多个 Arena 进行内存分配,Arena 的默认数量通常为 CPU 核数 * 2,也可以通过参数 io.netty.allocator.numHeapArenas 来指定,计算规则如下:

ini 复制代码
        final int defaultMinNumArena = NettyRuntime.availableProcessors() * 2;
        final int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER;
        DEFAULT_NUM_HEAP_ARENA = Math.max(0,
                SystemPropertyUtil.getInt(
                        "io.netty.allocator.numHeapArenas",
                        (int) Math.min(
                                defaultMinNumArena,
                                runtime.maxMemory() / defaultChunkSize / 2 / 3)));
        DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
                SystemPropertyUtil.getInt(
                        "io.netty.allocator.numDirectArenas",
                        (int) Math.min(
                                defaultMinNumArena,
                                PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));

线程在首次申请分配内存时,会通过 round-robin 的方式轮询 PoolArena 数组,选择一个固定的 PoolArena ,然后在该线程整个生命周期内就只会与该 PoolArena 打交道,所以每个线程都会保存对应的 PoolArena 信息,从而提高访问效率。

每个线程都会有一个 DirectPoolArena 和一个 HeapArena。

下面是 PoolArena 的数据结构:

scala 复制代码
abstract class PoolArena<T> extends SizeClasses implements PoolArenaMetric {
    static final boolean HAS_UNSAFE = PlatformDependent.hasUnsafe();
    
    // 内存规格
    enum SizeClass {
        Small,
        Normal
    }
      
    // 所属分配器
    final PooledByteBufAllocator parent;

    final int numSmallSubpagePools;      
    final int directMemoryCacheAlignment;
    
    private final PoolSubpage<T>[] smallSubpagePools;

    private final PoolChunkList<T> q050;
    private final PoolChunkList<T> q025;
    private final PoolChunkList<T> q000;
    private final PoolChunkList<T> qInit;
    private final PoolChunkList<T> q075;
    private final PoolChunkList<T> q100;
    
    // 省略代码
    
 }

图例如下:

一个 PoolArena 包含了一个 PoolSubpage<T>[] smallSubpagePools 数组和 6 个 PoolChunkList:

  • smallSubpagePools 数组用于存放 Small Subpage类型的内存块。
  • 6 个 PoolChunkList 用于存放使用率不同的 PoolChunk,6 个 PoolChunkList 构成一个双向循环链表。

6 个 PoolChunkList 内存使用情况如下:

每个 PoolChunk 会更加内存使用率的变化在这 6 个 PoolChunkList 来回移动。

PoolChunkList 数据结构

PoolChunkList 管理着多个 PoolChunk,多个使用率相同的 PoolChunk 通过双向链表的方式构建成一个 PoolChunkList,如下:

其定义如下:

java 复制代码
final class PoolChunkList<T> implements PoolChunkListMetric {
    // 所属 PoolArena
    private final PoolArena<T> arena;
    private final PoolChunkList<T> nextList;
    // 最小内存使用率
    private final int minUsage;
    // 最大内存使用率
    private final int maxUsage;
    private final int maxCapacity;
    
    private PoolChunk<T> head;
    private final int freeMinThreshold;
    private final int freeMaxThreshold;

    // 省略代码
}

每个 PoolChunkList 都有两个内存使用率的属性:minUsage 和 maxUsage。当 PoolChunk 进行内存分配时,如果内存使用率超过 maxUsage,则从当前的 PoolChunkList 中移除,并添加到下一个 PoolChunkList 中。同时,随着内存的释放,PoolChunk 的内存使用率就会减少,直到小于 minUsage ,则从当前的 PoolChunkList 中移除,并添加到上一个 PoolChunkList 中。

PoolChunk 数据结构

PoolChunk 是 Netty 真正分配内存的地方,一个 PoolChunk 代表 Netty 内存池中一整块的内存,也是 Netty 内存池向 Java 虚拟机申请和释放的最小单位。 其定义如下:

java 复制代码
final class PoolChunk<T> implements PoolChunkMetric {
    private static final int SIZE_BIT_LENGTH = 15;
    private static final int INUSED_BIT_LENGTH = 1;
    private static final int SUBPAGE_BIT_LENGTH = 1;
    private static final int BITMAP_IDX_BIT_LENGTH = 32;

    static final int IS_SUBPAGE_SHIFT = BITMAP_IDX_BIT_LENGTH;
    static final int IS_USED_SHIFT = SUBPAGE_BIT_LENGTH + IS_SUBPAGE_SHIFT;
    static final int SIZE_SHIFT = INUSED_BIT_LENGTH + IS_USED_SHIFT;
    static final int RUN_OFFSET_SHIFT = SIZE_BIT_LENGTH + SIZE_SHIFT;
    
    // 所属 PoolArena
    final PoolArena<T> arena;
    final Object base;
    // 存储的数据
    final T memory;
    // 是否池化
    final boolean unpooled;
    
    /**
     * 存储的是有用的run中的第一个和最后一个Page的句柄
     */
    private final LongLongHashMap runsAvailMap;

    /**
     * 管理 PoolChunk 的所有的 Run
     */
    private final LongPriorityQueue[] runsAvail;

    /**
     * 管理 PoolChunk 中所有的 PoolSubpage
     */
    private final PoolSubpage<T>[] subpages;

    private final LongCounter pinnedBytes = PlatformDependent.newLongCounter();
    
    // 一个 page 的大小
    private final int pageSize;
    private final int pageShifts;
    private final int chunkSize;

    // 主要是对PooledByteBuf中频繁创建的ByteBuffer进行缓存,以避免由于频繁创建的对象导致频繁的GC
    private final Deque<ByteBuffer> cachedNioBuffers;

    int freeBytes;
    
    // 所属 PoolChunkList
    PoolChunkList<T> parent;
    // 后置节点
    PoolChunk<T> prev;
    // 前置节点
    PoolChunk<T> next;
    
    // 省略代码
  }

PoolChunk 的数据结构比较复杂,其结构图如下:

一个 PoolChunk 由三个部分构成:

  1. Run:一个 Run 由若干个 Page 组成,Page 是 PoolChunk 的分配的最小单位。
  2. Subpage:用于分配 Subpage ,Subpage 的大小为 16B ~ 28K。
  3. free:空闲部分,待分配内存

PoolChunk 还有三个很重要的属性:

  1. runsAvailMap :它存储的是有用的 Run 中的第一个和最后一个 Page 的句柄。它是 runOffset → handle 之间的键值对。
  2. runsAvail:用于管理 PoolChunk 的所有的 Run,它是一个优先队列,每一个队列都管理着相同大小的 Run。
  3. subPages:用于管理 PoolChunk 中所有的 PoolSubpage。

PoolSubpage 数据结构

PoolSubpage 用于分配 Small Subpage,其定义如下:

java 复制代码
final class PoolSubpage<T> implements PoolSubpageMetric {
    // 所属 PoolChunk
    final PoolChunk<T> chunk;
    // 每块内存的大小
    final int elemSize;
    // 页面偏移量
    private final int pageShifts;
    // PoolSubpage 在 PoolChunk 中 memory 的偏移量
    private final int runOffset;
    // Run 的大小
    private final int runSize;
    // 每一小块内存的状态
    private final long[] bitmap;
    
    // 前置节点
    PoolSubpage<T> prev;
    // 后置节点
    PoolSubpage<T> next;

    boolean doNotDestroy;
    // 最多可以存放多少小内存块
    private int maxNumElems;
    private int bitmapLength;
    private int nextAvail;
    private int numAvail;
    
    // 省略代码
 }

其结构如下:

由于篇幅和内容问题,本篇文章就结束了,后面大明哥就这些组件做详细的介绍!!

相关推荐
得不到的更加爱5 分钟前
Java多线程不会?一文解决——
java·开发语言
五敷有你8 分钟前
Go:hello world
开发语言·后端·golang
拔剑纵狂歌26 分钟前
Golang异常处理机制
开发语言·后端·golang·go
缘友一世37 分钟前
Armbian 1panel面板工具箱中FTP服务无法正常启动的解决方法
linux·运维·后端·1panel
ffyyhh99551139 分钟前
java进行音视频的拆分和拼接
java·音视频
weixin_419349791 小时前
flask使用定时任务flask_apscheduler(APScheduler)
后端·python·flask
乐之者v1 小时前
Spring之 IoC、BeanFactory、ApplicationContext
java·后端·spring
DS_Watson1 小时前
字符串和正则表达式踩坑
java·开发语言
Wayfreem1 小时前
Java锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
java·开发语言
我焦虑的编程日记1 小时前
【Java EE】验证码案例
java·java-ee