当前 C3 的 GC 支持可以拆成两条链:对象根跟踪和 GC 屏障。这两件事不要混在一起理解。
Java 字节码
↓
oop 标记为 LLVM 特殊地址空间
↓
插入 safepoint / GC barrier
↓
LLVM 优化 O3
↓
RewriteStatepointsForGC
↓
生成 statepoint、gc-live、gc.relocate
↓
LLVM StackMap
↓
转换成 HotSpot OopMap
↓
安装到 nmethod
↓
GC 扫描并更新栈帧里的对象引用
1. 标识哪些值是 Java 对象
C3 使用 LLVM 地址空间区分:
- AddressSpace 1:普通 oop
- AddressSpace 3:压缩 oop
HotspotGC::isGCManagedPointer() 据此识别 GC 管理的指针。
代码:GCStrategy.cpp
2. 建立 safepoint
C3 把下面这些位置作为潜在 safepoint:
- 普通 Java method 调用
- 循环回边
- method return 前的 poll
- 显式 safepoint poll
poll 快路径只检查线程的 polling word;需要 GC 时走慢路径,调用 HotSpot safepoint handler。
同时调用会携带 deopt bundle,保存:
- BCI
- locals
- operand stack
- monitors
- 内联调用链
代码:jeandleRuntimeDefinedJavaOps.cpp
3. LLVM 计算 safepoint 处存活的 oop
函数会设置:
随后 RewriteStatepointsForGC 将可能触发 GC 的调用改成:
普通 call/invoke
↓
llvm.experimental.gc.statepoint
↓
gc-live:此处存活的 oop
↓
gc.relocate:GC 后重新取得 oop
例如对象移动前:
obj ── safepoint ──> 旧地址可能失效
改写后使用:
obj ── statepoint ──> gc.relocate(obj) ──> 新地址
它还会记录字段地址等 derived pointer 与对象 base pointer 的关系。
流水线位置:Pipeline.cpp
4. StackMap 转成 HotSpot OopMap
LLVM 后端把以下信息写入 .llvm_stackmaps:
- oop 位于哪个寄存器或栈槽
- narrow oop
- base/derived pointer 关系
- statepoint 位置
C3 再解析 StackMap,构造 HotSpot 的 OopMap:
LLVM StackMap
↓
JeandleCompiledCode::parse_stackmap
↓
OopMap::set_oop
OopMap::set_narrowoop
OopMap::set_derived_oop
代码:jeandleCompiledCode.cpp
5. 安装到 nmethod
生成的 OopMap 和反优化信息通过 DebugInformationRecorder 记录到编译后的 nmethod。
运行 GC 时,HotSpot 遍历 C3 编译栈帧:
- 根据 PC 找到 OopMap。
- 找到寄存器和栈中的 oop。
- 标记存活对象。
- 对象移动后更新 oop、narrow oop 和 derived pointer。
代码:jeandleReloc.cpp
6. 对象读写的 GC barrier
这属于另一条链。C3 会识别 Java heap 中的引用写入:
store oop
↓
pre_barrier
↓
真正 store
↓
post_barrier
对于 G1:
- pre barrier:保存旧引用到 SATB 队列,保证并发标记正确。
- post barrier:更新 Card Table,记录跨区域引用。
barrier 先以高级 JavaOp 形式插入,经过 O3 后再展开成具体 G1 操作。
代码:InsertGCBarriers.cpp
当前支持范围
目前代码明确面向:
- Serial GC:基本不需要并发屏障。
- G1 GC:实现 pre/post barrier、SATB 和 Card Table。
ZGC、Shenandoah 依赖更复杂的 load barrier/colored pointer,目前这条 C3 barrier 链还没有完整支持。
一句话概括:
C3 通过 LLVM statepoint 跟踪栈上的存活 oop,通过 StackMap 转成 HotSpot OopMap;同时通过 G1 pre/post barrier 保证堆内引用关系正确。
可以把 Jeandle 的 LLVM StackMap 理解成:把 LLVM 寄存器分配后的物理位置,转换为 HotSpot 能消费的 OopMap + ScopeValue/PcDesc + call relocation。
Java bytecode
↓
Jeandle Abstract Interpreter
↓
call/invoke + "deopt" operand bundle
+ statepoint-id
+ statepoint-num-patch-bytes
↓
LLVM Inline
父 scope deopt 参数 prepend 到子 scope
↓
RewriteStatepointsForGC
call/invoke → gc.statepoint
+ deopt args
+ GC live pointers
+ base/derived mapping
↓
SelectionDAG + 寄存器分配
LLVM Value → 寄存器 / 栈槽 / 常量
↓
StackMaps.cpp
生成 ELF .llvm_stackmaps
↓
JeandleCompiledCode::finalize()
解析 StackMap
↓
OopMap + ScopeDesc/PcDesc + CallReloc
↓
安装为 HotSpot nmethod
1. 前端生成 deopt 状态
Java invoke 在 jeandleAbstractInterpreter.cpp 中:
- 分配唯一的
statepoint-id
- 保存对应的
CallSiteInfo
- 创建带
"deopt" operand bundle 的 invoke
- 设置需要预留的 patch bytes
deopt bundle 由 deopt_args() 构造。
当前单个 scope 的协议大致是:
Root scope:
[
should_reexecute,
bci, bci,
<local encode, value>...,
<stack encode, value>...,
<monitor encode, object, lock>...,
<orig_pc encode, slot>
]
Inlinee scope:
[
<MethodType encode, ciMethod*>,
should_reexecute,
bci, bci,
locals...,
stack...,
monitors...
]
这里有几个要点:
encode 负责补充 LLVM StackMap 本身没有的 Java 类型和 slot 类型。
- BCI 故意保存两次,作为 scope 开头的识别协议。
num_deopts 数的是 LLVM operand 数量,不是 Java value 数量。
- local/stack 通常消费 2 个 operand,monitor 消费 3 个。
2. Inline 后形成 scope 链
LLVM inlining 时会把父调用点的 deopt bundle prepend 到子调用的 bundle,见 InlineFunction.cpp。
因此最终顺序是:
root scope
→ MethodType(callee1) + callee1 scope
→ MethodType(callee2) + callee2 scope
→ ...
→ youngest scope
如果 inline 导致一个 call site 被复制,Jeandle 还会通过 VM callback 分配新的 statepoint-id,保证:
record ID → _non_routine_call_sites[ID]
仍然是一一对应的。
3. RS4GC 生成真正的 statepoint
Pipeline 顺序在 Pipeline.cpp:
LLVM O3
→ ExpandNarrowOopCast
→ RewriteStatepointsForGC
→ JeandleNarrowOopMarker
→ 后续 lowering
RewriteStatepointsForGC 做两件核心事情:
- 将非 leaf call/invoke 改写成
llvm.experimental.gc.statepoint。
- 做 GC pointer liveness 和 base-pointer 分析,生成:
GCLive = [derived0, derived1, ...]
BasePtrs = [base0, base1, ...]
对应代码在 RewriteStatepointsForGC.cpp。
Compressed oop 则由 JeandleNarrowOopMarker 在 deopt bundle 尾部追加:
<NarrowOopMarkerType encode, narrow-oop value>
4. LLVM 后端生成 .llvm_stackmaps
SelectionDAG lowering 和寄存器分配完成后,抽象的 LLVM Value 已经变成:
- Register
- Indirect
[SP + offset]
- Direct
- Constant
- ConstantIndex
StackMaps::parseStatepointOpers() 将一个 statepoint 写成如下 location 流,见 StackMaps.cpp:
[
calling-convention,
statepoint-flags,
num-deopt-operands,
deopt-operands...,
base0, derived0,
base1, derived1,
...
]
最后序列化进 ELF 的 .llvm_stackmaps section。
这里仓库中有一个误导注释:parse_stackmap_prologue() 把前两项写成了 frame size 和 frame offset,见 jeandleCompiledCode.cpp。实际上它们是:
#0 Calling Convention
#1 Statepoint Flags
#2 Number of Deopt Operands
目前代码只是忽略前两项,所以行为没错,注释需要后续修正。
5. HotSpot 解析 ELF StackMap
LLVM 生成 object 后,compile_module() 把 object 交给 JeandleCompiledCode。
安装阶段在 resolve_reloc_info():
- 找到
.llvm_stackmaps。
- 遍历 record。
- 使用两个坐标匹配 call site:
普通 Java/Stub call:
record.ID → _non_routine_call_sites[ID]
Runtime routine call:
record.instructionOffset → _routine_call_sites[offset]
- 修正 LLVM code offset:
stackmap offset
- post-call nop
+ HotSpot 生成的 prolog length
6. 一个 record 被解析成 scope 链和 OopMap
parse_stackmap() 从 root method 开始解析,见 jeandleCompiledCode.cpp。
遇到 MethodType 时:
- 当前 scope 解析结束;
- 返回一个
JeandleStackMap;
- 把
ciMethod* 传给外层;
- 外层继续从同一个 record 的当前位置解析 inlinee scope。
因此得到:
_stack_maps = [
root JeandleStackMap,
inlinee1 JeandleStackMap,
inlinee2 JeandleStackMap
]
只有最后、也就是最年轻的 scope 会继续消费 record 尾部的 GC pair,并构造 OopMap:
base == derived
→ oop_map->set_oop(derived)
base != derived
→ set_oop(base)
→ set_derived_oop(derived, base)
derived 被 NarrowOopMarker 标记
→ set_narrowoop(derived)
7. 转换为 HotSpot nmethod 信息
最终 process_stack_map():
_stack_maps.back()->oop_map()
→ DebugInformationRecorder::add_safepoint()
每个 JeandleStackMap
→ create_scope_values()
→ create_monitor_values()
→ describe_scope()
最后
→ end_safepoint()
然后 JeandleCallReloc 还会 patch call instruction、生成 static call stub 等。
所以 StackMap 最终服务三件事:
- GC:生成
OopMap,找到 wide oop、narrow oop、derived oop。
- Deoptimization/StackWalk:生成 locals、expression stack、monitor 和 inline scope 链。
- Call relocation:根据 ID/PC 找到调用类型和目标,完成 call-site patch。
实际观察时,可以用:
-XX:+JeandleDumpIR
-XX:+JeandleDumpObjects
-Xlog:jeandle=trace
-XX:+PrintNMethods
然后查看 object:
jeandle-llvm/build-fastdebug/bin/llvm-readobj --stackmap <dumped-object.o>
最合适的现成入口是 TestScopeValues.java,它能同时观察 deopt bundle 和最终的 PcDesc/ScopeDesc。
当前 C3 的 GC 支持可以拆成两条链:对象根跟踪和 GC 屏障。这两件事不要混在一起理解。
1. 标识哪些值是 Java 对象
C3 使用 LLVM 地址空间区分:
HotspotGC::isGCManagedPointer()据此识别 GC 管理的指针。代码:GCStrategy.cpp
2. 建立 safepoint
C3 把下面这些位置作为潜在 safepoint:
poll 快路径只检查线程的 polling word;需要 GC 时走慢路径,调用 HotSpot safepoint handler。
同时调用会携带
deoptbundle,保存:代码:jeandleRuntimeDefinedJavaOps.cpp
3. LLVM 计算 safepoint 处存活的 oop
函数会设置:
随后
RewriteStatepointsForGC将可能触发 GC 的调用改成:例如对象移动前:
改写后使用:
它还会记录字段地址等 derived pointer 与对象 base pointer 的关系。
流水线位置:Pipeline.cpp
4. StackMap 转成 HotSpot OopMap
LLVM 后端把以下信息写入
.llvm_stackmaps:C3 再解析 StackMap,构造 HotSpot 的
OopMap:代码:jeandleCompiledCode.cpp
5. 安装到 nmethod
生成的
OopMap和反优化信息通过DebugInformationRecorder记录到编译后的nmethod。运行 GC 时,HotSpot 遍历 C3 编译栈帧:
代码:jeandleReloc.cpp
6. 对象读写的 GC barrier
这属于另一条链。C3 会识别 Java heap 中的引用写入:
对于 G1:
barrier 先以高级 JavaOp 形式插入,经过 O3 后再展开成具体 G1 操作。
代码:InsertGCBarriers.cpp
当前支持范围
目前代码明确面向:
ZGC、Shenandoah 依赖更复杂的 load barrier/colored pointer,目前这条 C3 barrier 链还没有完整支持。
一句话概括:
可以把 Jeandle 的 LLVM StackMap 理解成:把 LLVM 寄存器分配后的物理位置,转换为 HotSpot 能消费的
OopMap + ScopeValue/PcDesc + call relocation。1. 前端生成 deopt 状态
Java invoke 在 jeandleAbstractInterpreter.cpp 中:
statepoint-idCallSiteInfo"deopt"operand bundle 的invokedeopt bundle 由 deopt_args() 构造。
当前单个 scope 的协议大致是:
这里有几个要点:
encode负责补充 LLVM StackMap 本身没有的 Java 类型和 slot 类型。num_deopts数的是 LLVM operand 数量,不是 Java value 数量。2. Inline 后形成 scope 链
LLVM inlining 时会把父调用点的 deopt bundle prepend 到子调用的 bundle,见 InlineFunction.cpp。
因此最终顺序是:
如果 inline 导致一个 call site 被复制,Jeandle 还会通过 VM callback 分配新的
statepoint-id,保证:仍然是一一对应的。
3. RS4GC 生成真正的 statepoint
Pipeline 顺序在 Pipeline.cpp:
RewriteStatepointsForGC做两件核心事情:llvm.experimental.gc.statepoint。对应代码在 RewriteStatepointsForGC.cpp。
Compressed oop 则由
JeandleNarrowOopMarker在 deopt bundle 尾部追加:4. LLVM 后端生成
.llvm_stackmapsSelectionDAG lowering 和寄存器分配完成后,抽象的 LLVM Value 已经变成:
[SP + offset]StackMaps::parseStatepointOpers()将一个 statepoint 写成如下 location 流,见 StackMaps.cpp:最后序列化进 ELF 的
.llvm_stackmapssection。这里仓库中有一个误导注释:
parse_stackmap_prologue()把前两项写成了frame size和frame offset,见 jeandleCompiledCode.cpp。实际上它们是:目前代码只是忽略前两项,所以行为没错,注释需要后续修正。
5. HotSpot 解析 ELF StackMap
LLVM 生成 object 后,compile_module() 把 object 交给
JeandleCompiledCode。安装阶段在 resolve_reloc_info():
.llvm_stackmaps。6. 一个 record 被解析成 scope 链和 OopMap
parse_stackmap()从 root method 开始解析,见 jeandleCompiledCode.cpp。遇到
MethodType时:JeandleStackMap;ciMethod*传给外层;因此得到:
只有最后、也就是最年轻的 scope 会继续消费 record 尾部的 GC pair,并构造
OopMap:7. 转换为 HotSpot nmethod 信息
最终 process_stack_map():
然后
JeandleCallReloc还会 patch call instruction、生成 static call stub 等。所以 StackMap 最终服务三件事:
OopMap,找到 wide oop、narrow oop、derived oop。实际观察时,可以用:
然后查看 object:
最合适的现成入口是 TestScopeValues.java,它能同时观察 deopt bundle 和最终的
PcDesc/ScopeDesc。