Region的内部结构
⚙️ 一、小型 Region(2MB)内部结构
适用对象:< 256KB 的小对象
1.核心组件
bottom指针:Region 起始地址(固定)top指针:指针碰撞分配点(移动式)free_list:碎片块链表(分级管理)- 按碎片大小分级:16B/32B/64B/128B/256KB 五档
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 工作逻辑
- 对象死亡时,其空间按大小加入对应链表(如 48B 对象 → 加入 64B 链表)。
- 分配新对象时,从最接近对象大小的链表中获取碎片(如 60B 对象从 64B 链表分配)。
- 分配后剩余空间(如 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%)。 - 操作:
- 暂停 Region 分配。
- 将存活对象紧凑排列到 Region 头部。
- 重建空闲链表(合并所有空闲空间)。
- 开销:微秒级延迟(仅影响当前 Region)。
⚙️ 二、中型 Region(32MB)内部结构
适用对象:256KB ~ 4MB 的中型对象
1.核心组件
- 子区域(SubRegion):32MB 划分为 8192 个 4KB 子块
sub_top[8192]:每个子块的指针碰撞点(独立移动)span_map:跨子块对象记录(起始子块ID + 跨度)- 示例:3MB 对象占用 768 个子块(3×1024/4)
free_sub_list:空闲子块链表(记录连续空闲区间)free_map:字块的索引数组,主要通过子块id,能o(1)找到字块的地址和元数据
2.分配策略
- 单子块对象:直接移动
sub_top[i] - 跨子块对象:
- 从
free_sub_list获取连续空闲子块 - 在起始子块设置
sub_top,后续子块标记为SPANNED span_map记录对象跨度
- 从
3.span_map
,span_map 是管理跨子块(SubRegion)对象的核心数据结构,其作用是精确记录跨越多个连续子块的对象边界信息。
3.1 为什么需要 span_map?
中型 Region 被划分为 8192 个 4KB 的子块(SubRegion),但对象可能跨越多个子块:
- 示例:一个 3MB 的对象需占用 768 个连续子块(3×1024/4)。
- 问题:若仅记录起始地址,无法快速确定:
- 对象占用的总子块数
- 对象是否跨子块边界存储
- 回收时需清理的完整空间范围
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
回收阶段:快速释放完整空间
- 步骤:
- 根据对象起始地址找到起始子块 ID(如 100)
- 查
span_map[100]获取span_size=768 - 一次性释放子块 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 的大对象
核心组件:
start:Region 起始地址(对象对齐起点)object_size:对象实际大小(非 Region 大小)- 独占标志:整个 Region 仅存此对象(无碎片概念)
- 转发表项:染色指针指向的转移地址(并发转移用)
特殊机制:
- 无分配指针:对象独占 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通过以下手段降低影响:
- Region尺寸弹性缩放堆扩容时自动增大Region尺寸(如从4MB→8MB),减少超大型对象跨Region数量。
- 大对象下沉机制若连续分配多个LOR后剩余空间 ≥ 大对象阈值,后续对象可能复用碎片空间(需满足地址对齐)。
🔍 四、存活对象管理(三类 Region 通用)
ZGC 不依赖 Region 内位图标记存活,而是通过:
- 染色指针(Color Pointer)
- 对象引用地址的高 4 位存储标记状态(Marked0/1, Remapped)
- 全局转发表
- 记录转移中对象的新地址(旧地址 → 新地址映射)
- 读屏障自愈
- 访问对象时自动修正过期指针(基于染色位和转发表)
示例:
// 读屏障伪代码
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_list | free_sub_list(连续子块) | 无碎片 |
| 空间标记 | alloc_bitmap | 子块状态标记 | 无(整块占用) |
| 存活跟踪 | 染色指针 + 转发表 | 同左 | 同左 |
| 适用场景 | 高频小对象 | 中型数组/集合 | 大文件/缓存 |
关键结论:
- 小对象:位图 + 碎片链表 → 最大化空间利用率
- 中对象:子块指针 + 跨度记录 → 平衡连续性与碎片
- 大对象:独占 Region → 彻底规避碎片问题
三类 Region 通过 染色指针统一管理存活状态,实现并发回收的低延迟特性。