当前位置: 首页 > news >正文

Netty PoolChunk依赖的自定义数据结构:IntPriorityQueue和LongLongHashMap

IntPriorityQueue

IntPriorityQueue 是一个基于​​最小堆(Min-Heap)​​ 的优先级队列实现,用于高效管理整数元素。堆结构使用数组存储(索引从1开始),核心操作(插入、删除)的时间复杂度为 O(log n)。以下是对其实现的逐段解析:


成员变量​

private int[] array = new int[9]; // 堆存储数组,索引0未使用
private int size; // 当前元素数量public static final int NO_VALUE = -1;
  • ​数组设计​​:初始大小为9(包含索引0占位),实际存储从索引1开始,符合二叉堆的数组表示惯例。
  • ​索引规则​​:节点 k 的父节点为 k/2,左子节点为 2k,右子节点为 2k+1

offer(int handle) - 插入元素​

public void offer(int handle) {if (handle == NO_VALUE) throw new IllegalArgumentException();size++;// 动态扩容:数组满时按 2n - 1 扩容if (size == array.length) {array = Arrays.copyOf(array, 1 + (array.length - 1) * 2);}array[size] = handle; // 新元素置于末尾lift(size); // 上浮调整堆结构
}
  • ​扩容策略​​:当数组满时,容量扩展为 2n + 1(如初始9 → 17 → 33)。
  • ​上浮调整​​:新元素从末尾开始向上交换,直到满足堆序(父节点 ≤ 子节点)。

remove(int value) - 删除指定元素​

public void remove(int value) {for (int i = 1; i <= size; i++) {if (array[i] == value) {array[i] = array[size]; // 末尾元素覆盖删除位size--;lift(i); // 尝试上浮sink(i); // 尝试下沉return;}}
}
  • ​查找删除​​:遍历数组找到目标元素(O(n),非堆标准操作,特定场景使用)。
  • ​覆盖调整​​:用末尾元素替换被删元素,先后执行上浮和下沉确保堆序正确。

在堆数据结构中,​​上浮(lift)和下沉(sink)操作的调用顺序对堆的最终状态没有影响​​,但需要理解为什么在 remove() 方法中需要连续调用这两个操作,以及它们如何协同工作保证堆性质。

remove() 方法用末尾元素替换被删除元素时:

此时新元素可能处于以下三种状态之一:

  1. ​需要上浮​​:新元素比父节点小(最小堆)
  2. ​需要下沉​​:新元素比子节点大
  3. ​已在正确位置​​:既不需要上浮也不需要下沉

​关键点​​:

  • ​上浮操作只会让元素向上移动​​,移动后该元素的新位置必然满足:
    • 新父节点 ≤ 当前节点(堆序成立)
    • 但​​子节点可能更大​​(需要检查下沉)
  • ​下沉操作只会让元素向下移动​​,移动后:
    • 当前节点 ≤ 新子节点(堆序成立)
    • 但​​父节点可能更大​​(需要检查上浮)

通过​​连续调用上浮和下沉​​,实际上覆盖了所有可能:

  1. 若需要上浮 → lift() 将其移到正确位置 → sink() 发现无需操作
  2. 若需要下沉 → lift() 不操作 → sink() 将其移到正确位置
  3. 若已在正确位置 → 两个操作都不触发

​堆调整操作是​​幂等​​的。连续调用 lift+sinksink+lift 最终都会收敛到元素的理论正确位置。

虽然顺序可互换,但 Netty 选择先 lift()sink() 是出于​​性能优化​​:

  1. ​概率优势​​:
    当替换元素来自堆末尾时(通常是较大值),需要下沉的概率 > 需要上浮的概率。
    → 先调用 lift() 可快速跳过(大概率不触发)
    → 减少不必要的比较操作

  2. ​局部性原理​​:
    上浮操作只需比较父节点(1次比较),下沉操作需比较两个子节点(2次比较)。
    → 优先执行更轻量的操作

上浮和下沉的连续调用构成了堆调整的​​双保险机制​​,无论顺序如何都能保证堆性质,但特定顺序可能在统计上带来轻微性能优势。在工程实践中,这种差异通常可以忽略。


poll() - 移除堆顶元素​

public int poll() {if (size == 0) return NO_VALUE;int val = array[1]; // 保存堆顶(最小值)array[1] = array[size]; // 末尾元素移至堆顶array[size] = 0; // 清空末尾size--;sink(1); // 下沉调整堆顶return val;
}
  • ​堆顶移除​​:取堆顶后,将末尾元素移至堆顶,执行下沉操作维持堆序。
  • ​下沉逻辑​​:父节点与较小子节点交换,直到满足堆序或到达叶节点。

peek()isEmpty()

public int peek() {return (size == 0) ? NO_VALUE : array[1]; // 直接返回堆顶
}
public boolean isEmpty() {return size == 0;
}
  • ​堆顶访问​​:O(1) 直接返回索引1的值。
  • ​空队列判断​​:检查 size 是否为0。

​核心辅助方法 ​​lift / sink

    private void lift(int index) {int parentIndex;while (index > 1 && subord(parentIndex = index >> 1, index)) {swap(index, parentIndex);index = parentIndex;}}private void sink(int index) {int child;while ((child = index << 1) <= size) {// 选择较小子节点if (child < size && subord(child, child + 1)) child++;if (!subord(index, child)) break;swap(index, child); // 与子节点交换index = child;}
}private boolean subord(int a, int b) {return array[a] > array[b]; // 检查是否违反堆序(父 > 子)
}private void swap(int a, int b) {int temp = array[a];array[a] = array[b];array[b] = temp;
}
  • ​上浮(lift)​​:从叶节点向上交换,直到父节点 ≤ 当前节点。
  • ​下沉(sink)​​:从根节点向下交换,每次选择​​较小子节点​​确保最小堆性质。
  • ​堆序检查​​:subord(a, b) 判断 array[a] > array[b],用于触发交换。

为什么自己实现一个堆

Netty 选择自己实现 IntPriorityQueue(最小堆)而非使用 Java 标准库的 PriorityQueue,主要基于以下关键原因,这些原因与 Netty 的高性能、零拷贝和内存管理目标紧密相关:


极致性能优化​

  • ​避免装箱开销​​:
    Java 的 PriorityQueue<Integer> 需要将 int 装箱为 Integer 对象,导致:

    • 额外内存开销(每个对象增加 12-16 字节头部)
    • GC 压力增大(频繁创建/销毁对象)
    • 缓存局部性差(对象分散在堆内存)
      ​Netty 方案​​:直接使用 int[] 存储数据,内存紧凑,CPU 缓存命中率高。
  • ​位运算替代乘除​​:
    堆操作中大量使用 index >> 1(除2)和 index << 1(乘2)等位运算,比标准库的乘除指令快数倍。

  • ​避免 JDK 实现约束​​:
    JDK 的 PriorityQueue 依赖 Comparator 接口,引入虚方法调用开销,而 Netty 直接内联比较逻辑:
    private boolean subord(int a, int b) {return array[a] > array[b]; // 直接比较数组值
    }
  • ​规避锁开销​​:
    标准库的 PriorityQueue 是非线程安全的,但仍有冗余的并发检查。Netty 明确设计为​​单线程使用​​(内存分配在线程本地进行),彻底去除锁和 CAS 操作。

 高频操作优化

public int poll() {if (size == 0) {return NO_VALUE;  // 直接返回特殊值,无需异常}// ... 堆操作
}

设计优势

  • 无异常设计:返回特殊值而非抛异常,提高性能
  • 直接数组访问:避免集合框架的额外开销


​特定内存管理需求​

  • ​与 PoolChunk 协同设计​​:
    该堆是 PoolChunk 内存分配的核心组件,用于管理​​内存块句柄(handle)​​。句柄是整数编码,包含内存块大小、偏移量等信息。

    • 需要高效获取​​最小可用内存块​​(堆顶元素)
    • 支持动态插入(分配后分裂)和删除(释放后合并)
  • ​特殊值处理​​:
    定义了 NO_VALUE = -1 作为空值标记,避免使用 null(标准库需包装对象)。


​与 LongLongHashMap 协同​

  • ​高效元数据管理​​:
    LongLongHashMap 用于存储内存块元数据(Key: 内存地址, Value: 状态)。两者协同工作:
    1. IntPriorityQueue 快速获取最小可用内存块句柄
    2. 通过句柄从 LongLongHashMap 中查询内存地址和状态
  • ​统一优化目标​​:
    两者均使用基本类型数组,避免对象开销,构成 Netty 内存池的高效底层基础设施。

为什么不用第三方库?

  • ​零依赖原则​​:Netty 核心模块坚持不依赖外部库,确保:
    • 兼容性:无版本冲突风险
    • 可调试性:直接控制关键路径代码
    • 轻量化:减少最终部署体积

总结

Netty 自实现 IntPriorityQueue 的核心目的是:​​在内存池这一关键路径上,通过消除对象开销、优化 CPU 缓存利用和简化操作逻辑,实现亚微秒级的内存分配性能​​。这与其"在高负载下最小化延迟和 GC 压力"的设计哲学一致。这种深度优化在通用库中无法实现,却是 Netty 成为高性能网络框架基石的关键。


设计总结

  • ​最小堆特性​​:保证堆顶始终为最小值,适合需要高效取最小元素的场景(如内存分配)。
  • ​动态扩容​​:数组按需扩展,避免频繁内存分配。
  • ​非标准操作​​:remove(int) 遍历查找效率较低(O(n)),但满足特定需求(如Netty内存池管理)。
  • ​索引优化​​:位运算(>>1<<1)替代乘除,提升计算效率。

此实现是Netty内存池(PoolChunk)的核心组件,用于高效管理内存块优先级。

LongLongHashMap 

LongLongHashMap 是一个为 long 类型键值对优化的哈希表实现,专为 Netty 内存池 (PoolChunk) 设计。它采用​​开放寻址法​​处理冲突,结合​​双倍哈希间隔探测​​策略,针对长整型键进行了特殊优化。以下是逐段解析:


1. ​​成员变量​

private static final int MASK_TEMPLATE = ~1; // 掩码模板(保证偶数)
private int mask;          // 哈希掩码(数组长度-1,且为偶数)
private long[] array;      // 键值对存储数组(键在偶数索引,值在奇数索引)
private int maxProbe;      // 最大探测次数
private long zeroVal;      // 键为0的特殊值
private final long emptyVal; // 空值标记(构造时传入)
  • ​键值存储​​:array 数组中键值对相邻存储(键在 [0,2,4...],值在 [1,3,5...])。
  • ​特殊键处理​​:键 0 由独立变量 zeroVal 存储,避免哈希冲突。
  • ​掩码设计​​:mask 保证为偶数(MASK_TEMPLATE = ~1 清除最低位),确保索引计算后仍是偶数。

2. ​​构造函数​

LongLongHashMap(long emptyVal) {this.emptyVal = emptyVal;zeroVal = emptyVal; // 初始化键0的值array = new long[32]; // 初始容量32(2的幂)mask = array.length - 1;computeMaskAndProbe(); // 计算掩码和探测次数
}
  • ​初始容量​​:32(必须是2的幂,方便位运算替代取模)。
  • ​初始化​​:zeroVal 设为 emptyVal,表示键0未存储。

put(long key, long value)

public long put(long key, long value) {if (key == 0) {long prev = zeroVal;zeroVal = value;return prev; // 直接更新键0的值}for (;;) {int index = index(key); // 计算初始索引for (int i = 0; i < maxProbe; i++) {long existing = array[index];if (existing == key || existing == 0) {long prev = (existing == 0) ? emptyVal : array[index+1];array[index] = key; // 写入键array[index+1] = value; // 写入值// 清理可能重复的键(相同键在后续位置)for (; i < maxProbe; i++) {index = (index + 2) & mask; // 双倍间隔探测if (array[index] == key) {array[index] = 0; // 删除重复键prev = array[index+1]; // 保存旧值break;}}return prev;}index = (index + 2) & mask; // 双倍间隔探测}expand(); // 探测失败后扩容}
}
  • ​键0处理​​:直接更新 zeroVal
  • ​哈希探测​​:
    • 使用 index(key) 计算初始索引(偶数)。
    • ​双倍间隔探测​​:每次跳2个位置(index = (index + 2) & mask),避免聚类。
  • ​冲突解决​​:
    • 找到空槽(0)或相同键时插入/更新。
    • 插入后检查后续位置,删除可能的重复键。【因为之前删除后产生空槽,如果空槽插入,因为哈希冲突是间隔探测,之后可能会有原来的key
  • ​扩容触发​​:当探测次数超过 maxProbe 时扩容。

get(long key)

public long get(long key) {if (key == 0) return zeroVal;int index = index(key);for (int i = 0; i < maxProbe; i++) {if (array[index] == key) {return array[index+1]; // 返回相邻值}index = (index + 2) & mask;}return emptyVal; // 未找到
}
  • ​键0处理​​:直接返回 zeroVal
  • ​探测逻辑​​:沿双倍间隔路径查找,命中返回相邻值。
  • 限制maxProbe,对数次查找。
  • 限制查找maxProbe步 是不是可能实际有,但是查不到?实际上put的时候检查了,如果maxProbe没有散列到,会扩容的。

remove(long key)

public void remove(long key) {if (key == 0) {zeroVal = emptyVal;return;}int index = index(key);for (int i = 0; i < maxProbe; i++) {if (array[index] == key) {array[index] = 0; // 置0标记删除(惰性删除)return;}index = (index + 2) & mask;}
}
  • ​键0处理​​:重置 zeroValemptyVal
  • ​惰性删除​​:仅将键位置置 0,值保留(后续插入覆盖)。
  • 这里删除第一个,对应put只是替换第一个,删除之后的,因为之后的重复是旧的

辅助方法

 ​​哈希函数 index(long key)

private int index(long key) {key ^= key >>> 33;           // 三步混合哈希(类似MurmurHash)key *= 0xff51afd7ed558ccdL; key ^= key >>> 33;key *= 0xc4ceb9fe1a85ec53L;key ^= key >>> 33;return (int) key & mask;     // 位运算替代取模
}
  • ​高效哈希​​:三次移位和乘法混合,确保分布均匀。
  • ​位运算优化​​:& mask 替代取模(要求 array.length 是2的幂)。

扩容方法 expand()

private void expand() {long[] prev = array;array = new long[prev.length * 2]; // 双倍扩容computeMaskAndProbe(); // 更新掩码和探测次数for (int i = 0; i < prev.length; i += 2) {long key = prev[i];if (key != 0) {put(key, prev[i+1]); // 重新插入旧数据}}
}
  • ​双倍扩容​​:数组扩大一倍(保持2的幂)。
  • ​重哈希​​:遍历旧数组,非空键值对重新插入新数组。

掩码与探测计算 computeMaskAndProbe()

private void computeMaskAndProbe() {int length = array.length;mask = (length - 1) & MASK_TEMPLATE; // 保证偶数maxProbe = (int) Math.log(length);    // 探测次数 = log(容量)
}
  • ​掩码更新​​:mask = (length-1) & ~1 确保偶数索引。
  • ​探测次数​​:maxProbe = log2(length),容量越大允许探测次数越多。

设计总结

  1. ​开放寻址优化​​:

    • ​双倍间隔探测​​:每次跳2个位置(index = (index+2) & mask),减少聚类。
    • ​惰性删除​​:仅标记键为 0,避免数据移动。
  2. ​长整型特化​​:

    • ​高效哈希​​:三步混合哈希确保键分布均匀。
    • ​键0优化​​:独立变量存储,避免哈希冲突。
  3. ​动态扩容​​:

    • ​双倍扩容​​:容量不足时扩大一倍(O(n))。
    • ​重哈希​​:旧数据重新插入新表(利用改进的哈希分布)。
  4. ​性能平衡​​:

    • ​探测次数​​:maxProbe = log(length) 平衡查找效率与空间利用率。
    • ​位运算优化​​:& mask 替代取模,索引计算高效。

该实现针对 Netty 内存池中 long 类型的内存地址管理优化,在保证高效查找的同时,最小化内存开销。

PoolChunk中的应用分析

LongLongHashMap的应用​

  • ​作用场景​​:
    PoolChunk中,LongLongHashMap(命名为runsAvailMap)用于​​跟踪可用内存块(runs)的位置和元数据​​。键是内存块的起始页偏移(runOffset),值是一个编码的句柄(handle),包含:

    • 起始偏移(runOffset
    • 页数(pages
    • 使用状态(isUsed
    • 子页标识(isSubpage
    • 位图索引(bitmapIdx
  • ​关键操作​​:

    • ​插入​​:分配内存块时,记录新的可用块(insertAvailRun)。
    • ​查找​​:释放内存时,通过偏移快速定位相邻块以进行合并(collapseRuns)。
    • ​删除​​:内存块分配或合并后移除旧记录(removeAvailRun)。
  • ​自实现原因​​:

    1. ​性能优化​​:
      • 内存分配/释放是高频操作,需避免java.util.HashMap的自动装箱(Longlong)开销。
      • 开放寻址法减少内存碎片,比链式结构更紧凑。
    2. ​特定需求​​:
      • 键为long(偏移量),值也为long(编码句柄),需原生支持长整型存储。
      • 合并操作需快速访问相邻偏移(runOffset±1),开放寻址法局部性更好。
    3. ​轻量级​​:
      • 无需红黑树等复杂结构,哈希冲突通过线性探测解决,简化实现。

 ​​IntPriorityQueue的应用​

  • ​作用场景​​:
    IntPriorityQueue(作为runsAvail数组的元素)用于​​按偏移排序可用内存块​​。每个队列存储相同大小的内存块句柄(高32位),确保:

    • 分配时优先选择最小偏移的块(减少碎片)。
    • 高效查找最佳匹配块(runFirstBestFit)。
  • ​关键操作​​:

    • ​插入​​:可用块加入队列(insertAvailRun)。
    • ​删除​​:分配时移除块(removeAvailRun)。
    • ​堆调整​​:合并块后重新排序(sink/lift)。
  • ​自实现原因​​:

    1. ​性能优化​​:
      • 避免java.util.PriorityQueue的装箱开销(Integerint)。
      • 直接操作int[]数组,缓存局部性更优。
    2. ​内存效率​​:
      • 元素为基本类型int,比对象指针更节省内存(尤其在大量小块场景)。
    3. 其它优势见 IntPriorityQueue 一节

​总结

自实现数据结构的必要性​

​数据结构​​标准库替代方案​​自实现优势​
LongLongHashMapHashMap<Long, Long>避免装箱;开放寻址法缓存友好;长整型键值原生支持;快速相邻块查找。
IntPriorityQueuePriorityQueue<Integer>避免装箱;数组存储+堆操作更紧凑;支持高效随机删除;基本类型操作无额外开销。

​核心目的​​:Netty的内存池作为底层基础设施,需极致优化性能(纳秒级操作)和内存效率(减少GC压力)。自实现数据结构针对​​高频、小规模、基本类型操作​​的场景,消除标准库在​​装箱、内存布局、功能冗余​​上的开销,满足高并发内存分配的严苛要求。

相关文章:

  • 计算机网络:(五)信道复用技术,数字传输系统,宽带接入技术
  • C++中所有数据类型
  • CppCon 2017 学习:folly::Function A Non-copyable Alternative to std::function
  • 目标检测之YOLOV11自定义数据使用OBB训练与验证
  • Apache ECharts-01.介绍
  • Arduino Nano 33 BLE Sense Rev 2开发板使用指南之【外设开发】
  • 响应式数据框架性能深度分析报告(@type-dom/signals)
  • EchoEar(喵伴):乐鑫发布与火山引擎扣子联名 AI 智能体开发板
  • 20250619在Ubuntu20.04.6下编译Rockchip瑞芯微原厂的RK3576的Buildroot系统
  • SSH服务与rsync服务配置实战
  • 内网运行控制四百来个海康威视硬件物联网定员管控软件(华为平板电脑版)
  • 3.5 map_saver地图的保存与加载
  • STM32 定时器讲解
  • 【Bug:docker】--Docker同时部署Dify和RAGFlow出现错误
  • 《汇编语言:基于X86处理器》第2章 复习题
  • 【AI论文】SWE-Factory:您的自动化工厂,提供问题解决培训数据和评估基准
  • Java 正则表达式高级用法
  • 全局数据的处理
  • 设计模式 - 原型模式
  • LTC3130EMSE#TRPBF ADI电子元器件深度解析 物联网/工业传感器首选!
  • 地方网站建设/最新国际新闻大事件
  • 陕西建设网综合便民服务中心网站/windows10优化大师
  • 上海企业网站建设推荐/不受限制的搜索浏览器
  • 北京网站开发公司哪家好/北京seo代理计费
  • 电子商务网站建设费用/竞价开户
  • baubauhaus设计网站/关键词查询工具包括哪些