死磕 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;
    
    // 省略代码
 }

其结构如下:

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

相关推荐
似璟如你7 分钟前
Java 开发者的 Go 语法基础:从 0 开始快速上手 Go
java·开发语言·后端·golang·go·编程语言
zzz_236822 分钟前
TencentDB-Agent-Memory 深度解析:让多个 Agent 共享项目经验的记忆中枢
java·开发语言·jvm·人工智能·agent·memory·tencent db
vx-程序开发1 小时前
django医院预约挂号系统---附源码23353
java·javascript·spring boot·python·eclipse·django·php
金斗潼关1 小时前
ysoserial的使用
java
ruleslol1 小时前
volatile 关键字
java
Codelinghu1 小时前
AI 写代码越强,程序员越不能只懂代码
后端
卡卡敲码1 小时前
Skills 撞车了,Agent 怎么选
后端
羑悻1 小时前
周一早上三件事砸过来,我以为要延期,结果 AI 替我扛了一半!
后端
文艺理科生1 小时前
3 年,8 种方案,1 次重构:LangChain 记忆方案如何从混乱走向清晰
前端·后端·架构
lilian2332 小时前
Harmony os 技术实战|拼豆制图10:把取消、解析失败和保存失败写成可恢复状态机
java·javascript·华为·harmonyos