中对象如何选择Region
在ZGC中,中型对象(256KB ~ 4MB)首次分配时,Region的选择策略基于非独占共享机制,通过动态管理32MB中型Region的空间实现高效分配。以下是完整流程和核心技术细节:
🔍 一、Region选择流程
1. 检查空闲中型Region链表
- ZGC维护全局的空闲中型Region链表(
free_medium_regions)。 - 分配器优先从链表中取出一个空闲Region:
若链表为空,则向操作系统申请新的32MB内存块作为新Region。
if (free_medium_regions != null) {
region = free_medium_regions.pop(); // 直接复用空闲Region
}
2. NUMA本地化优化
- 若启用
XX:+UseNUMA(默认开启),优先选择当前CPU节点所属的中型Region,减少跨节点内存访问延迟(降幅可达30%)。
3. Region内部空间分配
- 非独占分配:每个中型Region可容纳多个中型对象(例如:32MB Region可存放8个4MB对象或128个256KB对象)。
- 子块分割:
-
Region被划分为4KB子块,通过
span_map数组记录每个子块的分配状态(起始ID、长度、空闲标记)。 -
分配对象时,遍历空闲子块链表(
free_sub_list),找到≥对象大小的连续空闲子块区间:for (span in free_sub_list) { if (span.size >= obj_size) { split_span(span, obj_size); // 分割空闲区间 update_span_map(); // 更新元数据 break; } }
-
⚙️ 二、关键技术机制
1. 并发控制与锁分割
- 分区锁:中型Region分配使用16个分区锁(默认),通过线程ID哈希选择锁槽,减少竞争:
实测可降低94%的锁冲突率。
lock_id = thread_id % 16;
lock(medium_region_locks[lock_id]);
2. 空闲合并优化(O(1)复杂度)
- 释放对象时,通过
free_map数组和边界标记实现快速邻居查找:- 左邻居:
free_map[sub_id - 1] - 右邻居:
free_map[sub_id + 1]
- 左邻居:
- 若相邻子块空闲,则合并为更大区间并更新链表。
3. 内存碎片控制
- 空间利用率:典型场景下可达94%(如存放10个3MB对象,占用30MB/32MB)。
- 碎片风险:极端情况下剩余小碎片无法分配,但ZGC通过定期Region级压缩(将存活对象迁移到新Region)解决。
⚖️ 三、与小型/大型Region的对比
| Region类型 | 容量 | 对象大小 | 分配策略 | 内部管理 |
|---|---|---|---|---|
| 小型Region | 2MB固定 | <256KB | 线程本地缓存(TLA) | 指针碰撞分配 |
| 中型Region | 32MB固定 | 256KB~4MB | 共享Region | span_map+空闲链表管理子块 |
| 大型Region | ≥4MB动态 | >4MB | 独占Region | 整Region仅存一个对象 |
注:大型对象独占Region避免复制开销,中型对象共享Region提升内存利用率。
⚡️ 四、设计优势与代价
✅ 优势
- 高内存利用率:避免大型对象独占造成的空间浪费(如4MB对象独占32MB Region)。
- 分配效率:复用空闲Region减少OS调用,NUMA优化降低延迟。
- 碎片可控:子块合并+Region压缩双重保障。
⚠️ 代价
- 合并开销:释放对象时合并相邻区间需更新
free_map,时间复杂度O(n)(n=子块数),大对象释放略慢。 - 锁竞争:高并发场景下仍需分区锁兜底。
💎 总结
ZGC中型对象的首次分配策略是空间效率与并发性能的平衡:
- 复用优先:从空闲链表获取Region,减少OS申请。
- 精细管理:以4KB子块为单位分割空间,支持多对象共享。
- 低延迟保障:NUMA本地化+锁分割降低竞争。
- 设计本质:以可控的合并开销换取高内存利用率和亚毫秒级分配延迟,适应TB级堆下对中型对象的频繁分配需求。