Netty内存管理机制

1、内存管理介绍

内存管理的目的是合理分配内存,减少内存碎片,及时回收资源,提高内存的使用资源。

可以带着以下问题进行研究:

  • 内存池管理算法是如何实现高效内存分配释放,减少内存碎片?
  • 高负载下内存池不断申请/释放,如何实现弹性伸缩?
  • 内存池作为全局数据,在多线程环境下如何减少锁竞争?

常见的一些算法有slab,buddy,jemalloc等经典算法。

Netty中的内存管理应该是借鉴了FreeBSD内存管理的思想——jemalloc。Netty内存分配过程中总体遵循以下规则:

  • 优先从缓存中分配
  • 如果缓存中没有的话,从内存池看看有没有剩余可用的
  • 如果已申请的没有的话,再真正申请内存
  • 分段管理,每个内存大小范围使用不同的分配策略

2、分配算法

jemalloc依赖多个Arena来分配内存,运行中的应用都有固定数量的多个Arena,默认的数量与处理器的个数相关。系统中多个Arena的原因是由于各个线程进行内存分配时竞争不可避免,Netty允许使用者创建多个分配器来分离锁,提高内存分配效率。

内存分配的调用堆栈看内存分配的主要过程:

  1. new一个ByteBuf,如果是direct则new:PooledUnsafeDirectByteBuf
  2. 从缓存中查找,没有可用的缓存进行下一步
  3. 从内存池中查找可用的内存,查找的方式如上所述(tiny、small、normal)
  4. 如果找不到则重新申请内存,并将申请到的内存放入内存池
  5. 使用申请到的内存初始化ByteBuf

线程首次分配/回收内存时,首先会为其分配一个固定的Arena。线程选择Arena时使用round-robin的方式,也就是顺序轮流选取

各个线程保存各种Arena和缓冲池信息,这样可以减少竞争并提高访问效率。

Arena将内存分为很多Chunk进行管理,Chunk内存保存Page,以页为单位申请
申请内存分配时,会将分配到的规格分为几类:TINY,SMAILL,NORMAL和HUGE,分别对应不同的范围,处理过程也不相同。
![image.png](https://upload-images.jianshu.io/upload_images/6271376-d6a0844f340a893a.png

  1. 内存分配的最小单位为16B。
  2. 小于512B的请求为Tiny,小于8KB(PageSize)的请求为Small,小于等于16MB(Chunk Size)的请求为Normal,大于16MB(Chun kSize)的请求为Huge。
  3. 小于512B的请求以16B为起点每次增加16B;大于等于512B的请求则每次加倍。

为了分配内存块保存连续和减少内存碎片,因此Jemalloc使用Buddy内存分配算法

其实使用二叉树进行管理,树中每个叶子节点表示一个Page,即树高为12。具有相同父节点的叶子节点称为buddy关系,buddy之间自底向上链接为二叉树,直到根节点。

image.png

举个例子:8KB、16KB、8KB为例分析分配过程(每个Page大小8KB):

  1. 8KB:需要一个Page,第11层满足要求,故分配2048节点即Page0;
  2. 16KB:需要两个Page,故需要在第10层进行分配,而1024的子节点2048已分配,从左到右找到满足要求的1025节点,故分配节点1025即Page2和Page3;
  3. 8KB:需要一个Page,第11层满足要求,2048已分配,从左到右找到2049节点即Page1进行分配。

分配结束后,已分配连续的Page0-Page3,这样的连续内存块,大大减少内部碎片并提高内存使用率

ByteBuf分类

Netty使用ByteBuf对象作为数据容器,进行I/O读写操作,Netty的内存管理也是围绕着ByteBuf对象高效地分配和释放

当讨论ByteBuf对象管理,主要从以下方面进行分类:

Pooled 和 Unpooled

  • 池化内存分配时基于预分配的一整块大内存,取其中的部分封装成ByteBuf提供使用,用完后回收到内存池中。
  • 非池化内存每次分配时直接调用系统 API 向操作系统申请ByteBuf需要的同样大小内存,用完后通过系统调用进行释放Pooled。

tips: Netty4默认使用Pooled的方式,可通过参数-Dio.netty.allocator.type=unpooled或pooled进行设置

Heap 和 Direct

  • Heap,指ByteBuf关联的内存JVM堆内分配,分配的内存受GC 管理
  • Direct,指ByteBuf关联的内存在JVM堆外分配,分配的内存不受GC管理,需要通过系统调用实现申请和释放,底层基于Java NIO的DirectByteBuffer对象

申请/释放内存

当申请分配内存,会首先将请求分配的内存大小归一化(向上取值),通过PoolArena#normalizeCapacity()方法,取最近的2的幂的值​,例如8000byte归一化为8192byte( chunkSize/2^11 ),8193byte归一化为16384byte(chunkSize/2^10)

处理内存申请的算法在PoolChunk#allocateRun方法中,当分配已归一化处理后大小为chunkSize/2^d的内存,即需要在depth = d的层级中找到第一块空闲内存,算法从根节点开始遍历 (根节点depth = 0, id = 1),具体步骤如下:

  1. 步骤1 判断是否当前节点值memoryMap[id] > d,如果是,则无法从该chunk分配内存,查找结束。
  2. 步骤2 判断是否节点值memoryMap[id] == d,且depth_of_id == h
    。如果是,当前节点是depth = d的空闲内存,查找结束,更新当前节点值为memoryMap[id] = max_order + 1,代表节点已使用,并遍历当前节点的所有祖先节点,更新节点值为各自的左右子节点值的最小值;如果否,执行步骤3
  3. 步骤3 判断是否当前节点值memoryMap[id] <= d,且depth_of_id < h。如果是,则空闲节点在当前节点的子节点中,则先判断左子节点memoryMap[2 * id] <=d(判断左子节点是否可分配),如果成立,则当前节点更新为左子节点,否则更新为右子节点,然后重复步骤2。

释放内存

释放内存时,根据申请内存返回的id,将 memoryMap[id]更新为depth_of_id,同时设置id节点的祖先节点值为各自左右节点的最小值

巨型对象内存管理

对于申请分配大小超过chunkSize的巨型对象(huge),Netty采用的是非池化管理策略,在每次请求分配内存时单独创建特殊的非池化PoolChunk对象进行管理,内部memoryMap为null,当对象内存释放时整个Chunk内存释放,相应内存申请逻辑在PoolArena#allocateHuge()方法中,释放逻辑在PoolArena#destroyChunk()方法中。

小对象内存管理

这些小对象直接分配一个page会造成浪费,在page中进行平衡树的标记又额外消耗更多空间,因此Netty的实现是:先PoolChunk中申请空闲page,同一个page分为相同大小规格的小内存进行存储。

image.png

弹性伸缩

PoolChunk管理

为了解决单个PoolChunk容量有限的问题,Netty将多个PoolChunk组成链表一起管理,然后用PoolChunkList对象持有链表的head

将所有PoolChunk组成一个链表的话,进行遍历查找管理效率较低,因此Netty设计了PoolArena对象(arena中文是舞台、场所),实现对多个PoolChunkList、PoolSubpage的管理,线程安全控制、对外提供内存分配、释放的服务。

PoolSubpage管理

PoolArena内部持有2个PoolSubpage数组,分别存储tiny和small规格类型的PoolSubpage

并发设计

为了减少线程间的竞争,Netty会提前创建多个PoolArena(默认生成数量 = 2 * CPU核心数),当线程首次请求池化内存分配,会找被最少线程持有的PoolArena,并保存线程局部变量PoolThreadCache中,实现线程与PoolArena的关联绑定(PoolThreadLocalCache#initialValue()方法)。

Netty设计了ThreadLocal的更高性能替代类:FastThreadLocal,需要配套继承Thread的类FastThreadLocalThread一起使用,基本原理是将原来Thead的基于ThreadLocalMap存储局部变量,扩展为能更快速访问的数组进行存储(Object[] indexedVariables),每个FastThreadLocal内部维护了一个全局原子自增的int类型的数组index。

Netty还设计了缓存机制提升并发性能:当请求对象内存释放,PoolArena并没有马上释放,而是先尝试将该内存关联的PoolChunk和chunk中的偏移位置(handler变量)等信息存入PoolThreadLocalCache中的固定大小缓存队列中(如果缓存队列满了则马上释放内存);当请求内存分配,PoolArena会优先访问PoolThreadLocalCache的缓存队列中是否有缓存内存可用,如果有,则直接分配,提高分配效率。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容