Netty高低版本内存管理对比&源码分析

95 阅读4分钟

由于工作时仍然使用低版本的Netty;所以当前还是详细介绍低版本的Netty的内存管理;作为扩展,仍会简单介绍下最新版本的Netty的内存管理;
整体来看,Netty的内存管理是jmelloc的java实现版本;4.1.38.Final版本是Jemalloc3版本的实现,后续版本实现了Jemalloc4,加强了对堆外内存的管理,减少内存碎片的产生;

4.1.38 Final

内存管理作用

  1. 减少由于频繁向OS申请内存,导致的上下文切换次数;
  2. 高效分配内存,提升整体运行效率;
  3. 管理小内存,减少内存碎片的产生
  4. 缓存小内存,避免重复创建对象
  5. 减少垃圾回收压力

使用方式

通过PooledByteBufAllocator开放统一的API接口,开放给用户使用;

PooledByteBufAllocator allocator = new PooledByteBufAllocator();
ByteBuf byteBuf = allocator.directBuffer(512);

整体流程图

PoolThreadCache & PoolArena

  1. PoolThreadCache是线程级别的缓存,先通过缓存MemoryRegionCache进行判断是否已经缓存
  2. 若MemoryRegionCache中没有缓存,则需要从PoolArena中申请;
  3. 注意PoolArena的申请是存在多线程竞争的;
  4. 为了减少资源竞争,默认实现中,PoolThreadCache 和PoolArena的个数是相同且一一对应的;都是CPU核心数*2;

image.png

PoolThreadCache 思维导图

image.png

PoolArena的关键信息

MemoryRegionCache

主要的信息有

  1. 一个Queue<Entry> 用于保存当前规格的所有对象;
  2. SizeClass : 尺寸大小;
  3. 用于表示一个MemoryRegionCache对象保存多大SizeClass的内存对象,一共有Queue.size个缓存对象;
PoolChunkList
  1. PoolChunkList是一个双向链表的数据结构
  2. 每个PoolChunk 也都是一个双向链表,所以只要堆外内存不被撑爆,理论上可以一直递增
  3. PoolChunkList中的PoolChunk都是动态维护,动态变化的;比如:原本分配内存只占用40%的PoolChunk,他是属于q025的PoolChunkList,但是一旦他被分配一块内存后,分配内存占用达到60%,此时就需要将这个PoolChunk移动到q050的PoolChunkList中;

image.png

PoolChunk中内存管理二叉树

image.png

如何进行内存管理?

引入depthMap和MemoryMap; 两者都是用于保存当前二叉树中的所在层的编号(0-11)

  1. depthMap : 用于记录当前节点的高度,一旦初始化,就不在变化;主要用于索引
  2. memoryMap :用于记录当前节点内存使用情况,时刻变化;

附: 内存申请使用的流程;
关键点:

  1. memoryMap所构成的二叉树中的每个节点都代表一块内存;
  2. 每次申请都是拿memoryMap的一个节点的内存;而不是拿多个节点;
  3. 根节点所代表的内存值是16M, 叶子结点代表的内存值是8k
  4. memoryMap构成的满二叉树中,若分配过,父节点是两个节点的最大值;(因为代表可分配内存的最大值) image.png

PoolChunk关键信息

image.png 可以参看:

整体类图

image.png

源码实现

内存管理的代码太多了;找一些关键、精妙的地方解释一下;

这里另写几篇文章介绍吧;(后续会陆续补充)

  1. Recycler组件?
  2. PoolArena申请内存的细节分享
  3. MemoryRegionCache里面的小技巧
  4. PoolSubpage的源码解析&思考

4.1.112 Final

笔者对这个版本了解较少,秉着跟上时代迭代步伐的想法;在研读过程中,重点参看了大佬写的谈一谈 Netty 的内存管理 —— 且看 Netty 如何实现 Java 版的 Jemalloc  做了一些自己的心得体会;有很多细节的地方还需要进一步强化;

整体思维导图

image.png

4.1.112版本 VS 4.1.38版本的变化点

  1. 去掉了Tiny这种内存规格;减少内存碎片化
  2. 新增当前Small的规格范围,由之前的[512B,4k] -> [16B-28KB]
  3. 内部是实现方式也发生变化,之前是通过memoryMap/treeMap 进行标记,寻址;现在变成了runAvail[]数组的方式进行寻址;内存管理效率也有所提升
  4. 为了节省内存,线程级别的缓存默认不开启,同时为了减少内存浪费,ChunkSize的大小默认从16M --> 4M

学习&思考

  1. 从4.1.38--> 4.1.112的版本演化过程中,可以看到针对过度设计的优化(去掉Tiny、默认关闭线程级缓存、减少PoolChunk的大小)等,所以我们在设计的时候,除了很常见的SOLID原则外,可以多考虑考虑KISS、YAGNI等
  2. 空间换时间,利用一次申请大内存(16M),缓存形成一个内存管理单元,每次从单元中拿取指定大小内存;
  3. 规则化,正则化;这其实日常我们说的,没有规矩不成方圆;如果没有这些规则,那管理起来会非常复杂;大家一旦形成默认共识并遵守,事情做起来就会极其通畅;

参看网址

谈一谈 Netty 的内存管理 —— 且看 Netty 如何实现 Java 版的 Jemalloc 
Netty Release Notes