> 文章列表 > Netty内存管理--ChunkPage

Netty内存管理--ChunkPage

Netty内存管理--ChunkPage

写在前面

必要性

网络通信中,接收端通常需要多层解码,最终在应用层解码后才能得到业务层可处理的message。Netty需要缓存接收到的网络数据和待发送的网络数据,缓存发送/解码完成后就可以释放。其中对缓存的获取和释放频次高;
通过系统调用获取内存本身比较耗时;高频的缓存释放对Java GC产生压力;随之带来的是GC压力。而GC过程中可能会导致STW(Stop The Wold)现象,进而影响响应延迟。
因此需要对缓存的分配和释放做必要的管理。提高内存利用率。

难点

Netty中链接是并发的,如何做到线程安全?
如何尽可能降低对GC的影响?
如何平衡内存分配效率和内存利用率?

从本篇开始总结下Netty中内存管理的实现。

一、内存规格化

名称 尺寸 分配位置 分配单位
Normal [8K, 16M] Chunk Page
Tiny [496B, 4096B] Page SubPage
Small (0, 496B) Page SubPage
Huge 16M+ 不复用 不复用

最终形成两级颗粒度, 从Chunk中划分Page, 从Page中划分SubPage。本篇咱们先聚焦在Page级别的分配。
从4.1.52.Final开始, Netty删除了TinyPagePool, 以减少内部碎片。官方提供的原因是由于jmelloc 4.x的更新, Netty随后也进行了必要的更新。参见Netty Release Note

二、Chunk中Page管理

Chunk默认向OS申请16M的连续内存, 按照伙伴算法的划分方式, 直到每个伙伴的空间大小为一个page。整个划分过程结束后形成一颗满二叉树, 后续的空间管理基于该二叉树完成;

1.二叉树表示

Netty使用两个数组来memoryMap和depthMap表示该二叉树。memoryMap和depthMap, 下标都表示内存块编号,数值标识内存块层级。至于空间大小, 直接基于节点层级计算即可。

数值内容 含义
memoryMap[id] = depthMap[id] 该节点没有被分配, 初始化完成时为该状态。
depthMap[id] < memoryMap[id] < 最大高度(12) 至少有一个子节点被分配,但尚未完全被分配,不能再分配该高度对应的内存,只能根据实际情况在更低层次分配内存
memoryMap[id] = 最大高度(12) 该节点及其子节点已被完全分配,没有剩余空间

2 Page分配过程

假设现在有一个新初始化的chunk, 从中分配150K内存;

  1. 将150K的空间规格化, 结果为256K, 应该在第7层(每个单元的空间大小为256K);
  2. 遍历memoryMap, 遍历的索引分别为(1,2,4,8,16,32,64), 如下图黑框中内容为实际为遍历的节点(注意在Netty的MemoryMap和depthMap中有效索引从1开始,0是被浪费掉的);
  1. 结合前面的条件, memoryMap[64]这个节点可以正常分配, 将memoryMap[64]设置为12, 并关联更新父节点为左右节点中层级较小的depthMap。
memoryMap[index] 容量单位 原值 更新后的值
64 256K 7 12
32 512K 6 7
16 1M 5 6
8 2M 4 5
4 4M 3 4
2 8M 2 3
1 16M 1 2
  1. 虽然申请150K, 但最终实际获得了256K也就是32个Page的内存。到这里想必你已经对伙伴分配算法的分配过程有了一个初步的理解, 目标是要做到对不同颗粒度的连续内存分配。

3 Page回收过程

作为分配的逆过程, 回收过程就相对清晰了。比如前面150K内存回收, 将起始位置offset+32个page的内容清理掉, 然后memoryMap[64]=depthMap[64], 并关联设置父节点的值为左右子节点的memoryMap中较小的那个, 回收结束。

总结

本篇Netty中内存内存规格化,对应的管理颗粒Chuank和Page,以及Chunk中Page的分配与回收过程。Netty具体的实现中会做额外的一些优化,后续会一并总结。