中对象如何选择Region

68 阅读3分钟

中对象如何选择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类型容量对象大小分配策略内部管理
小型Region2MB固定<256KB线程本地缓存(TLA)指针碰撞分配
中型Region32MB固定256KB~4MB共享Regionspan_map+空闲链表管理子块
大型Region≥4MB动态>4MB独占Region整Region仅存一个对象

注:大型对象独占Region避免复制开销,中型对象共享Region提升内存利用率。


⚡️ 四、设计优势与代价

优势

  1. 高内存利用率:避免大型对象独占造成的空间浪费(如4MB对象独占32MB Region)。
  2. 分配效率:复用空闲Region减少OS调用,NUMA优化降低延迟。
  3. 碎片可控:子块合并+Region压缩双重保障。

⚠️ 代价

  • 合并开销:释放对象时合并相邻区间需更新free_map,时间复杂度O(n)(n=子块数),大对象释放略慢。
  • 锁竞争:高并发场景下仍需分区锁兜底。

💎 总结

ZGC中型对象的首次分配策略是空间效率与并发性能的平衡

  1. 复用优先:从空闲链表获取Region,减少OS申请。
  2. 精细管理:以4KB子块为单位分割空间,支持多对象共享。
  3. 低延迟保障:NUMA本地化+锁分割降低竞争。
  4. 设计本质:以可控的合并开销换取高内存利用率亚毫秒级分配延迟,适应TB级堆下对中型对象的频繁分配需求。