G1如何管理空闲空间的
G1 垃圾收集器通过高效的空闲 Region 管理系统来优化内存分配和回收效率,这是其实现低停顿和高吞吐的关键。G1通过一个空闲链表和一个区间树来管理空闲的Region。
graph TD
A[空闲Region管理] --> B[数据结构]
A --> C[分配策略]
A --> D[回收机制]
A --> E[优化技术]
B --> B1[空闲列表]
B --> B2[区间树]
C --> C1[小对象分配]
C --> C2[大对象分配]
D --> D1[GC回收]
D --> D2[碎片整理]
E --> E1[尾部保留]
E --> E2[预分配]
一、核心数据结构
1. 空闲列表(Free List)
g1会在jvm启动时,预先将内存都划分为逻辑的Region对象,并通过空闲链表连接起来。当有申请空间的请求时,会将对应的region从空闲链表中移除。空闲链表是连接完全空闲的region。
classDiagram
class FreeList {
+regions: List~Region~
+add_region(Region)
+remove_region(Region)
+find_single_region(): Region
}
class Region {
+address: pointer
+size: size_t
+type: RegionType
+next_free: Region*
}
FreeList "1" -- "*" Region : 管理
2. 区间树(Interval Tree)
主要用于大对象分配,当区间树没有节点时,会从从空闲链表中选择的region,并标记为humongous,将region剩余空间加入到区间树,因为region会有部分区域没有用完,所以G1通过区间树,将各自region剩余的空间组织起来。只有区间树存在根节点,那么大对象分配就优先用空间树来寻找空间,否则使用空闲链表来寻找空间。
classDiagram
class IntervalTree {
+root: IntervalTreeNode*
+size: size_t
+insert(start, end)
+remove(node)
+find_contiguous(size) IntervalTreeNode*
+update_max_end(node)
}
class IntervalTreeNode {
+start: size_t
+end: size_t
+max_end: size_t
+color: Color
+left: IntervalTreeNode*
+right: IntervalTreeNode*
+parent: IntervalTreeNode*
+is_red() bool
+update_max_end_recursive()
}
class HeapRegion {
+bottom: address
+end: address
+top: address
+type: RegionType
}
class FreeBlock {
+start: address
+size: size_t
}
IntervalTree "1" -- "*" IntervalTreeNode : 管理
IntervalTreeNode "1" -- "0..1" HeapRegion : 关联
IntervalTreeNode "1" -- "0..1" FreeBlock : 表示空闲块
G1实现的区间树,可以理解为红黑树,只是value不是单一值,而是描述一段空间的起始地址,又因为内存没有相同地址描述不同物理地址的可能,所以每个空闲的区间段时唯一的。同时G1在实现区间树的节点里面添加了max_end值,表示当前节点包括所有子节点最大的end值,也就是说,选定某个节点max_end表示了这个节点为根的子树,最大空闲空间的截止地址为max_end。
2.1 相邻空间合并
区间树插入新节点时,会先检查是否存在相邻的空间,会将相邻的空间合并。
graph TD
A[新节点插入] --> B{相邻检查}
B -->|左侧相邻| C[合并左节点]
B -->|右侧相邻| D[合并右节点]
B -->|两侧相邻| E[三节点合并]
B -->|无相邻| F[直接插入]
二、启动内存管理
G1在启动的时候就会预先将内存划分等大的region,通过空闲链表串联起来。至于每个region多大,划分多少个region详情见Region 大小和数量,需要注意的是这个时候区间树是没有数据的。
三、小对象的分配策略
1. 小对象分配策略
小对象的分配,我们在另一篇文章有详细讲解,G1如何给新建的对象选择Region
sequenceDiagram
分配请求->>FreeList: 请求单个Region
FreeList->>选择: 最近释放优先
选择-->>分配请求: 返回Region地址
分配请求->>Region: 初始化为Eden
四、 大对象分配策略
1.大对象分配的流程
graph TD
A[大对象分配请求] --> B{计算需求}
B --> C["Region数 N = ceil(对象大小 / RegionSize)"]
C --> D{区间树查询}
D -->|成功| E[区间树分配]
D -->|失败| F[空闲链表分配]
E --> G[标记为Humongous Region]
F --> G
G --> H{空间使用}
H -->|完全使用| I[仅维护_next链]
H -->|部分使用| J[加入区间树]
I --> K[分配完成]
J --> K
style C white-space:normal
2.大对象分配流程详解
sequenceDiagram
请求->>G1: 分配大对象(size)
G1->>计算: N = ceil(size / RegionSize)
alt 区间树路径
计算->>区间树: 查询连续N-Region空间
区间树->>遍历: 利用max_end剪枝
遍历-->>区间树: 返回候选节点
区间树->>分配器: 返回空间地址
else 空闲链表路径
计算->>空闲链表: 查找连续N-Region
空闲链表->>扫描: 检查物理连续性
扫描-->>空闲链表: 返回Region链
空闲链表->>分配器: 返回Region链
end
分配器->>Region管理器: 锁定Region
Region管理器->>标记: 设置StartsHumongous
标记->>链接: 设置_next指针链
分配器->>检查: 是否完全使用?
检查-->>|是| 结束: 不操作区间树
检查-->>|否| 区间树: 添加新节点
区间树-->>分配器: 确认
分配器-->>请求: 分配成功
3.使用空闲链表分配Region
首次给大对象分配时,区间树都是没有数据的,因为区间树的节点都是来自大对象分配时,按region分配后,region剩余空间的起始地址作为区间树的节点数据。需要注意的是Full GC会整理空间,将区间树的节点数据清空,将碎片都整理为连续的空间,并加入空闲链表。所以Full GC后的大对象分配大概率是通过空闲链表来分配的。
3.1 优先使用连续的高地址空间(尾空间)
当区间树为空时首次分配大对象,G1 确实会优先选择高地址的连续 Region 区域(连续性优先级高于高地址),这是通过空闲链表的降序排序机制实现的。以下是完整的技术解析:
3.2 降序插入原理
sequenceDiagram
新Region释放->>FreeList: 插入操作
FreeList->>比较: 当前head地址 vs 新Region地址
比较-->>FreeList: 新Region地址 > head地址?
FreeList->>插入: 是:插入链表头部
FreeList->>插入: 否:遍历找到合适位置
插入-->>链表: 保持降序
3.3 大对象分配后的剩余处理
如果大对象需要5.5m,region每个2m,分配了3个之后,会存在0.5m的碎片区域,这0.5m的起始空间会作为区间树的一个节点插入到区间树中,这个空间可以用于新生代晋升的对象分配和新生代无法分配时的备选分配路径。当然如果剩余空间超过region的一半也可以用于大对象的分配,前提是超过一半剩余,才有可能用于大对象分配。
4.使用区间树寻找空间
4.1 先假设一个区间树
这个区间树最开始只有root和节点1节点2,当分配了3.5mb空间后,节点2变成了新节点。其实就是将剩余的0.5mb作为新节点插入到区间树,把节点2移除。
graph TD
%% 区间树结构
root[根节点<br/>start=0x00000000<br/>max_end=0x0F000000] --> node1
root --> node2
node1[节点1<br/>start=0x0E000000<br/>end=0x0E200000<br/>max_end=0x0E200000<br/>region=1536]
node2[节点2<br/>start=0x08000000<br/>end=0x08400000<br/>max_end=0x08400000<br/>region=1024]
%% Region链接
region1024[Region1024<br/>0x08000000-0x08200000] --> region1025
region1025[Region1025<br/>0x08200000-0x08400000]
%% 分配空间
allocated[已分配空间<br/>0x08000000-0x08380000] --> frag[剩余空间<br/>0x08380000-0x08400000]
%% 新节点
newNode[新节点<br/>start=0x08380000<br/>end=0x08400000<br/>region=1025]
%% 关系
node2 -.->|分配3.5MB之后的区间树| allocated
frag -.-> newNode
newNode -.-> region1025
node2 -.->|最初节点2是2个连续region| region1024
4.2 分配一个3.5MB大对象
- 先检查高地址节点1空间是否足够,不够则检查节点2
- 通过节点2的max_end-start是否大于分配的空间size,大于则说明子树只是有可能拥有足够的空间,不能代表一定有,因为这里节点2没有子节点,所以就不举例说明了,如果小于则说明子树的左边分支(这个动作叫做:剪枝)一定没有足够的空间,可以提高查找的效率,不需要继续深入到叶子节点来查询。
- 通过end-start≥size来判断当前节点是否满足空间分配需要。
- 如果足够将当前节点移除,除非节点的空间刚好=size,否则就将剩余的空间起始地址作为新的节点插入到区间树。
详细时序如下所示:
sequenceDiagram
participant Request
participant IntervalTree
participant Node1
participant Node2
participant Allocator
Request->>IntervalTree: 分配3.5MB空间
IntervalTree->>Node1: 检查高地址节点
Node1-->>IntervalTree: 空间不足(2MB<3.5MB)
IntervalTree->>Node2: 检查第二节点
Node2->>计算: end-start=4MB > 3.5MB
计算-->>Node2: 满足要求
Node2-->>IntervalTree: 返回可用
IntervalTree->>Allocator: 分配节点2空间
Allocator->>分割: 0x08000000-0x08380000
分割->>更新: 创建新节点管理剩余空间
更新-->>IntervalTree: 添加新节点
IntervalTree-->>Request: 返回地址0x08000000
5.大对象分配优化(G1 尾部保留策略)
G1会将尾部空间的15%保留,不加入正常的region分配中,当大对象分配无法从区间树和空闲链表中分配空间时,会启用尾部的15%的空间分配给大对象。
将空闲链表尾部的15%与前85%隔离开
graph TD
A[保留区管理] --> B[包含在空闲链表]
A --> C[特殊分配策略]
A --> D[尾部隔离机制]
B --> B1["物理上属于空闲链表"]
C --> C1["分配时跳过尾部"]
D --> D1["尾部Region标记"]
大对象的三层空间分配
graph TD
A[大对象分配] --> B{分配路径}
B --> C1["1.区间树(部分空闲Region)"]
B --> C2["2.空闲链表(完全空闲Region)"]
B --> C3["3.保留区(尾部15%)"]
C1 -->|失败| C2
C2 -->|失败| C3
C3 -->|成功| D[分配成功]
C3 -->|失败| E[触发Full GC]
style C2 white-space:normal
5.为什么要用区间树
1.因为碎片化是不可避免的问题,如果仅仅使用空闲链表来管理碎片化,那么空闲链表将不得不使用更多的类型标记不同大小的碎片空间,会提高空闲链表复杂度。
2.影响小对象的分配,复杂的空闲链表会降低小对象的分配速率。
五、Region的状态
1.region的五种状态
stateDiagram-v2
[*] --> 空闲
空闲 --> Eden: 分配小对象
空闲 --> Survivor: 晋升
空闲 --> Old: 直接分配
空闲 --> Humongous: 大对象
2.region状态转换
stateDiagram-v2
[*] --> Free
Free --> Eden: 小对象分配
Free --> Humongous: 大对象分配
Eden --> Survivor: Young GC 存活
Survivor --> Survivor: Young GC 存活
Survivor --> Old: 晋升(年龄阈值)
Survivor --> Free: Young GC 死亡
Old --> Old: Mixed GC 存活
Old --> Free: Mixed GC 死亡
Humongous --> Free: Full GC 回收
Humongous --> Humongous: Full GC 存活
Old --> Humongous: 特殊分配(极少)
note right of Free
初始状态,可分配
end note
note left of Eden
新对象分配区
占用整个Region
end note
note right of Survivor
Young GC存活对象
可能多次停留
end note
note left of Old
长期存活对象
混合GC管理
end note
note right of Humongous
大对象专用
跨多个Region
end note
六、G1中管理region的顶层结构
classDiagram
class G1CollectedHeap {
-_hrm : HeapRegionManager
-_eden_set : HeapRegionSet
-_survivor_set : HeapRegionSet
-_old_set : HeapRegionSet
-_humongous_set : HeapRegionSet
-_free_list : FreeRegionList
-_allocator: G1Allocator
+allocate_new_edem_region() : HeapRegion*
+allocate_new_survivor_region() : HeapRegion*
}
class HeapRegionManager {
-_regions[] : HeapRegion*
-_available_map : BitMap
-_committed_length : uint
+region_at(index) : HeapRegion*
+expand_single_region() : HeapRegion*
}
class HeapRegionSet {
<<interface>>
+add(region)
+remove(region)
+length() : uint
}
class HeapRegionSetBase {
-_head : HeapRegion*
-_tail : HeapRegion*
-_length : uint
+verify_region(hr)
}
class HeapRegionSet {
-_set_type : RegionType
}
class HeapRegionLinkedList {
-_head : HeapRegion*
-_tail : HeapRegion*
-_length : uint
+add_to_tail(hr)
+remove_from_head() : HeapRegion*
}
class HeapRegion {
-_type : RegionType
-_top : HeapWord*
-_end : HeapWord*
-_rem_set : PerRegionTable
-_next : HeapRegion*
-_prev : HeapRegion*
}
class G1Allocator {
-_region_allocator: G1RegionAllocator
+allocate(size): HeapWord*
}
class G1RegionAllocator {
-_interval_tree: IntervalTree
-_free_list: FreeRegionList
}
class IntervalTree {
-_root: IntervalTreeNode
+insert(start, end)
+find_contiguous(size)
}
G1CollectedHeap "1" --> "1" HeapRegionManager
G1CollectedHeap "1" --> "1" HeapRegionSet : _eden_set
G1CollectedHeap "1" --> "1" HeapRegionSet : _survivor_set
G1CollectedHeap "1" --> "1" HeapRegionSet : _old_set
G1CollectedHeap "1" --> "1" HeapRegionSet : _humongous_set
G1CollectedHeap "1" --> "1" FreeRegionList
HeapRegionSetBase <|-- HeapRegionSet
HeapRegionSetBase <|-- HeapRegionLinkedList
HeapRegionSetBase <|-- FreeRegionList
HeapRegionManager "1" --> "*" HeapRegion
HeapRegionSet "1" --> "*" HeapRegion
G1CollectedHeap "1" --> "1" G1Allocator
G1Allocator "1" --> "1" G1RegionAllocator
G1RegionAllocator "1" --> "1" IntervalTree
- eden_set:存储eden类型的region的指针数组
- survivor_set:存储survivor类型的region的指针数组
- old_set:存储老年代类型的region的指针数组
- humongous_set:大对象region的指针数组
- free_list:空闲region链表
- IntervalTree:这是大对象分配的核心指针,区间树
region按照分配的用图将指针加入对应的数组
七、生产环境配置
1. 关键参数优化
# 空闲列表管理
-XX:G1ConcRegionFreeingThreshold=5
-XX:G1HeapRegionSize=4m
# 尾部保留策略
-XX:G1ReservePercent=15
# 碎片控制
-XX:G1HeapWastePercent=5
-XX:G1MixedGCLiveThresholdPercent=85
2. 监控命令
# 查看空闲Region状态
jcmd <pid> GC.region_info free
# 输出示例
Free Regions:
Count: 15
Total Size: 60MB
Largest Contiguous: 16MB
Fragmentation: 12%
3. 问题诊断
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 分配延迟高 | 空闲列表碎片 | 降低G1HeapWastePercent |
| 大对象失败 | 连续空间不足 | 增加G1ReservePercent |
| GC频繁 | 空闲Region不足 | 调整新生代比例 |
八、总结
大小对象分配差异
| 特性 | 小对象分配 | 大对象分配 |
|---|---|---|
| 核心目标 | 低延迟分配 | 连续空间保障 |
| 关键约束 | 连续要求不敏感 | 必须物理连续 |
| 选择维度 | 多维价值评估 | 单一连续空间 |
| 数据结构 | 价值模型排序 | 物理地址扫描 |
| 性能目标 | 纳秒级分配 | 微秒级连续保障 |
G1 的空闲 Region 管理系统通过 空闲列表 + 区间树 + 智能策略 的组合:
- 实现小对象 O(1) 分配
- 大对象 O(log n) 连续分配
- 碎片率控制在 5% 以下
配合尾部保留和预分配策略,使 G1 能在亚毫秒级完成 95% 的内存分配请求,成为现代 Java 应用的理想选择。