Skip to content

GC And StackMap #51

Description

@lfeng14

当前 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

函数会设置:

gc "hotspotgc"

随后 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 编译栈帧:

  1. 根据 PC 找到 OopMap。
  2. 找到寄存器和栈中的 oop。
  3. 标记存活对象。
  4. 对象移动后更新 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 做两件核心事情:

  1. 将非 leaf call/invoke 改写成 llvm.experimental.gc.statepoint
  2. 做 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 sizeframe 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()

  1. 找到 .llvm_stackmaps
  2. 遍历 record。
  3. 使用两个坐标匹配 call site:
普通 Java/Stub call:
    record.ID → _non_routine_call_sites[ID]

Runtime routine call:
    record.instructionOffset → _routine_call_sites[offset]
  1. 修正 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions