ZGC中Region的内部结构

93 阅读10分钟

Region的内部结构

⚙️ 一、小型 Region(2MB)内部结构

适用对象:< 256KB 的小对象

1.核心组件

  1. bottom 指针:Region 起始地址(固定)
  2. top 指针:指针碰撞分配点(移动式)
  3. free_list:碎片块链表(分级管理)
    • 按碎片大小分级:16B/32B/64B/128B/256KB 五档
  4. alloc_bitmap:512位位图(每 bit 对应 4KB 块)
    • 1:已分配
    • 0:空闲(可指针碰撞或碎片利用)

2.分配流程

flowchart TD
    A[分配请求] --> B{对象大小}
    B -->|<=256KB| C[尝试指针碰撞]
    C -->|top+size<=end| D[移动top分配]
    C -->|空间不足| E[查free_list]
    E -->|有合适碎片| F[碎片分配]
    E -->|无碎片| G[标记位图对应块]

3.碎片块链表

ZGC 通过 分级空闲链表(Segregated Free List) 管理碎片:

graph TD
    FreeList --> Level16[16B 空闲块链表]
    FreeList --> Level32[32B 空闲块链表]
    FreeList --> Level64[64B 空闲块链表]
    FreeList --> Level128[128B 空闲块链表]
    FreeList --> Level256[256KB 空闲块链表]

3.1 工作逻辑

  1. 对象死亡时,其空间按大小加入对应链表(如 48B 对象 → 加入 64B 链表)。
  2. 分配新对象时,从最接近对象大小的链表中获取碎片(如 60B 对象从 64B 链表分配)。
  3. 分配后剩余空间(如 4B)加入更小链表(16B 链表)。

3.2 碎片产生的三大场景

对象尺寸不匹配

  • 场景:Region 内已分配对象后,剩余空间不足以容纳新对象,但碎片总和足够。
  • 示例
    • Region 剩余空间:128KB(连续)
    • 新对象需求:64KB(可放入)
    • 但若剩余空间被已死亡对象分割为两段(如 64KB + 64KB),则无法直接分配。
  • 结果:形成外部碎片(空闲但无法利用)。

并发回收延迟

  • 场景:ZGC 的并发标记阶段未及时回收死亡对象,导致存活对象“孤岛”分隔空闲空间。

  • 示例

    回收前:[存活32KB][死亡64KB][存活96KB][死亡128KB]
    并发标记中:死亡对象未被清除 → 空闲空间碎片化
    

4.碎片合并优化

4.1 相邻空闲块合并

  • 时机:对象释放空间时,检查相邻地址是否空闲。

  • 合并规则

    void merge_free_blocks(Block* block) {
      if (prev_block_is_free(block))
        merge(prev, block);// 向前合并if (next_block_is_free(block))
        merge(block, next);// 向后合并
      add_to_freelist(merged_block);// 加入更大链表
    }
    
  • 效果:将小碎片合并为大块,减少外部碎片。

4.2 定期碎片整理

  • 触发条件:Region 内碎片率 > 阈值(默认 XX:ZFragThreshold=30%)。
  • 操作
    1. 暂停 Region 分配。
    2. 将存活对象紧凑排列到 Region 头部。
    3. 重建空闲链表(合并所有空闲空间)。
  • 开销:微秒级延迟(仅影响当前 Region)。

⚙️ 二、中型 Region(32MB)内部结构

适用对象:256KB ~ 4MB 的中型对象

1.核心组件

  1. 子区域(SubRegion):32MB 划分为 8192 个 4KB 子块
  2. sub_top[8192]:每个子块的指针碰撞点(独立移动)
  3. span_map:跨子块对象记录(起始子块ID + 跨度)
    • 示例:3MB 对象占用 768 个子块(3×1024/4)
  4. free_sub_list:空闲子块链表(记录连续空闲区间)
  5. free_map :字块的索引数组,主要通过子块id,能o(1)找到字块的地址和元数据

2.分配策略

  • 单子块对象:直接移动 sub_top[i]
  • 跨子块对象
    1. free_sub_list 获取连续空闲子块
    2. 在起始子块设置 sub_top,后续子块标记为 SPANNED
    3. span_map 记录对象跨度

3.span_map

span_map 是管理跨子块(SubRegion)对象的核心数据结构,其作用是精确记录跨越多个连续子块的对象边界信息

3.1 为什么需要 span_map?

中型 Region 被划分为 8192 个 4KB 的子块(SubRegion),但对象可能跨越多个子块:

  • 示例:一个 3MB 的对象需占用 768 个连续子块(3×1024/4)。
  • 问题:若仅记录起始地址,无法快速确定:
    1. 对象占用的总子块数
    2. 对象是否跨子块边界存储
    3. 回收时需清理的完整空间范围

span_map 通过轻量级元数据解决此问题。

3.2 span_map 的数据结构

全局数组:每个中型 Region 包含一个 span_map[] 数组,长度 = 子块数(8192)。

struct SpanEntry {
    uint16_t start_id;  // 起始子块ID(0~8191)
    uint16_t span_size; // 占用子块数(1~1024)
};

3.3 工作逻辑

graph TD
    A[分配对象] --> B[计算所需子块数]
    B --> C[查找连续空闲子块]
    C --> D[在起始子块设置span_map]
    D --> E[标记后续子块为SPANNED]
    
    E -->|回收时| F[根据span_map清理]

3.4 span_map 的核心功能

分配阶段:建立对象-子块映射

  • 起始子块:写入 SpanEntry{start_id=X, span_size=N}
  • 后续子块:标记为 SPANNED(避免重复分配)
  • 示例:对象从子块 100 开始,占用 768 个子块 →
    • span_map[100] = {100, 768}
    • 子块 101~867 标记为 SPANNED

回收阶段:快速释放完整空间

  • 步骤
    1. 根据对象起始地址找到起始子块 ID(如 100)
    2. 查 span_map[100] 获取 span_size=768
    3. 一次性释放子块 100~867(共 768 块)
  • 优势:避免逐子块扫描,释放操作 O(1) 完成

4.free_map 的本质与构建

4.1 数据结构定义

// 全局索引表(每个中型 Region 独立)
FreeSpan* free_map[8192];  // 8192 = 32MB / 4KB(子块数)
  • 作用:将子块 ID 映射到所属空闲区间的指针
  • 内存开销:8192 条目 × 8 字节 = 64KB(占 Region 的 0.2%)

4.2 初始化构建

sequenceDiagram
    participant GC as GC线程
    participant Region as 中型Region
    participant FreeMap as free_map
    
    GC->>Region: 1. 初始化Region(首次分配前)
    Region->>FreeMap: 2. 分配64KB内存
    Region->>FreeMap: 3. 全部初始化为NULL
    Note over FreeMap: free_map[i]=NULL 表示子块i已被占用

4.3 运行时维护

事件free_map 更新操作
分配连续子块将区间内所有子块的 free_map[id] = NULL(标记为占用)
释放连续子块创建新 FreeSpan 节点,将区间内所有子块的 free_map[id] = &new_span
合并空闲区间更新合并后新区间的 FreeSpan 范围,并重设相关子块的 free_map[id] = &merged_span

4.4 free_map 如何实现 O(1) 邻居查找?

定位左邻居

// 释放区间 [S, E] 时
FreeSpan* left_neighbor = free_map[S - 1];// 直接访问左侧子块
  • 逻辑
    • 若 S-1 子块对应的 free_map[S-1] != NULL → 该子块属于某个空闲区间
    • 通过 free_map[S-1] 直接获取该空闲区间指针

定位右邻居

FreeSpan* right_neighbor = free_map[E + 1];// 直接访问右侧子块

判断是否相邻

// 检查左邻居是否与当前区间物理相邻
if (left_neighbor != NULL && left_neighbor->end_id == S - 1) {
		// 可合并(左邻居的结束子块 = 当前起始子块-1)
}

// 检查右邻居是否相邻
if (right_neighbor != NULL && right_neighbor->start_id == E + 1) {
	// 可合并(右邻居的起始子块 = 当前结束子块+1)
}

5.free_sub_list碎片链表

在 ZGC 的中型 Region(32MB)中,free_sub_list 是管理连续空闲子块的核心数据结构,其设计目标是高效分配连续子块空间以满足中型对象的内存需求。以下是其完整工作机制:

5.1 数据结构定义

struct FreeSpan {
    uint16_t start_id;  // 起始子块ID(0~8191)
    uint16_t length;    // 连续空闲子块数量
    FreeSpan* next;     // 指向下一个空闲区间
    FreeSpan* prev;     // 指向上一个空闲区间
};

FreeSpan* free_sub_list; // 链表头指针
  • 每个节点:记录一段连续的 4KB 子块区间(如从子块 100 开始,连续 16 个子块)
  • 链表排序:按 start_id 升序排列(便于快速合并相邻区间)

5.2 内存布局示例

Region 32MB → 8192 个 4KB 子块
free_sub_list 链表:
  节点1: [start=100, length=8]   → 空闲区间:子块100~107
  节点2: [start=200, length=16]  → 空闲区间:子块200~215
  节点3: [start=500, length=32]  → 空闲区间:子块500~531

5.3 分配连续子块(对象分配)

graph TD
    A[申请N个连续子块] --> B{遍历free_sub_list}
    B -->|找到长度>=N的节点| C[切分该节点]
    C --> D[分配前N个子块]
    C --> E[剩余空间生成新节点]
    B -->|无合适节点| F[分配失败]
  • 切分示例:从节点 [start=200, length=16] 分配 8 个子块:
    • 分配子块 200~207
    • 新节点 [start=208, length=8] 加入链表

5.4 释放连续子块(对象回收)

graph TD
    A[释放子块S->E] --> B{检查相邻子块}
    B -->|左侧空闲| C[合并左侧空闲节点]
    B -->|右侧空闲| D[合并右侧空闲节点]
    C & D --> E[更新节点长度]
    E -->|无相邻| F[新建节点加入链表]
  • 合并示例:释放子块 208~215:
    • 左侧相邻节点 [start=200, length=8](子块200~207)
    • 合并后 → [start=200, length=16]

5.5 缓存优化

额外维护 free_sub_cache[8] 数组,缓存长度为 2^N 的区间(如 2/4/8/16 子块),下标0的元素指向的就是2个连续子块的指针,下标1指向的就是连续4个字块的指针,以此类推。所以可以直接看申请空间离的最近的2的次幂数,寻找下标获取空间。

5.6 防碎片策略

  • 区间合并阈值:仅当相邻空闲区间 >4 子块(16KB) 时才合并(避免微小碎片)

    #define MERGE_THRESHOLD 4// -XX:ZSubRegionCoalesce=4
    
  • 区间分裂限制:剩余空间 <4 子块 时不分裂,整块分配(减少碎片)

    if (remaining < 4) {
        allocate_full_block();// 分配整个节点
    }
    

5.7 与小region的空闲链表对比

维度中型 Region free_sub_list小型 Region free_list
管理单元连续子块区间(4KB 粒度)独立碎片块(16B~256KB 分级)
分配目标满足跨子块对象的连续空间需求快速分配零散小对象
合并机制实时合并相邻空闲区间(O(1))仅合并相邻碎片(需遍历)
碎片控制通过区间维护避免外部碎片容忍外部碎片(靠定期整理)
适用对象256KB~4MB 中型对象<256KB 小对象

⚙️ 三、大型 Region(动态)内部结构

适用对象:≥4MB 的大对象

核心组件

  1. start:Region 起始地址(对象对齐起点)
  2. object_size:对象实际大小(非 Region 大小)
  3. 独占标志:整个 Region 仅存此对象(无碎片概念)
  4. 转发表项:染色指针指向的转移地址(并发转移用)

特殊机制

  • 无分配指针:对象独占 Region,无需碰撞指针
  • 无碎片链表:对象存活期间 Region 完全占用
  • 回收即释放:对象死亡后整个 Region 直接归还空闲池

独占一个或多个完整的Region(Region大小可变,通常为2MB/4MB/8MB/16MB/32MB

可配置性与默认值

参数默认行为手动设置
Region尺寸 (ZGranuleSize)根据堆大小自动选择:-Xmx<4G → 2MB-Xmx4G~64G → 4MB通过 -XX:ZGranuleSize=8m 强制为8MB (需JDK16+)
大对象阈值 (ZLargeObjectThreshold)默认等于Region尺寸(即≥ Region大小的对象视为大对象)支持动态调整:-XX:ZLargeObjectThreshold=4m

虽然存在内部碎片,但ZGC通过以下手段降低影响:

  1. Region尺寸弹性缩放堆扩容时自动增大Region尺寸(如从4MB→8MB),减少超大型对象跨Region数量。
  2. 大对象下沉机制若连续分配多个LOR后剩余空间 ≥ 大对象阈值,后续对象可能复用碎片空间(需满足地址对齐)。

🔍 四、存活对象管理(三类 Region 通用)

ZGC 不依赖 Region 内位图标记存活,而是通过:

  1. 染色指针(Color Pointer)
    • 对象引用地址的高 4 位存储标记状态(Marked0/1, Remapped)
  2. 全局转发表
    • 记录转移中对象的新地址(旧地址 → 新地址映射)
  3. 读屏障自愈
    • 访问对象时自动修正过期指针(基于染色位和转发表)

示例

// 读屏障伪代码
void* load_barrier(void* ptr) {
  if (is_remapped(ptr)) {         // 检查染色位
    void* new_addr = forward_table.get(ptr);  // 查转发表
    update_reference(ptr, new_addr); // 自愈指针
    return new_addr;
  }
  return ptr;
}

💎 总结:Region 内部结构核心差异

组件小型 Region中型 Region大型 Region
分配指针top(全局移动)sub_top[i](子块独立)无(独占)
碎片管理分级 free_listfree_sub_list(连续子块)无碎片
空间标记alloc_bitmap子块状态标记无(整块占用)
存活跟踪染色指针 + 转发表同左同左
适用场景高频小对象中型数组/集合大文件/缓存

关键结论:

  • 小对象:位图 + 碎片链表 → 最大化空间利用率
  • 中对象:子块指针 + 跨度记录 → 平衡连续性与碎片
  • 大对象:独占 Region → 彻底规避碎片问题

三类 Region 通过 染色指针统一管理存活状态,实现并发回收的低延迟特性。