Android Runtime(ART)机制与 RISC-V64 支持研究(v2.1-public)

由 art-runtime-riscv64-research.md 生成
项目内容
文档类型技术调研报告
版本v2.1-public(机制 v2.0-r7 + benchmark 增补)
状态公开研究稿(含 benchmark 增补;源码与固定 pin 复核,未做设备实测)
密级公开
作者振军
生成日期2026-10-21
研究截点2026-10-19(公开 refs 复核;ART 主 pin android17-release b753cf97)
目标读者系统/性能/虚拟机/编译器工程师,RISC-V64 平台与后续优化任务执行者
调研问题ART 端到端运行机制;RISC-V64 的真实支持面与缺口;上游演进与支持状态分层;面向后续优化的可执行问题地图
覆盖范围platform/art 的 android17-release 主 pin(对照 main 与 25Q2/25Q4)、RISC-V64 相关公开提交(2023-01 至 2026)、libcore/frameworks 各 android17-release 的案例与启动文件;测试与工具目录;ART benchmark/run-test/gtest 设施、libcore 代表链与测量方法
排除范围内部树实现与私有数据、任何设备实测、Gerrit 在审变更的完整枚举(通道受限,见附录 C)

摘要

背景与现状

ART 的位置与组件。 ART 是 Android 的 Java 运行时,随 APEX(com.android.art)更新。它在进程内的建立以 JNI_CreateJavaVM(runtime/jni/java_vm_ext.cc:1237)为入口,经 Runtime::Create(runtime/runtime.cc:1007)与 Runtime::Init(:1625)构造 gc::Heap(boot image 的装载就发生在 Heap 构造函数内,heap.cc:520 → ImageSpace::LoadBootImage)、fault manager、JavaVMExt、主线程与 ClassLinker(InitFromBootImage,:1351),再由 Runtime::Start(:1098)完成 root clinit、运行期 native 注册与 JIT 创建。应用进程由 zygote fork 得到,boot image 是 PROT_READ|PROT_WRITE + MAP_PRIVATE 的写时复制映射:未写页在 fork 后共享物理页,重定位/入口修正会弄脏私有副本;fork 前后分别执行 Runtime::PreZygoteFork(:869)与 InitNonZygoteOrPostFork(:1265)[S05][S07][S08][S25]。

产物与执行形态。 dex2oat 产出三类文件:vdex(原始 DEX + checksum + verifier deps + 类型查找表)、oat(编译代码 + 元数据 + stack map + boot classpath 键)、art image(堆对象快照与入口点/stub 地址)。a17r 的 vdex 已经没有 quickening 段,过滤器名 quicken 只作兼容告警并映射为 verify(compiler_filter.cc:181-184);把 vdex 讲成“quickening 缓存”是过时说法 [S32][S36]。运行期执行形态由三层条件共同决定:编译过滤器/profile 决定哪些方法有 AOT 代码(libartbase/base/compiler_filter.h:34);CanRuntimeUseNterp() 决定未编译方法能否用 nterp(nterp.cc:61-75);JIT 的 MaybeEnqueueCompilation(jit.cc:1747)决定热度触发后的编译类型。任何“某方法一定跑在 X 引擎”的断言都必须给出这三层条件 [S23][S28][S32]。构建与部署侧,com.android.art 以 APEX 形式更新,设备开机窗口由 odrefresh 产出/校验引导镜像与其 oat/vdex(26Q2 的主镜像和 mainline 默认过滤器都是 speed-profile,odrefresh.cc:130/133),artd 负责运行期的 dexopt/工件管理;这两条链路决定了“设备上哪些方法有 AOT 代码”这一常被忽略的前提 [S32]。测试设施方面,test/ 的 run-test、TEST_MAPPING 的 host gtest 与 tools/buildbot-* 的 Cuttlefish/VM 流程构成可见的验证面;本报告只引用这些设施与上游记录,不把它们当作本人实测 [S37]。

分支模型(2025 年的变化)。 公开 main 自 2025-03-26 冻结在 6484611f(tree 688a2326… 与官方网页一致),开发转入 release 列车:25Q2(android16-release = android-16.0.0_r1/r2)、25Q3(qpr1 = r3)、25Q4(qpr2 = r4)、26Q2(android17-release = android-17.0.0_r1);android17-security-release 不是 17 release 的后代(两者 merge-base 为 d9ee3abab2,安全线只多两个非 RISC-V 提交)。截至 2026-10-19,公开 refs 中没有 android18/26Q4 分支 [S01][S33]。

RISC-V64 的现状速览。 2023-01 起,Alibaba(Lifang Xia)与 Google(Ulya Trafimovich、Jaeheon Yi)把 RV64 作为新 InstructionSet 接入;2023 年补汇编器、codegen、JNI、nterp、intrinsics 与 oat 重定位;2024 年补 C/Zba/Zbb/Zbs、简化器与字符串/数学内建;2025 年进入维护与“向量寄存器分配 part 1”等前沿基础设施。到 26Q2,riscv64 专属源码约 60 文件(汇编器约 7900 行、codegen 约 7400 行、intrinsics 约 5900 行、170 个内建实现),并配有 458-checker-riscv64-* 用例与 Cuttlefish/Orange Pi 的测试记录;同时有三处“源码存在≠能力可用”的反例:nterp 早期只是 unimp 桩、RVV 只有汇编器与寄存器分配(向量 codegen 仍 FATAL)、GC 无 marking register [S11][S12][S30][S33][S34]。

研究基线与观察身份。 主 pin 为 android17-release b753cf97923c3695338d21466fa14c57b480a59a;libcore 与 frameworks/base 分别取其 android17-release tip(4ebfb391…、94b4c163…)。所有 ART 行号以主 pin 工作树为准。观察身份分四级:源码确证(本报告主体)、可复现但未执行(命令)、外部测试记录(上游提交信息中的 Test 步骤,非本报告实测)、本人实测(本报告无——未上板、未构建)。因此下文不出现任何实测性能数字;涉及性能处只给机制、观测口径与正确性门 [S01][S27]。

技术详解

本章以一条真实调用(String.equals)为主线组织:4.1 案例与基线 → 4.2 启动/产物/类加载 → 4.3 解释器 → 4.4 编译管线 → 4.5 RV64 后端 → 4.6 GC → 4.7 线程与内存序 → 4.8 JNI/ABI → 4.9 演进与支持分层 → 4.10 优化地图。

第 1 章(4.1)案例与基线:一次 String.equals 的调用点与执行路径

本章是重写版的“纵切案例”。它先把一条真实调用放回启动链(图 1),再把“调用点是否内建”与“callee 走哪个入口”拆成两条正交的分派轴(图 2),给后续各章提供统一参照。所有行号以 ART android17-release b753cf97、libcore/frameworks 各自 android17-release pin 为准;本报告未运行设备。
2026-10-20T01:16:39.170098 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/
2026-10-20T01:16:42.094204 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

手机可横向滑动查看本图

图 1:案例调用在启动链中的位置(简化)

图 1: 案例调用在启动链中的位置(来源:ART a17r 的 runtime.cc/java_vm_ext.cc 与 frameworks ZygoteInit.java、AndroidRuntime.cpp、app_main.cpp,[S05][S23][S24][S48],本项目绘制)。读图要点:先建立 Runtime(Heap 内装载 boot image,ClassLinker 接管),再执行 root clinit 并启动 JIT,最后进入 ZygoteInit.main 的参数解析;案例的 equals 调用就发生在参数解析循环里。main 后半段才创建 ZygoteServer、fork system_server 并进入 runSelectLoop;zygoteInit/nativeZygoteInit 是子进程初始化函数,不属于 main 的 fork 循环。图只画顺序,不画分派;分派见图 2。

4.1.1 为什么选这条案例

String.equals 是启动与日常运行中常见的字符串比较方法(本报告不引用“频率排名”这类无公开依据的说法)。选择它作为样本,是因为它同时满足四个可验证条件:

  1. 有真实调用点:ZygoteInit.main 解析 zygote 启动参数时执行 "start-system-server".equals(argv[i])(frameworks/base android17-release,core/java/com/android/internal/os/ZygoteInit.java:856)[S23]。
  2. 同一个调用点存在“内建/非内建”两种可能:ART 把 Ljava/lang/String;.equals(Ljava/lang/Object;)Z 登记为内建(runtime/intrinsics_list.h:214)[S13];是否在本调用点生效取决于调用者是否被编译以及编译器是否把该 invoke 识别为内建(4.1.4)。
  3. callee 本身有“编译/未编译”两种状态:libcore 的 String.equals 源码是纯 Java 循环(String.java:1660 起,循环在 :1675-1682)[S21];但 boot-image 编译可尝试 StringEquals 的整方法 intrinsic body,因此不能从源码形态推断 callee 一定执行 Java 循环;它是否以 Java 循环执行取决于 callee 的入口点,而不是调用点是否内建。
  4. 能落到 RV64 的两类真实代码:内建路径有 riscv64 专属 codegen(VisitStringEquals,intrinsics_riscv64.cc:1152)[S12];解释路径中的 charAt 是 @FastNative native 方法(String.java:1273;实现 runtime/native/java_lang_String.cc:36),nterp 对 native 目标按 managed→quick code 的规则调用(mterp/riscv64/invoke.S 头注释)[S17][S19]。

4.1.2 版本基线(2026-10-19 复核)

对象Pin说明
ART(主对象)android17-release = b753cf97923c3695338d21466fa14c57b480a59a26Q2 线;ART 源码行号以此工作树为准 [S02]
ART 对照android16-qpr2-release = 1690c6912a7972c9e62c39b48c706de9b8b18b4a(=android-16.0.0_r4)25Q4 [S04]
ART 公开 main6484611fd45e69db9f33f98bfd6864014b030ecf2025-03-26 起冻结 [S03]
libcoreandroid17-release = 4ebfb391ee0109d16abe6e2bc965724b940928c5String.java、StringLatin1.java、StringUTF16.java、System.java [S21][S22]
frameworks/baseandroid17-release = 94b4c163b7dfe5ce3607f7bb8456f9573f7de57dZygoteInit.java、RuntimeInit.java、AndroidRuntime.cpp [S23][S24]

观察身份与研究方法统一见附录 C(源码确证/可复现未执行/外部测试记录/本人实测四级;本报告无本人实测)。

4.1.3 端到端链路:从进程入口到这次调用

把这次 equals 放回启动链(图 1),顺序如下:

  1. 进程入口与 JVM 创建:zygote 由 app_process 启动,AndroidRuntime::startVm 组装 JavaVMOption(AndroidRuntime.cpp:657)后调用 JNI_CreateJavaVM(:1208);ART 侧入口是 runtime/jni/java_vm_ext.cc:1237,它调用 Runtime::Create(runtime/runtime.cc:1007)与 Runtime::Init(:1625),再 Runtime::Start(:1098)[S24][S06][S05]。
  2. Runtime::Init 的关键对象:Monitor/ThreadList 之后构造 gc::Heap(:1894);boot image 的装载在 Heap 构造函数内部(heap.cc:520 → ImageSpace::LoadBootImage,image_space.cc:3016),随后注册 fault manager 与 nterp 代码段、创建 JavaVMExt、Thread::Attach,最后由 ClassLinker::InitFromBootImage(:1351)接管镜像中的类与入口点 [S05][S07][S08]。
  3. 镜像映射的真实权限:非压缩镜像走 MemMap::MapFileAtAddress(..., PROT_READ | PROT_WRITE, MAP_PRIVATE, ...)(image_space.cc:1383-1396);压缩镜像先匿名映射 RW 再解压拷贝(:1407-1420)。因此镜像是可写私有映射(写时复制),不是“整块只读共享”:fork 后未写的物理页共享,重定位与 fork 前入口点修正会弄脏私有副本;这解释了下文“同一镜像、不同进程可写状态不同”的现象 [S07]。
  4. Runtime::Start 与最初的 Java 执行:Runtime::Start 依次做 root clinits、注册运行期 native、按条件创建 JIT(:1149-1150);Thread::FinishStartup 创建 main 线程 peer 并执行 root clinits,这是运行时内部最早的 Java 方法执行 [S05]。
  5. Zygote 的 Java 层(注意函数作用域):ZygoteInit.main(ZygoteInit.java:824)解析参数;命中 "start-system-server".equals(argv[i])(:856)时置位 startSystemServer;非懒预加载分支在 :889-896 调 preload(),随后 gcAndFinalize()、Zygote.initNativeState、new ZygoteServer(:909)、forkSystemServer(:912),父进程进入 runSelectLoop(:926)。另一函数 zygoteInit(:983)是子进程初始化入口,其中 :993 调 nativeZygoteInit()(声明 :1008);其 JNI 实现把调用转给 AppRuntime::onZygoteInit(core/jni/AndroidRuntime.cpp:284-287、注册 :311-315),后者 ProcessState::startThreadPool() 启动 Binder 线程池(cmds/app_process/app_main.cpp:92-97)。childZygoteInit(:1003)明确跳过这条会启动 Binder threadpool 的初始化。因此不能把 :993 写成 main 末尾进入 fork 循环的调用 [S23][S24][S48]。
  6. fork 前后:ZygoteHooks.preFork()(ZygoteHooks.java:198)→ nativePreFork()(:202)→ ZygoteHooks_nativePreFork(dalvik_system_ZygoteHooks.cc:255)→ Runtime::PreZygoteFork()(runtime.cc:869):JIT pre-fork(:871)、等待单线程化、首次 fork 前修正已初始化类的 ArtMethod 入口点、heap_->PreZygoteFork()(:912)。子进程经 postForkChild(ZygoteHooks.java:230)→ nativePostForkChild(:294)→ Runtime::InitNonZygoteOrPostFork(runtime.cc:1265)。因此“案例调用会落到哪个入口”在 fork 前后也可能不同(入口点可被 JIT/配置改写)[S25][S26][S05]。
  7. 调用点本身:ZygoteInit.main 里的 invoke-virtual 指向 String.equals(Object)。下一步不是直接“看 ArtMethod 入口”,而是先判断这个 invoke 是否被编译器内建——见 4.1.4。

4.1.4 两条正交的分派轴:调用点内建 vs callee 入口

两条分派轴与它们的组合见图 2。读图要点:轴 1 只看“这次 invoke 是否被编译期内建”,轴 2 只看“callee 的 ArtMethod 入口实际指向哪里”;“否”分支并不自动等于解释执行,callee 可能已有 AOT/JIT 代码;只有 callee 未编译且落入 nterp 时,才会执行 Java 循环并逐轮产生两处 charAt 调用(AOT intrinsic 方法体或 GenericJNI 回退)。

2026-10-20T01:53:56.144768 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/
2026-10-20T01:53:58.085935 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

手机可横向滑动查看本图

图 2:调用点与 callee 的两条分派轴

图 2: 调用点内建与 callee 入口的正交关系(来源:ART a17r 的 intrinsics_list.h/instruction_builder.cc/intrinsics_riscv64.cc 与 image_writer.cc,[S12][S13][S14][S20],本项目绘制)。nterp 分支的 charAt 目标按 callee 的 quick entry 分两种:有 boot-image AOT intrinsic 方法体时落到该方法体;没有时才是 GenericJNI 回退。FastNative 不转 kNative;若走 JNI stub/GenericJNI,LRT 仍建立/弹出。

“调用点被内建”“callee 有编译代码”“callee 正在解释”是三个不同的问题,不能互相推导。

轴 1:调用点是否被内建(caller 侧)

若包含该调用的方法被优化编译器编译,且该 invoke 在编译期被识别为 kStringEquals(runtime/intrinsics_list.h:214,签名精确匹配),编译器在调用点直接生成内建序列(riscv64 为 VisitStringEquals,intrinsics_riscv64.cc:1152 起),不会经过 String.equals 的 ArtMethod 入口。若编译器未能识别(方法未解析、版本/选项不启用该内建等),该调用按普通 invoke 生成,转轴 2。轴 1 的结论只与“caller 的编译产物 + 本次 invoke 的识别结果”有关,与 callee 本身是否已编译无关。

轴 2:callee 的入口点(被调用方)

若调用点没有被内建(调用者未编译,或编译了但该 invoke 未内建),执行时读取 callee 的 ArtMethod 入口点,可能是:AOT quick code(boot image 中已编译)、JIT quick code(运行期编译完成、入口已替换)、nterp 入口(未编译且允许解释)、quick-to-interpreter bridge / switch 解释器(-Xint、nterp 不可用等)。

callee 若是已编译的 String.equals,不能无条件说它就是“Java 循环 + charAt”:① String.equals 是非 native 的 kStringEquals intrinsic,非 native Compile 在 method->IsIntrinsic() 时会尝试 TryCompileIntrinsic()(optimizing_compiler.cc:1142-1154;instruction_builder.cc:494-525 在无 simple IR 时仍可构造自 invoke;intrinsics_riscv64.cc:1141-1149 的 StringEquals 走 CreateNoCall),若通过 leaf 门(:1079-1084)可生成整方法 intrinsic body,此时不执行 Java 循环;②若走普通 DEX graph 编译,方法体才是 libcore 的 Java 循环,charAt 调用点可被 kStringCharAt 内建为 HArrayGet(instruction_builder.cc:2125、:2225;简化器 instruction_simplifier.cc:2859),循环内没有对 callee 的调用;③callee 若在 nterp 中执行,则按 libcore 的 Java 循环逐条解释;循环每轮仍有两处 charAt 调用(String.java:1675-1682),每处都是普通 invoke。nterp 在本 pin 没有运行期内建(invoke.S 的专门路径只处理 String.<init>,见 :119-130、:444),因此按 callee 的 quick entry 离开 nterp。String.charAt 虽声明为 @FastNative native 方法,但它在 intrinsics_list.h:112 登记为 kStringCharAt;native charAt 的 boot-image 编译走 OptimizingCompiler::JniCompile(:1235),其 intrinsic 路径在 optimizing_compiler.cc:1244-1277(TryCompileIntrinsic() 调用在 :1269),可生成真正的 AOT 方法体,而 image_writer.cc:1488-1495 明确不为 intrinsic native 方法写 JNI stub。因此,若该 boot image 已为它生成 AOT intrinsic 方法体,callee quick entry 就是编译后的方法体(普通 managed quick 调用,做 count/bounds/字符加载),不是 JNI 入口;thread.cc:4368-4381 也把“AOT-compiled String.charAt”与 GenericJni 路径区分开。只有当该方法没有 AOT intrinsic 代码(quick_code == nullptr,例如非 boot image、过滤器未覆盖、编译失败或产物未包含)时,image_writer.cc:3530-3534 才回退到 GenericJNI trampoline,再由 dlsym lookup 找到 String_charAt(4.8.3)。不能断言标准镜像必然已 AOT:具体产品是否编译了该方法体取决于实际 boot image/profile/过滤器/产物,本报告未测,故两轴表把它写成条件分支。若确实走到 JNI stub/GenericJNI,@FastNative 不转 kNative、不调 artJniMethodStart,但 LRT 仍按 !is_critical_native 的门建立/弹出(jni_compiler.cc:285-305、:519-524)[S19][S43]。

组合表(对“这次调用”逐个判定)
caller 状态本次 invoke 内建?实际执行说明
已编译是调用点内联 kStringEquals 序列不进入 callee 入口;不执行 Java 循环
已编译否(未识别/禁用)普通 invoke → callee 入口callee 可能 AOT 整方法 intrinsic body、普通 DEX graph 编译、JIT/nterp;具体 boot image 是否生成 body 未实测
未编译(nterp/bridge)不适用(解释器无编译器内建)callee 入口 → 若 callee 已编译则 quick code;若未编译则 nterp 执行 Java 循环caller 未编译 ≠ callee 未编译
未编译不适用callee 在 nterp:每轮两处 charAt → callee quick entry(AOT intrinsic 方法体,或 quick_code==nullptr 时的 GenericJNI 回退)每轮两次调用;不必然是 JNI 帧/LRT/C++;是否 AOT 取决于产物

内建序列本身(当轴 1 为“是”时):参数空检查(Beqz)→ 引用相等(Beq)→ klass_ 比较(两次 Loadwu,依赖 String 类对象不可移动,无读屏障)→ count 比较(Loadwu)→ 压缩标志/字符数换算 → 从 value_offset 起以 8 字节 Ld 批量比较(压缩时每轮 8 字节,非压缩时每轮 4 个字符)→ 返回 0/1(intrinsics_riscv64.cc:1152-1240;存储布局见 mirror/string.h:316-326)[S12][S16]。复杂度:比较长度相等时最坏需要扫描整串,仍是 O(n);8 字节批处理降低的是常数因子,不改变最坏复杂度。

count 比较为什么成立(内部不变量,不是 Java 语义):Java 的 String.equals 语义是“字符序列相等”,编码是实现细节。本 pin 能先比较 count(长度<<1 | 压缩标志)再比字节,依赖构造时的压缩规范化:例如 String::AllocFromModifiedUtf8 以 utf16_length == utf8_length 判 compressible(string.cc:254),其它构造/替换路径也按内容计算 compressible(string.cc:90、:129、:194、:217),并由 GetFlaggedCount 写入标志位(string.h:271-280)。因此同一字符序列必然得到同一 coder:长度相同但 coder 不同的两串不可能字符序列相等。RV64 内建的 count 比较(含 compression flag)正是利用这一内部不变量;不能把它写成“Java 规定编码不同返回 false”——这是实现层优化,若字符串实现允许非规范化表示,该比较就不再成立 [S16]。

4.1.5 失败与回退

4.1.6 观察方法与后续导览

成本与权衡(案例口径):内建路径把比较变成调用点内的批量 load/比较,最坏仍 O(n),但省去方法调用与循环解释开销;非内建且 callee 未编译时,每轮两处 charAt 是两次 quick 调用。若目标已有 AOT intrinsic 方法体,成本是普通 quick 帧、count/bounds 检查与字节加载;只有回退到 GenericJNI 时才有 JNI 帧、参数搬运、LRT 压/弹与返回检查。不能把“解释态 charAt”一概记成 JNI 边界。选择哪条由轴 1/轴 2 共同决定,不是“架构优劣”;代价是编译需要热度/profile/过滤器与代码缓存,且产物增加代码尺寸与构建时间。成本测量口径见 4.10,未测量不写数字 [S12][S28][S43]。

可复现(未执行)的最小观察集:oatdump --oat-file=<boot.oat> 看 String.equals 是否有 AOT 代码及其入口;logcat 过滤 JIT 日志看编译发生;profman --dump-info-for=<profile> 看该方法/其调用者是否进入 profile。设备前提与命令细节见 4.9。

后续:4.2 启动/产物/类加载;4.3 nterp(帧、invoke 层级、native 转换);4.4 编译管线(profile/热度/JIT/AOT/IR/寄存器/stack map/OSR/去优化);4.5 RV64 后端(ABI/指令选择/内建/RVV);4.6 GC;4.7 线程与内存序;4.8 JNI/native;4.9 演进与支持分层;4.10 优化地图。

第 2 章(4.2)运行时建立与产物加载:zygote、boot image、DEX/OAT/VDEX 与类加载

本章回答三个问题:zygote fork 前后运行时到底改写了什么;启动时加载的 boot.art/oat/vdex 各自是什么、校验什么;一个类从 DEX 字节到“可执行入口点”经过哪些状态。所有行号以 ART android17-release b753cf97、libcore/frameworks 各自 android17-release pin 为准。本报告未运行设备,本章所有观察均为源码确证;命令均标注为可复现未执行。

4.2.1 fork 模型与对象所有权

zygote 的核心假设是“先把运行时和常用类一次性准备好,再 fork 共享”。这要求 fork 前把多线程状态收拢为单线程、把会弄脏共享页的工作提前做掉,fork 后只为子进程重建必要的线程与状态。

fork 前(父进程):libcore 的 ZygoteHooks.preFork()(ZygoteHooks.java:198)调用 nativePreFork()(:202),进入 ART 的 ZygoteHooks_nativePreFork(runtime/native/dalvik_system_ZygoteHooks.cc:255),再到 Runtime::PreZygoteFork()(runtime/runtime.cc:869)。按源码顺序,它做四件事:

  1. Jit::PreZygoteFork()(:871)——通知 JIT 进入 fork 前的安全状态;
  2. 收紧线程列表:等待其他线程完成 Unregister,并 WaitUntilSingleThreaded()(:876-900 区段),这是“fork 只能发生在单线程进程”的运行时侧保证;
  3. 首次 fork 时修正所有已初始化类的 ArtMethod 入口点:MakeInitializedClassesVisiblyInitialized + UpdateMethodsPreFirstForkVisitor(:902-911)——把类初始化完成后的入口点(trampoline → 真实代码/nterp)在共享页上一次性写好,避免每个子进程各自弄脏;
  4. heap_->PreZygoteFork()(:912)与 PreZygoteForkNativeBridge()(:913)——堆与 native bridge 的 fork 前收尾 [S05][S25][S26]。

fork 后(子进程):ZygoteHooks.postForkChild(ZygoteHooks.java:230)→ ZygoteHooks_nativePostForkChild(dalvik_system_ZygoteHooks.cc:294)→ Runtime::InitNonZygoteOrPostFork(runtime/runtime.cc:1265)。关键动作包括:native bridge 的卸载/初始化(:1273-1284);child-zygote 的提前返回(只初始化 native bridge,其余交给 Java 侧,:1286-1293);system_server 的应用信息注册与 profile 配置(:1297-1330 区段);按属性 dalvik.vm.parallel-image-loading 创建最多 4 个 runtime 线程用于并行加载 app image(:1327-1350);重置 GC 性能数据与 metrics(:1321-1322);StartSignalCatcher()(:1364);perfetto hprof 插件、odrefresh 统计上传(system_server)、JNI id 类型选择(debuggable=indices,否则 pointer,:1393-1397)以及最后启动调试器(:1407-1409)[S05][S25]。

对象所有权(谁活多久、谁持有谁):

对象所有者/生命周期与案例的关系
gc::HeapRuntime 持有;boot image 的 ImageSpace 由 Heap 持有(heap.cc:520 起)字符串对象与内联字符数据都在 Heap 的空间里;镜像为 RW+MAP_PRIVATE 映射,fork 后未写页共享物理页
ClassLinkerRuntime 持有;持有 class_table/class_roots/boot_class_pathString 类与 String.equals/charAt 的 ArtMethod 由它链接
ThreadListRuntime 持有;每线程 Thread 持有 JNI/quick 入口点表安全点/挂起以线程为粒度;fork 要求单线程
Jit/JitCodeCache条件创建(Runtime::Start :1149-1150);zygote 共享映射 + 私有区编译后的 String.equals 调用点可能来自 JIT code cache
boot image 的 ImageSpacefork 后共享同一 COW 映射(未写页共享物理页)镜像内的入口点被 fork 前修正,子进程直接复用

4.2.2 Runtime::Init 的顺序与 boot image 的装载/校验

启动装载与校验的整体关系见图 3。读图要点:磁盘上的四类产物(DEX/VDEX/OAT/ART image)各司其职;装载时先做镜像/oat/boot classpath 三重校验,再重定位并交给 Heap/ClassLinker;失败有三条分支(内存编译、跳过扩展 chunk、无镜像路径)。图下方的“案例落点”提醒:类在镜像里不等于方法已编译。

2026-10-20T01:16:39.680137 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/
2026-10-20T01:16:42.315091 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

手机可横向滑动查看本图

图 3:启动时加载什么、校验什么

图 3: 启动装载与校验关系(来源:ART a17r 的 heap.cc/image_space.cc/class_linker.cc,[S07][S08][S32],本项目绘制)。校验通过走“使用 boot image”分支;失败按 checksum/BCP/版本、扩展 chunk、无镜像三类进入不同兜底,不是同一条必经直线,也不保证所有失败都能成功启动。

Runtime::Init(runtime/runtime.cc:1625)的组件顺序在 4.1.3 已给出骨架;这里补“装载与校验”的细节,因为它决定了案例方法在启动后处于什么执行状态。

  1. 镜像发现与映射:boot classpath 与镜像位置来自 -Xbootclasspath/-Ximage(framework 侧由 dalvik.vm.boot-image 映射,AndroidRuntime.cpp:783-784)[S24];Heap 构造时调用 ImageSpace::LoadBootImage(image_space.cc:3016),BootImageLayout::Load 先校验镜像位置与 boot classpath 形状,再逐组件 ReadHeader → ValidateOatFile(image_space.cc:1791 起)。
  2. 三类失败与兜底:镜像版本/oat checksum/boot classpath 任一不匹配,ValidateOatFile 报 "ValidateOatFile found checksum mismatch … (0x%x != 0x%x)" 一类错误;主镜像失败且允许内存编译(zygote 默认)时走 CompileBootclasspathElements 在内存里重建完整 boot image,否则整体失败;扩展镜像失败则跳过该 chunk(image_space.cc:2140-2204 区段)。镜像格式版本在 a17r 为 119(runtime/oat/image.cc:39),a16q2 为 118——跨版本互不兼容 [S07][S08]。
  3. 入口点的两次改写:镜像生成时,未编译的 Java 方法写 nterp trampoline(需要 clinit 的写 resolution trampoline);非 intrinsic 的 native 方法在 boot image 中写 JNI stub(image_writer.cc:1488-1495),而 intrinsic native 方法(如 String.charAt)经 OptimizingCompiler::JniCompile 的 intrinsic 路径生成 AOT 方法体(optimizing_compiler.cc:1244-1277,TryCompileIntrinsic 调用 :1269),只有 quick_code == nullptr 时才回退 GenericJNI trampoline(image_writer.cc:3530-3534)。镜像加载时 ClassLinker::AddImageSpace 把 nterp trampoline 原地改成 interpreter::GetNterpEntryPoint()(class_linker.cc:2332-2342,条件为 CanRuntimeUseNterp()),否则改回 quick-to-interpreter bridge。若根本无镜像(InitWithoutImage,class_linker.cc:699;调用点 runtime.cc:2173),则直接以 nterp 入口自建 trampoline(:914)[S08][S20]。
  4. 首个 Java 执行:Runtime::Start → Thread::FinishStartup → RunRootClinits(4.1.3 的链条)。此处的类初始化是“真实 Java 代码第一次跑”的位置,String 等核心类的状态机也从这里开始进入 kInitialized/kVisiblyInitialized [S05]。

4.2.3 DEX、VDEX、OAT、ART image 的当前关系(含一处重要纠错)

四种产物的职责经常被写混。按 a17r 源码,准确关系是:

a17r 的 VDEX 不承载 quickening。 在当前 pin 中:

// runtime/vdex_file.h:80-86(a17r)
enum VdexSection : uint32_t {
  kChecksumSection = 0,
  kDexFileSection = 1,
  kVerifierDepsSection = 2,
  kTypeLookupTableSection = 3,
  kNumberOfSections = 4,
};

没有任何 quickening section;解释器目录也没有对 quickened 指令的引用;dex2oat 的过滤器名 quicken 只剩兼容告警——libartbase/base/compiler_filter.cc:181-184 明确打印 “'quicken' is an obsolete compiler filter name … please use 'verify' instead”,并映射到 kVerify。历史注释里仍能看到 “unquickened dex” 字样(如 oat_file_manager.h:86),但它们不是现行数据通路 [S32][S36][S39]。

这个纠错直接影响两个常被误传的结论:其一,“解释器靠 vdex 里的 quickened 指令加速”在 a17r 不成立——nterp 用标准 DEX + 解释器缓存/内联缓存执行;其二,“vdex 只用于快速验证”也不完整——它同时保存了原始 DEX 本体(kDexFileSection),运行期类加载从 vdex/oat 打开的就是这份 DEX。VdexFile 对外提供 GetNumberOfDexFiles/GetDexChecksumAt/GetNextDexFileData 等访问器(vdex_file.h:167-273),镜像/oat 校验时用 checksum 与真实 DEX 对齐 [S32]。

4.2.4 类加载与状态机:从 FindClass 到可执行入口点

类加载的主链(runtime/class_linker.cc;行号已按 a17r 复核):

FindClass(:3266)→ LookupClass 命中则 EnsureResolved(:3296),未命中 DefineClass(:3505)→ LoadClass(:4491,成员加载由 LoadClassHelper 完成)→ LoadSuperAndInterfaces(:3642)→ LinkClass(:6646:LinkSuperClass/LinkMethods/字段链接,LinkMethodsHelper 构建 vtable/IMT,成功后 SetStatus(kResolved) :6733)→ 首次主动使用时 InitializeClass(:5938)。状态机在 runtime/class_status.h:111-129:kLoaded → kResolved → kVerified → kInitializing → kInitialized → kVisiblyInitialized。EnsureResolved 的等待协议是“自旋 1000 次 + 1ms sleep + ObjectTryLock 防死锁”,并检测类初始化循环依赖(ThrowClassCircularityError,:2937-2939)[S08][S39]。

InitializeClass 的并发协议(JLS 12.4.2):快路径无锁判断已初始化(:5947)→ ObjectLock<Class> 下重查 → 未验证先 VerifyClass(:5980)→ 状态为 kInitializing 且非本线程则 WaitForInitializeClass(:6037)→ 置 ClinitThreadId、kInitializing(:6062-6063)→ 递归初始化父类与默认方法接口 → 执行 <clinit> → kInitialized/kVisiblyInitialized(可见性由全线程 checkpoint 保证,注释见 class_status.h:102-110)[S08][S39]。

验证器的软失败会改变“何时可编译”:方法校验的 kSoftFailure/kTypeChecksFailure 在 AOT 下映射为 kRetryVerificationAtRuntime(运行期重验、方法不可编译)或 kVerifiedNeedsAccessChecks(class_linker.cc:5419-5425);这正是“某些方法明明有 AOT 机会却只在运行期验证”的机制来源。分派则在链接期定表:虚调用经 vtable 入口(mirror/class-inl.h:531 的 FindVirtualMethodForVirtualOrInterface),接口调用走 iftable 逻辑;未解析调用由 art_quick_resolution_trampoline(riscv64 汇编 quick_entrypoints_riscv64.S:872,C++ 端 quick_trampoline_entrypoints.cc:1205)解析后跳转。异常语义在 ArtMethod::FindCatchBlock(art_method.cc:328)按 try item 匹配,编译代码的异常分派走 QuickExceptionHandler(quick_exception_handler.cc:146)[S08][S39]。

4.2.5 案例落点:String.equals 在启动后的真实状态

结合以上:String 类属于 boot classpath,其 ArtMethod 在镜像中已存在;String.equals 是否有 AOT 代码取决于 boot image 的编译配置(过滤器与 profile),没有 AOT 时其入口是 nterp(若允许)或 quick-to-interpreter bridge;String.charAt 是 native 方法,但也是 kStringCharAt intrinsic:若 boot image 为它生成了 AOT intrinsic 方法体,quick entry 指向该方法体;否则才回退 GenericJNI(image_writer.cc:3530-3534)。因此案例在启动后的第一次调用可能落在任一入口上,不能从“类在 boot image 里”推断“方法已编译/未编译”;具体要看该 boot image 的过滤器/profile/产物 [S08][S20]。fork 之后子进程复用的是同一份 COW 镜像映射,但入口点是否被 JIT 替换、nterp 是否可用(debuggable/-Xint)因进程配置而异——这就是 4.1 图 1 中分支判断的实际来源。

4.2.6 失败、回退与观察

后续第 3 章进入解释器。

成本与权衡:镜像校验换启动安全(版本/checksum/BCP 三重比对),失败时的内存编译兜底换正确性但会显著拉长首次启动;vdex 保存原始 DEX 换“打开时无需全量重算”,代价是磁盘占用;fork 前把入口点与堆状态修好,换子进程共享 COW 页(未写页不复制)、避免每进程重复准备。每一项都是“启动时间/内存/安全”的取舍,具体数值需设备测量(未做)。

String.equals 在未编译时如何一字节一字节执行、charAt 如何转入 native;第 4 章进入编译流水线:这段代码如何被 profile 选中、如何生成 RV64 的 8 字节比较。

第 3 章(4.3)解释执行:nterp 的帧、分派、invoke 与 native 边界

本章回答:String.equals 未被编译时,nterp 如何执行它;一次 charAt 调用如何从解释器离开、落到 intrinsic quick 方法体或 GenericJNI 回退再回来;解释器在什么条件下根本不启用。行号均以 ART android17-release b753cf97 为准;未运行设备。

4.3.1 解释器的现状:只有一个快解释器

公开 ART 中的经典 mterp 已于 2021 年被整体删除(提交 e4f983c808 "Remove mterp.",Nicolas Geoffray;该提交不在 android12 分支、在 android13 及以后)[S41]。当前解释执行只有两种形态:

nterp 的启用判定是两层:IsNterpSupported()(runtime/interpreter/mterp/nterp.cc:38-59)在配置层判断——riscv64 分支直接 return true,arm/arm64 还要求 kReserveMarkingRegister && !kUseTableLookupReadBarrier;CanRuntimeUseNterp()(:61-75)在进程层判断——非 debuggable、无 entry/exit stubs、非 InterpretOnly、非 AOT 编译器、无异步异常待决、JIT 非 JitAtFirstUse。任一不满足就回退 switch 解释器。方法级还有 CanMethodUseNterp():native 方法、非 invokable、需要计数锁(MustCountLocks)、代理方法、以及帧尺寸超过 kNterpMaxFrame 的方法都不能用 nterp(runtime/nterp_helpers.cc:225-257 区段)[S18][S40]。

4.3.2 帧布局:一种“quick 栈帧 + 双数组”的混合结构

nterp 帧布局与 invoke 层级见图 4。读图要点:frame(ArtMethod**,即新 sp)指向低地址的 method 槽,向上依次是 out 参数、saved dex pc、previous frame、引用影子数组、dex 寄存器数组;callee-save 区在更高的地址,由序言先 INCREASE_FRAME NTERP_SIZE_SAVE_CALLEE_SAVES 留出。charAt 这类 @FastNative 调用从解释器离开时按 callee quick entry 走第②/③/④层:有 boot-image AOT intrinsic 方法体时落到该方法体(普通 quick 调用);没有时才是 GenericJNI 回退(此时才有 JNI 帧/LRT)。

2026-10-20T01:53:56.645616 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/
2026-10-20T01:53:58.289333 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

手机可横向滑动查看本图

图 4:nterp 帧布局与 invoke 四层路径

图 4: nterp 帧与调用路径(来源:ART a17r 的 nterp_helpers.cc、mterp/riscv64/main.S/invoke.S,[S19][S40],本项目绘制)。帧内低地址是 method/out/dex pc/prev/双数组,callee-save 区在更高地址;invoke 第②层是 Nterp→managed ABI,不是 JNI native ABI。charAt 的 quick 目标可为 AOT intrinsic 方法体,也可在无代码时回退 GenericJNI。

nterp 的帧由两段组成:低地址是与 quick 调用约定兼容的解释器帧(method/out/dex pc/prev/双数组),高地址是 callee-save 区。NterpGetFrameSizeWithoutPadding(runtime/nterp_helpers.cc:153-171)给出的是总大小(加法与地址方向无关):

总大小 = callee-save frame entry(NTERP_SIZE_SAVE_CALLEE_SAVES=192B)
       + num_regs*4 (dex 寄存器数组) + num_regs*4 (引用影子数组)
       + 8 (previous frame) + 8 (saved dex pc)
       + out_regs*4 (传出参数) + 8 (method)

地址从低到高则是:

frame/sp → method* (8B)
           out 参数 (out_regs*4,按指针大小向上对齐)
           saved dex pc (8B)
           previous frame (8B)
           refs 数组 (num_regs*4)
           regs 数组 (num_regs*4)
           callee-save 区 (192B,位于更高地址)

对应到 riscv64 汇编(runtime/interpreter/mterp/riscv64/main.S:422 起的 ExecuteNterpImpl 序言;invoke 路径 invoke.S:1374-1431):先 INCREASE_FRAME 留出 callee-save 区并保存,再从当前 sp 向低地址减去解释器帧大小;sd a0,(sp) 把 ArtMethod* 放在新帧最低槽,xREFS 指向引用影子数组、xFP 指向 dex 值数组、xPC 是解释器的 dex 指令指针、xINST 是当前 16 位指令、xIBASE 是 handler 表基址;s7=refs/fp vreg 数、s8=ins 数、s9=spills 后、帧前 sp。CFI_DEFINE_DEX_PC_WITH_OFFSET 把 dex pc 暴露给栈展开器——调试与异常回溯依赖它 [S19][S40]。

读取函数与上述地址顺序一一对应:NterpGetReferenceArray = frame + 8(method) + RoundUp(out_regs*4,8) + 8(dex pc) + 8(prev frame);NterpGetRegistersArray 在引用数组之上(+ num_regs*4);NterpGetDexPC 从保存的 dex pc 槽减去指令基址(nterp_helpers.cc:189-212)。NterpFrameInfo 把同一布局换算成 QuickMethodFrameInfo,告知 GC/栈展开器需要保存哪些 callee-save 寄存器 [S40]。

进入 nterp 的序言有三条快路径(main.S:432-470):零参数;全引用参数(检查 ART_METHOD_NTERP_ENTRY_POINT_FAST_PATH_FLAG_BIT 后把 a1.. 直接写入 xFP/xREFS);实例方法零参数。当这些快路径不适用时才走慢路径:SPILL_ALL_ARGUMENTS → call NterpGetShorty → 按 shorty 把参数从机器寄存器搬进 dex 寄存器数组/引用数组。慢路径的成本只落在未命中快路径的调用上,不能写成“每次调用都走 shorty 慢路径” [S19][S40]。

4.3.3 分派与解析缓存

nterp 用 computed-goto:每个 opcode 的 handler 位于 artNterpAsmInstructionStart + opcode*步长,handler 末尾 FETCH_INST → GET_INST_OPCODE → GOTO_OPCODE(main.S:680 起的模板;riscv64 单套 handler 表,arm/arm64 为四套 16 KiB 对齐副本,见 CheckNterpAsmConstants)。解析结果用解释器缓存加速:InterpreterCache 是 256 项直接映射表(runtime/interpreter/interpreter_cache.h:54,索引为 (dex_pc_ptr >> kKeyLowBit) & 255),按 dex 指令指针缓存方法、字段偏移、类、字符串等解析结果。慢路径 C++ 函数在 nterp 代码结尾以 trampoline 暴露:nterp_get_method、nterp_get_static_field、nterp_get_instance_field_offset、nterp_get_class、nterp_allocate_object、nterp_load_object 等(main.S:661-672 的 NTERP_TRAMPOLINE 列表),对应 NterpGetMethod/NterpGetStaticField 等实现。慢路径解析会通过 UpdateHotness 给方法 +0xf 热度 [S18][S19][S40]。

4.3.4 invoke 的四个层级:什么时候留在 nterp,什么时候离开

riscv64 的 invoke.S 把调用分成清晰的层级(源码头注释 :10-33)[S19]:

  1. nterp-to-nterp 快路径:若目标方法的 quick code 指针等于 ExecuteNterpImpl(try_nterp 宏,:670 起用 lla 比较),且方法带 ART_METHOD_NTERP_INVOKE_FAST_PATH_FLAG_BIT,则直接在 vreg 之间搬运参数、建立新 nterp 帧、跳到目标方法第一条指令的 handler。nterp_invoke_* 系列与 NterpToNterp* 标签(main.S:640-653)即此路径。这条路径没有 native 调用、没有 shorty 解析。
  2. 全引用参数(Nterp → managed quick code):目标不是 nterp,但参数全是对象引用时,按 managed ABI 布置——A0=ArtMethod*、实参从 A1 起,用 LWU 把每个 vreg 装进相应机器参数寄存器后 jalr 目标 quick code。这里不是 JNI native ABI;若目标 quick code 恰好是 GenericJNI(无 AOT intrinsic/编译代码的 native 回退),再由它把 managed 参数搬到 JNIEnv*/jobject 的 JNI 位置。
  3. 零/单参数:窥探下一 opcode 判断返回值类型,必要时只解析 shorty;标量返回要处理 fa0 → a0 的搬运。
  4. 完整 shorty 路径:多参数混合 float/double/long、或 invoke-polymorphic/custom 时,按 shorty 做完整的 managed ABI 参数布置,再 jalr quick code;polymorphic/custom 分别转 art_quick_invoke_polymorphic / art_quick_invoke_custom(:579、:596)。

这正是案例的执行细节:解释态下 String.equals 的循环每轮仍有两处 charAt 调用(String.java:1675-1682),每处都属于第②/③/④层——callee charAt 声明为 @FastNative native,但也是 kStringCharAt intrinsic;若 boot image 为它生成了 AOT intrinsic 方法体,quick code 就是这段编译方法体(count/bounds/字节加载,普通 quick 帧),不是 GenericJNI;只有在没有 AOT intrinsic 代码时,image_writer.cc:3530-3534 才回退 GenericJNI,建立 JNI 帧并压 LRT(FastNative 只跳过状态转换与 artJniMethodStart,不跳过 LRT)。nterp 先按 managed ABI 把 receiver (String*) 与 index 放到 quick code 参数位置(A0=ArtMethod*、实参从 A1);若落到 GenericJNI,后者再按 JNI 约定把它转成 JNIEnv*、jobject/jclass 与 native 参数。返回后再把结果放回 dex 寄存器(char 为 32 位整数,走 a0)。两个 ABI 段不能合并成“寄存器布局恰好一致”,也不能把“可能走 GenericJNI”写成“必然走 GenericJNI” [S17][S19][S20][S43]。

4.3.5 异常、挂起与热度

4.3.6 失败与回退

4.3.7 小结:解释器的可复述模型

对一次未编译的 String.equals:入口 → nterp 序言(快路径或短慢路径)→ 循环 opcode(iget/invoke-virtual/if-*)→ charAt 的 invoke 按四层规则离开 nterp,落到 AOT intrinsic 方法体或 GenericJNI 回退 → 返回后继续循环 → 计数归零触发 NterpHotMethod,可能 OSR 进 JIT。下一章回答:编译侧如何把同一个循环变成 RV64 的内联比较。

第 4 章(4.4)从 profile 到机器码:JIT/AOT 编译流水线

本章回答:String.equals 这段代码“什么时候、被谁、用什么规则”编译;同一个方法在不同构建/运行模式下的入口为何不同;IR、寄存器分配、stack map 与去优化如何串起来。行号均以 ART android17-release b753cf97 为准;阈值只按 pin 与条件陈述,不把默认值当设备实际值。

4.4.1 两条编译入口与编译过滤器

ART 有两条编译路径,共用优化编译器的后半段:

编译过滤器(libartbase/base/compiler_filter.h:34 起,8 档:kAssumeVerified/kVerify/kSpaceProfile/kSpace/kSpeedProfile/kSpeed/kEverythingProfile/kEverything)决定 AOT 覆盖范围:默认 kSpeed;dex2oat 检测到 profile 时把 kSpeed 升为 kSpeedProfile(dex2oat.cc:1217-1222);超大应用降级 kVerify(:129-130、:1562-1575)。设备侧主镜像的过滤器来自 odrefresh:a17r 中主镜像与 mainline 扩展的默认值都是 speed-profile(odrefresh/odrefresh.cc:130、:133;无 profile 时回退 verify)。过滤器名 quicken 只保留兼容:解析时告警并映射为 kVerify(compiler_filter.cc:181-184)——与 4.2 的“VDEX 无 quickening”一致 [S32][S36]。

4.4.2 热度、tiering 与执行路径

层级与回退关系见图 5。读图要点:解释器用方法入口/回边热度与解析慢路径累积信号;触发后进入 1 个 worker 的 JIT 队列,任务类型由“能否分配 ProfilingInfo/是否 native”决定;运行中的去优化会把执行按 stack map 拉回解释器。

2026-10-20T01:16:40.094237 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/
2026-10-20T01:16:42.503551 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

手机可横向滑动查看本图

图 5:热度、tiering 与回退

图 5: 编译分层与回退(来源:ART a17r 的 jit.cc/jit_options.cc/quick_exception_handler.cc,[S28][S29][S38],本项目绘制)。-Xint 强制解释;debuggable 禁 nterp/AOT Java 但 JIT 仍可用,二者不能混成一行。

阈值:ART 内的 JIT 默认阈值是 kJitDefaultWarmupThreshold = 0xffff、kJitDefaultOptimizeThreshold = 0xffff(runtime/jit/jit_options.cc:34、:27);设备实际值由框架属性下发(AndroidRuntime.cpp:897 把 dalvik.vm.usejit 映射为 -Xusejit:、:900 把 dalvik.vm.jitthreshold 映射为 -Xjitthreshold:;warmup 阈值的属性映射不在此文件,按“由框架/配置层决定”处理,不臆测具体数值)。-Xjitosrthreshold 已列入忽略参数(runtime/parsed_options.cc:514)——OSR 没有独立阈值。

触发入口:nterp 的方法入口与回边递减方法计数器(4.3.5);缓存 miss 的解析慢路径通过 UpdateHotness 加 0xf;计数器归零进 NterpHotMethod(nterp.cc:995 起)→ Jit::MaybeEnqueueCompilation。编译任务类型的决策原文(jit.cc:1819-1824):

if (!method->IsNative() && GetCodeCache()->CanAllocateProfilingInfo()) {
  AddCompileTask(self, method,
                 UseFastCompiler() ? CompilationKind::kFast : CompilationKind::kBaseline);
} else {
  AddCompileTask(self, method, CompilationKind::kOptimized);
}

也就是说:能分配 ProfilingInfo 的普通方法先进 baseline(arm64 且满足条件时是 fast),否则直接 optimized;fast 失败会回退 baseline。对 riscv64:TryPatternMatch(jit.cc:138-153)服务 arm/arm64,SupportsFastCompiler(jit.cc:1262-1266)仅 arm64;riscv64 只能走 baseline→optimized 的通用阶梯 [S28][S38]。

AOT/JIT/解释三态的选择:镜像加载后方法入口可能是 AOT quick code、nterp 入口或 quick-to-interpreter bridge(4.2.2);JIT 编译完成由 Instrumentation::UpdateMethodsCode 替换入口。-Xint(InterpretOnly)强制 Java 方法走 switch 解释器并停用 nterp;debuggable 使 nterp 与 AOT Java 代码不可用、fast tier 被拒,但 JIT 仍可生成带 entry/exit hooks 的优化代码(instrumentation.cc:232-238;optimizing_compiler.cc:1440-1474 只拒 debuggable 的 fast tier,CriticalNative 特例在 :1352-1365;AOT 在 debuggable 下仅 native 方法可用,instrumentation.cc:331-335)。因此“同一个 String.equals”的入口要看构建过滤器、运行属性、-Xint/-Xusejit 与 debuggable/instrumentation 状态,不能只用 debuggable 一位推断“必定解释” [S28][S38]。

4.4.3 IR:从 DEX 到 HGraph

优化编译器入口 OptimizingCompiler::TryCompile(compiler/optimizing/optimizing_compiler.cc:782):HGraphBuilder::BuildGraph(builder.cc:90)把 DEX 字节码降为 SSA 形式的 HGraph;关键 HInstruction 与案例相关的是:

调用点内建:String.equals 调用在 HIR 层被识别为 Intrinsics::kStringEquals(runtime/intrinsics_list.h:214,签名精确匹配),编译器把整个调用替换为前面的 KV 序列(4.1.4),即使不内联方法体。反过来,String.equals 方法体自身被编译时有两种可能:若非 native Compile 对 method->IsIntrinsic() 走 TryCompileIntrinsic() 并生成整方法 intrinsic body(optimizing_compiler.cc:1142-1154、:1079-1084;intrinsics_riscv64.cc:1141-1149),则不执行 Java 循环;只有走普通 DEX graph 编译时,其循环内的 charAt 调用才被替换为上面的 HArrayGet 路径(instruction_builder.cc:2125)。两种“内建”作用在不同调用点上,这是读 IR 时最容易混淆的一处;具体 boot image 是否生成整方法 body 未实测。

4.4.4 优化 passes 与寄存器分配

RunOptimizations(optimizing_compiler.cc:620,内置序列 :644-716)按阶段运行:常量折叠、InstructionSimplifier、DeadCodeElimination;Inliner(其后 $after_inlining 组);GVN、控制流简化;不变量外推、归纳变量、BoundsCheckElimination、循环优化;LoadStoreElimination、CHA guard、EnvironmentInputElimination、CodeSinking;最后是 codegen 前清理。arch 专属 pass 单独挂载:riscv64 为 InstructionSimplifierRiscv64、GVN$after_arch、CriticalNativeAbiFixupRiscv64(同文件 :539 附近)[S31]。

对案例的两个机制点:其一,String.equals 的 8 字节循环是否被向量化与本 pin 无关——riscv64 的向量指令 codegen 尚未实现(4.5),因此只能走标量整字比较;其二,只有走普通 DEX graph 编译、String.equals 方法体中的 charAt 循环存在时,HBoundsCheck 能否被 BCE 消除才取决于归纳变量分析能否证明 i < length();若走整方法 intrinsic body,则不进入该循环;length() 内建读 count 字段,循环上界与数组长度同源,这是 BCE 可能成功的结构基础(是否成功由具体图决定,本文不给未经反汇编的断言)。

寄存器分配只有线性扫描一种实现(register_allocator_linear_scan.cc:AllocateRegisters :393,活跃集满时 TryAllocateFreeReg :1238/AllocateBlockedReg :1470);Location 抽象负责把 HIR 值放到寄存器/栈/常量位。codegen 前 WriteBarrierElimination 与 AllocateRegisters 的顺序见 optimizing_compiler.cc:943-981 [S31]。

4.4.5 stack map、dex pc 与 CFI

编译产物的栈映射在 StackMapStream 中生成:BeginMethod(stack_map_stream.cc:49)→ BeginStackMapEntry/AddDexRegisterEntry → EndMethod(:84)→ Encode(:382),CreateDexRegisterMap(:318)做寄存器映射的差异压缩。运行期读取侧是 runtime/oat/stack_map.h 的 CodeInfo:按 native pc 取栈映射(GetStackMapForNativePcOffset)、按 dex pc 取(GetStackMapForDexPc),并给出寄存器/栈位与 dex 寄存器的对应。异常展开与去优化都消费这份数据(4.4.6)[S29][S31]。

nterp 帧的“dex pc”不走 stack map,而是帧内保存的 saved dex pc 槽(4.3.2)+ CFI 描述;这解释了为什么去优化时 nterp 帧可以直接拷贝(HandleNterpDeoptimization,quick_exception_handler.cc:583),而已编译帧必须按 stack map 重建(HandleOptimizingDeoptimization,:612,其中 :619 取栈映射)[S29][S40]。

4.4.6 OSR 与去优化

4.4.7 同一用例为什么“模式不同性能不同”

把案例放进不同模式,入口与执行体可以完全确定地推导出来:

模式/条件调用点执行体charAt 路径说明
-Xint(InterpretOnly)String.equals 的 Java 循环,switch 解释器每次 charAt 按 callee quick entry:AOT intrinsic 方法体,或 quick_code==nullptr 时的 GenericJNI 回退强制解释;charAt 不必然走 JNI/LRT
仅 nterp(默认未编译)同上,nterp 执行每轮两处 charAt → AOT intrinsic 方法体或 GenericJNI 回退每轮两次调用;是否 JNI 取决于产物,不是固定边界
baseline/optimized(JIT 或 AOT)调用点内建 kStringEquals:8 字节批量比较不再执行 Java 循环见 4.1.4 的 RV64 序列;是否内建仍取决于调用点识别
String.equals 自身被编译——整方法 intrinsic body(条件)StringEquals 的 CreateNoCall 整方法体(非 native Compile/TryCompileIntrinsic,需过 leaf 门)不执行 Java 循环;是否存在取决于该 boot image/编译器产物与调用点内建是两回事;具体是否生成未实测
String.equals 自身被编译——普通 DEX graph方法体 Java 循环 + charAt 内建为 HArrayGet无 native 调用,直接读内联数据仅“普通 DEX graph”这一支;不要无条件写成所有编译都如此
speed/speed-profile AOT取决于该方法是否在过滤器/profile 覆盖内同上过滤器决定“有没有 AOT”
debuggable(无 -Xint)nterp 与 AOT Java 代码不用于执行;可由 JIT 生成带 entry/exit hooks 的代码若在 JIT 代码中则可能被 kStringCharAt 内建;否则解释JIT 仍可用;CriticalNative 是 debuggable 下 JIT 的例外(走 GenericJNI)
debuggable + instrumentation/deopt命中挂钩或强制去优化的帧回到解释器解释态每轮两处 charAt quick 调用(AOT intrinsic 或 GenericJNI 回退)具体入口由 entrypoint 与 instrumentation 状态决定

这张表也说明:不能用“某个方法是不是 boot classpath 类”推断它的执行体,必须看过滤器、profile、-Xint/-Xusejit、debuggable/instrumentation 与入口点状态;debuggable 不是“禁止所有机器码”的同义词。本文不给任何耗时数字;表中的“慢/快”只是相对路径描述,真实差异需设备测量(观测口径见 4.10)。

4.4.8 失败、回退与观察

成本与权衡:AOT 把编译成本前移到安装/开机窗口,换稳态执行;JIT 按热度分摊,换来代码缓存与运行时编译开销;OSR 缩短长循环等待,但要求回边有 OSR stack map;去优化保证正确性,代价是回到解释器并可能再次编译。选择由过滤器、profile 与热度共同决定,不是单一“优化等级” [S28][S31][S36]。

下一章进入 RV64 后端:同一份 HIR 在 riscv64 上如何变成寄存器分配后的 LIR、如何为 String.equals/charAt 选指令,以及 ABI/内联数据/RVV 的边界。

第 5 章(4.5)RISC-V64 后端:ABI、指令选择、内建与扩展边界

本章回答:同一份 HIR 到 riscv64 机器码之间隔着哪些层;ART 私有 managed ABI 与 RISC-V psABI 在哪里分界;String.equals/charAt、System.arraycopy 在 RV64 上被选成什么指令序列;ISA 扩展(C/Zba/Zbb/Zbs/V)是怎么被发现和消费的;哪些“能力”只是汇编器存在而不是 codegen 可用。行号以 ART android17-release b753cf97 为准;未反汇编、未运行设备。

4.5.1 后端分层与 ISA 特性的真实来源

RV64 后端由四层构成:汇编器(compiler/utils/riscv64/assembler_riscv64.{h,cc},约 2900+7900 行,负责编码、伪指令、压缩指令与扩展掩码)、指令选择/codegen(compiler/optimizing/code_generator_riscv64.cc)、内建(intrinsics_riscv64.cc)与运行时入口汇编(runtime/arch/riscv64/quick_entrypoints_riscv64.S)。此外还有 managed_register_riscv64、instruction_simplifier_riscv64、critical_native_abi_fixup_riscv64、relative_patcher_riscv64、disassembler_riscv64 [S10][S11][S12][S34]。

ISA 特性探测(关键事实):riscv64 的特性对象在 ART 里没有运行期探测。Riscv64InstructionSetFeatures::FromCpuInfo/FromHwcap/FromAssembly/FromCpuFeatures 全部是 UNIMPLEMENTED(WARNING),然后回退到 FromCppDefines();后者只看编译期宏(__riscv_c/__riscv_v/__riscv_zba/__riscv_zbb/__riscv_zbs,加总是 kExtGeneric)。FromVariant 只接受 "generic";唯一的运行期输入是 AddFeaturesFromSplitString 解析显式 ISA 字符串(供交叉/镜像指令集声明等场景)(runtime/arch/riscv64/instruction_set_features_riscv64.cc:40-145)[S34]。这意味着目标二进制在编译时假定哪些扩展,运行时不会再去核对;与 ARM 的 HWCAP 探测模型不同。特性的消费者是可核对的:汇编器用 HasCompressed/HasZba/HasZbb/HasZbs/HasVector 组合扩展掩码(assembler_riscv64.h:1909-1923),codegen 用 HasCompressed 决定代码对齐粒度(code_generator_riscv64.cc:810),Zba/Zbb 路径带 DCHECK(:5537、:5651)[S34][S11]。

4.5.2 ABI 与栈帧:managed ABI ≠ psABI

RV64 的寄存器分工、帧尺寸与 psABI 差异见图 6。读图要点:托管 ABI 用 a0 固定携带 ArtMethod、s1 固定携带 Thread*;托管 ABI 的浮点实参使用 FA0-FA7,超过 8 个直接上栈,不借用整数实参寄存器。psABI 的规则不同:FA0-FA7 用尽后,浮点参数进入 A0-A7,再上栈;这两段在 JNI/GenericJNI 边界衔接,不能把“托管 ABI 不借 GPR”误读成“托管 ABI 不用浮点寄存器”。

2026-10-20T01:16:40.280224 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/
2026-10-20T01:16:41.666970 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

手机可横向滑动查看本图

图 6:RV64 托管调用的寄存器与栈帧

图 6: RV64 的 managed quick ABI 与 JNI native ABI(来源:ART a17r 的 registers_riscv64.h/asm_support_riscv64.h/jni_frame_riscv64.h/critical_native_abi_fixup_riscv64.cc,[S10][S30],本项目绘制)。GenericJNI 在两段之间转换 A0/A1 与 jobject;CriticalNative fixup 只处理超出 FP 寄存器、进入整数寄存器的那部分实参。

托管调用约定(riscv64 私有):A0 = ArtMethod*(code_generator_riscv64.h 的 kArtMethodRegister = A0),实参从 A1 开始,浮点参数用 FA0-FA7;xSELF=s1 始终携带 Thread*;GP(x3) 被征用为影子调用栈指针(SCS);TMP=t6、TMP2=t5 保留给汇编宏;TP(x4) 是 bionic 的 TLS 指针,不是 ART Thread(runtime/arch/riscv64/registers_riscv64.h:26-70)。帧尺寸常量(asm_support_riscv64.h:23-56):FRAME_SIZE_SAVE_REFS_ONLY=112、SAVE_ARGS_ONLY=128、SAVE_ALL_CALLEE_SAVES=208、SAVE_REFS_AND_ARGS=224、SAVE_EVERYTHING=480,nterp 帧 NTERP_SIZE_SAVE_CALLEE_SAVES=192;注释明确列出每个帧排除/包含的寄存器(例如 SAVE_REFS_AND_ARGS 排除 A0 与 TR)。

调用桩:art_quick_invoke_stub(quick_entrypoints_riscv64.S:266)用 INVOKE_STUB_CREATE_FRAME(:23-90 区段)建立 48 字节骨架帧:保存 RA/FP/xSELF/A4/A5,把 A0=ArtMethod*、A1=args ptr、A2=args len、A3=thread 规整后 mv xSELF, a3,再按 16 字节对齐复制参数、最后 ld t0, ART_METHOD_QUICK_CODE_OFFSET_64(a0); jalr t0。返回时恢复帧并处理返回值 [S09][S10]。

与 psABI 的分界:native 调用遵循 RISC-V psABI,浮点参数在 FA0-FA7 用尽后可进入 A0-A7,再上栈;托管 ABI 不做这种 GPR 借用:runtime/arch/riscv64/jni_frame_riscv64.h:35-52 的注释说的是“超过 8 个 FP 实参时不把它们放进整数寄存器,而是直接上栈,以避免编译器复杂度”,同一段仍明确托管 ABI 使用 FA0-FA7,且 kMaxFloatOrDoubleArgumentRegisters=8。因此不能把这句话单摘成“managed 不用浮点寄存器”。@CriticalNative 直呼时,编译期 CriticalNativeAbiFixupRiscv64 只为超过 FA0-FA7、按 native 布局必须占用整数寄存器的那部分 FP 实参插入 Float.floatToRawIntBits/Double.doubleToRawLongBits 型转换;前 8 个 FP 实参仍走 FP 寄存器,并非所有 FP 参数都转整型(critical_native_abi_fixup_riscv64.cc:27-54)[S30]。

4.5.3 指令选择案例:String.equals 与 charAt

当调用点被内建时(轴 1 为“是”,见 4.1.4),RV64 的内建序列(intrinsics_riscv64.cc:1152-1240)落实为:参数空检查(Beqz)→ 引用相等(Beq)→ klass_ 比较(两次 Loadwu,无读屏障,依赖 String 类对象不可移动)→ count 比较(Loadwu,同时比较长度与压缩标志;依赖构造规范化,见 4.1.4)→ 压缩标志/字符数换算(Andi/Srliw/Sllw)→ value_offset 起 8 字节 Ld 循环(压缩按 8 字节/轮,非压缩按 4 字符/轮)→ 返回 0/1。value_offset 是 String 对象内联数据区(4.2.3),因此这里没有任何“取数组引用+读屏障”的步骤 [S12][S16]。

charAt 内建(HArrayGet(is_string_char_at))在 RV64 的实现(code_generator_riscv64.cc:2876-2905):读 String::CountOffset() 的 count → Andi 0x1 判压缩 → 压缩时 Loadbu 取单字节;非压缩走 16 位加载;越界慢路径按 HBoundsCheck::IsStringCharAt() 选 kQuickThrowStringBounds(而不是数组越界入口,:396-401),抛出 StringIndexOutOfBoundsException。这与案例解释路径的 mirror::String::CharAt(C++)语义对齐:两者都从同一 count 布局出发 [S11][S16][S17]。

与 ARM64 的可核对差异:arm64 的 VisitStringEquals(intrinsics_arm64.cc:2539 起)在常规 8 字节循环之外还有一个 const-string 特化:当调用点一侧是编译期常量字符串且长度不超过阈值时,直接用 Ldp 成对加载与常量比较,并先用常量长度生成 GetFlaggedCount 与参数 count 比较;riscv64 的 intrinsics_riscv64.cc 中没有 GetConstString/DexFileStringAllASCII/GetFlaggedCount 的对应路径(检索为空)。这是一处真实的 RV64 内建缺口,而不是“指令更强/更弱”的判断——两条路径的正确性相同,差异只在编译期可做的特化数量 [S12][S34](arm64 对照实现:intrinsics_arm64.cc:2539 起)。

4.5.4 原子与内存序的 RV64 形态

这条组合的语义边界是:RVWMO 下 fence 组合提供所需的获取/释放与顺序一致语义;LR/SC 循环在争用下需要重试;AMO 路径避免了 RMW 循环,但并不自动提供比所标注 aq/rl 更强的语义。与 arm64 LSE 的差异是机制差异,不是快慢结论(未测量)[S11][S12][S10]。

4.5.5 内建覆盖与两条热点链

覆盖盘点(按 IntrinsicCodeGenerator<ARCH>::Visit* 去重):a17r riscv64 170 项,a16q2 176 项,arm64 179 项。arm64 有而 riscv64 无的差集在本次重写中复核为两类:CRC32 系列(CRC32Update/Bytes/ByteBuffer)与 FP16 系列(算术/比较/转换);此外 4.5.3 的 const-string 特化是代码内特化而非独立 Visit,不计入该项数 [S12][S38]。

4.5.6 RVV/向量的现状(只写源码可见的事实)

4.5.7 失败、回退与观察

成本与权衡:内联字符数据让 equals/charAt 的比较只读连续字节,但依赖“字符串不可变 + 零填充”前提;CAS 使用 LR/SC 循环,代价是高争用下的重试;AMO get-and-update 对完整字宽避免循环,但带 mask 的 32 位路径仍要 LR/SC;受约束 LR/SC 的序列长度与前向进展约束影响薄锁快路径的设计;C 压缩指令节省代码体积,代价是汇编器需要更强的约束与分支提升逻辑;RVV 基础设施先行但生成端不可用,当前不能拿来换吞吐。以上都是机制层面的取舍,未做测量 [S11][S12][S34]。

String 的内联字符数据让 equals 的读路径免去字符数组引用的间接加载;klass_ 仍是引用字段(两次 Loadwu),只因 String 类对象不可移动而免读屏障。charAt 越界、分配与引用数组写入仍要与 GC 屏障、TLAB 和 safepoint 打交道。下一章进入 GC/内存。

第 6 章(4.6)GC 与内存:对象、分配、collector、屏障与完整回收链

本章回答:Java 对象长什么样、new 在快慢路径上各做什么、默认 collector 如何被选中、读/写屏障在 RV64 上花在哪里、一次 CC 与一次 CMC 回收怎样从根扫描走到对象移动与页回收、引用/finalizer/OOM 的顺序。行号以 ART android17-release b753cf97 为准;设备默认与实测未运行,文中只写条件与源码。

4.6.1 对象与引用:8 字节头、32 位堆引用、内联字符串数据

mirror::Object 的头部只有两个 32 位槽(共 8 字节):klass_(HeapReference<Class>)与 monitor_(LockWord 编码:锁状态/哈希/读屏障状态位/CMC 的 mark bit)(runtime/mirror/object.h:75、:807-809;位域见 runtime/lock_word.h:94-140)。Java 堆内的对象引用是 32 位压缩引用(HeapReference 内部 Atomic<uint32_t> reference_,static_assert 校验大小;kHeapReferenceSize = sizeof(uint32_t),runtime/runtime_globals.h:173、object_reference.h:220-224),因此堆地址空间受 4GB 约束。这里必须限定范围:32 位的是堆引用字段/数组元素/栈上根槽,不是所有指针;JNI jobject、native 指针、ArtMethod*/mirror::Object* 在 64 位进程里仍是 64 位。把“压缩引用”推广成“JNI 句柄或 native 指针也是 32 位”会直接读错边界代码 [S30]。

对象头之外,String 是特例:字符数据内联在对象里(count_、hash_code_、value_/value_compressed_ 联合,runtime/mirror/string.h:316-326;ValueOffset() 即内联数据起点)。因此 String.equals 内建(4.1.4/4.5.3)读的是同一对象的连续字节,不需要“先取数组引用”。但不能据此说“没有引用字段读取”:内建在需要类型检查时会两次 Loadwu 读取 str->klass_/arg->klass_(intrinsics_riscv64.cc:1185-1200),klass_ 是 HeapReference<Class> 引用字段;它之所以不需要读屏障,是因为 AssertNonMovableStringClass() 依赖 String 类对象不可移动。value 内联只说明没有数组引用加载,不能推出所有引用读取都不存在。

4.6.2 分配:TLAB bump 是主路径,慢路径才是 GC

分配统一入口 Heap::AllocObjectWithAllocator(runtime/gc/heap-inl.h:45)。快路径判断:大对象(ShouldAllocLargeObject,:95,判定为 byte_count >= large_object_threshold_ 且是 primitive array 或 String;阈值 kMinLargeObjectThreshold = 12KB,heap.h:160)走 LOS;否则若分配器是 TLAB 型且空间足够,直接 Thread::AllocTlab(bytes)——thread-inl.h:441 只做 ret = thread_local_pos; thread_local_pos += bytes,空间由调用方保证。TLAB 的扩张按 kPartialTlabSize=16KB(heap.h:138)做部分扩张,新 TLAB 从 BumpPointer/RegionSpace 取得;失败才进入 Heap::AllocateInternalWithGc(heap.cc:2002)的等待/GC 循环。

分配失败循环的顺序(heap.cc:2002-2160):先 WaitForGcToComplete,若已有 GC 完成则重试分配;否则按 next_gc_type_ 跑一次不清软引用的 GC;若回收量/空闲比例仍不够,再跑 gc_plan_.back() 的完整 GC,并把 clear_soft_references 置 true;最多重试 kMaxGcAttempts=5,仍失败才抛 OOM。这个顺序是 4.6.8 软引用语义的基础,不要写成“每次 GC 都清软引用”或“只有 OOM 瞬间才处理软引用”。

注意“纯 bump”并不完整:C++ 快路径在 bump 之后还要 SetClass,Baker 配置下做读屏障状态断言,并执行构造屏障(ThreadFenceForConstructor,heap-inl.h:118-128);编译代码则由编译器在构造器返回处插入 StoreStore 构造屏障(VisitConstructorFence,code_generator_riscv64.cc:3655-3660),所以“对象引用发布前可见构造结果”是单独的一步,不是 bump 自带的。

RV64 的编译代码路径:TLAB 快路径是手写汇编(ALLOC_OBJECT_TLAB_FAST_PATH_RESOLVED,quick_entrypoints_riscv64.S:1793 起):读 THREAD_LOCAL_POS_OFFSET(xSELF)、加大小、写回、更新对象头;大数组检查阈值后转慢路径。也就是说,new 在热路径上通常只有“读线程指针 + 加法 + 写回 + 对象头初始化”,没有锁、没有 GC 调用——GC 成本主要来自 TLAB 用尽后的批量记账与并发触发(ShouldConcurrentGCForJava,heap-inl.h:471,按 concurrent_start_bytes_ 或时间触发策略判定)[S30]。

4.6.3 Collector 选择:默认由 uffd 与读屏障共同决定

runtime/gc/collector/ 在 a17r 的集合是:concurrent_copying、mark_compact、mark_sweep、partial_mark_sweep、sticky_mark_sweep、semi_space 等,没有 cms\* 目录——独立 CMS 实现早已删除;仍保留 kCollectorTypeCMS 枚举与 MarkSweep(is_concurrent) 复用形态(collector_type.h、mark_sweep.h:65-73):CreateGarbageCollectors 在 -Xgc:CMS 且 MayUseCollector(kCollectorTypeCMS) && concurrent 时创建并发 MarkSweep(heap.cc:861-866),但这不是默认路径。构建默认 GC 类型为 ART_DEFAULT_GC_TYPE=CMC(build/art.go:50);运行期前景 collector 的选择是 gUseReadBarrier ? kCollectorTypeCC : xgc_option.collector_type_(runtime.cc:1914),背景为 CC 后台(:1878 区段)。

CMC 的可用性条件(按源码逐项):ShouldUseUserfaultfd()(mark_compact.cc:310-325)在 Linux 上返回:显式 -Xgc:CMC(FetchCmdlineGcType 扫 /proc/self/cmdline),或(未显式指定 && Android 目标 && SysPropSaysUffdGc() && 内核支持)。SysPropSaysUffdGc 的完整布尔是 (phenotype_enable || build_enable || is_at_most_u) && !phenotype_force_disable(mark_compact.cc:293-305):phenotype_enable 读 persist.device_config.runtime_native_boot.enable_uffd_gc_2(默认 false),phenotype_force_disable 读 force_disable_uffd_gc(默认 false),build_enable 读 ro.dalvik.vm.enable_uffd_gc(默认 false),is_at_most_u = !IsAtLeastV() 是 OR 项——Android U 及以下在未 force_disable 且内核支持时默认启用 uffd/CMC,V 及以上才依赖显式属性;此外还有内核能力与构建/运行期 CHECK 门,不能把“U 及以下”单项当成设备必然 CMC;内核侧最低要求 MREMAP_DONTUNMAP + userfaultfd(UFFD_USER_MODE_ONLY) + UFFD_FEATURE_SIGBUS(:130-185)。不满足则 gUseUserfaultfd=false,回落到“读屏障 + CC”组合。构建与运行期还有交叉校验:heap.cc:452-467 要求 uffd 配置必须配 CMC,读屏障配置必须配 CC,否则 CHECK 失败。generational:ShouldUseGenerationalGC()(mark_compact.cc:331;host 恒真,:346)与运行时门控共同决定 minor(sticky)/major(full)策略;CC 的 young 实现与 CMC 的 YoungMarkCompact 都在 collector 目录内 [S30]。

这里不要写“默认 collector=CC”或“默认=CMS”这类绝对句:a17r 的默认是“构建期 CMC + 运行期 uffd/读屏障条件选择”;设备上是否真的用 CMC,要看内核 uffd 能力与属性,本文未在设备验证。

4.6.4 屏障:读屏障的 RV64 形态与写屏障卡表

4.6.5 根、挂起、checkpoint 与对象移动

GC 安全点由 ThreadList::SuspendAll(thread_list.cc:910)→ SuspendAllInternal(:980)实现:取线程列表与挂起计数锁、对其他线程逐个 IncrementSuspendCount(带 suspend barrier 计数与 immune 机制)、等待被挂起线程到达安全点。GC 主流程 Heap::CollectGarbageInternal 以 kWaitingPerformingGc 状态串行化回收。根集合包含线程栈(quick frame 逐 vreg)、ClassLinker/DexCache/Intern 表等(Runtime::VisitRoots/ClassLinker::VisitRoots/Thread::VisitRoots)。nterp 帧的根扫描走双数组协议:NterpGetReferenceArray(nterp_helpers.cc:196)给出引用影子数组,对象移动时同时改写 regs 与 refs 数组(4.3.2)[S30][S40]。

普通 JNI 与 Fast/Critical 的挂起语义不同:普通 JNI 在进入 native 前转为非 kRunnable(quick_jni_entrypoints.cc:71-72 的 artJniMethodStart 路径),对 GC 而言属于可挂起状态;@FastNative 通常保持 kRunnable,只在返回时 CheckSuspend()(:157-169);@CriticalNative 不做状态转换也不检查挂起。Thread::IsSuspended() 的判据是“非 Runnable 且有 kSuspendRequest”(thread.h:461-468)。因此“native 长驻导致 GC 常卡住”必须限定:普通 JNI 不会因为“在 native 里不检查 flag”而阻塞 GC;真正可能拖住挂起的是长期运行的 FastNative/CriticalNative 代码、持有 checkpoint 的线程,或 native 重入 Java 时未正确转换/检查的路径。CriticalNative 的“GC disabled”是运行时对这段调用不进行 GC 的协议约束,不等于“全进程任何并发 GC 都停止” [S43]。

checkpoint 的语义(thread_list.h:131-182):RunCheckpoint 不等待运行中线程完成(:151);对已挂起线程由调用者代跑。返回本身不是完成门:调用者还须正确等待所有 callback 完成(通常通过 barrier)并观察完成,之后才可用“先前存在的线程执行过一段保持挂起的代码,且调用 happens-before 其结束”的保证(:168-173)。新线程范围按 :161-166,只覆盖先前存在的线程,不扩到所有未来线程。RunEmptyCheckpoint 当前实现明确不保证与 RunCheckpoint 相同的性质(thread_list.h:189-199 的待办注释 b/382722942),不能把它当作等价同步点(4.7.2)。

4.6.6 CC 的一轮完整回收:从根翻转到 from-space 清空

2026-10-20T01:53:57.224685 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/
2026-10-20T01:53:58.679192 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

手机可横向滑动查看本图

图 7:CC 与 CMC 一轮回收的阶段时序

图 7: CC 与 CMC 的暂停/并发阶段(来源:ART a17r 的 concurrent_copying.cc、mark_compact.cc、mark_compact-inl.h,[S30],本项目绘制)。CC 常规路径有根翻转与“关闭弱引用访问”两次 STW(可选 from-space 验证暂停另计);CMC 有标记暂停与压缩暂停两次 STW,压缩页由 uffd 故障按需完成。

CC 的阶段时序见 ConcurrentCopying::RunPhases(concurrent_copying.cc:214-269),图 7 的 CC 泳道给出暂停/并发位置。下面按“谁在跑、什么被暂停、引用何时改写、mutator 如何继续访问、何时完成”复述一轮 full-heap、非 young 的 CC:

  1. 触发与串行化:分配失败、显式 System.gc() 或后台策略进入 Heap::CollectGarbageInternal;它设置当前 GC、选择 collector,并调用 collector->Run(gc_cause, clear_soft_references)。此后同一时刻只有一个 GC 在执行(kWaitingPerformingGc),其他分配线程等待。
  2. InitializePhase(并发):记录分配量、清空/绑定位图、计算 immune spaces,标记 zygote large objects;此时 mutator 继续运行,from/to 语义尚未翻转。
  3. 可选 MarkingPhase(并发,仅 generational 的非 young full GC 且非 force_evacuate_all):扫描 immune spaces、runtime roots、thread roots 与 mark stack,计算 live bytes;这是“先标记、后复制”的两阶段模式。非 generational 的 full CC 可以跳过独立 MarkingPhase,直接在翻转后靠读屏障复制。
  4. GrayAllDirtyImmuneObjects(并发,Baker 条件):在翻转前把 immune 空间的脏卡对象置灰;ActivateReadBarrierEntrypoints() 先把读屏障入口切到“标记中”版本,避免 mutator 看到灰位却走到旧入口(concurrent_copying.cc:231-240)。
  5. FlipThreadRoots(STW 暂停):FlipThreadRoots 让所有 mutator 停在安全点;ThreadFlipVisitor 对每个线程 SetIsGcMarkingAndUpdateEntrypoints(true)、撤销 TLAB/分配栈、用 Mark() 把线程根从 from-space 改写到 to-space;FlipCallback 调 RegionSpace::SetFromSpace、交换 live/mark 栈、置 is_marking_、灰化新脏的 immune 对象。暂停结束后 mutator 恢复,从此读屏障保证“看不到 from-space 引用”。
  6. CopyingPhase(并发,但内含一次 weak-ref-access-disable STW):GC 线程与 mutator 的读屏障共同处理 mark stack;处理弱引用前调用 SwitchToSharedMarkStackMode(),其 RevokeThreadLocalMarkStacks(disable_weak_ref_access=true) 在 ScopedPause 下关闭弱引用访问(concurrent_copying.cc:1648-1653、:2068-2085),这是 CC 的第二个暂停点。ConcurrentCopying::Copy()(concurrent_copying.cc:3375)在 to-space 分配同尺寸对象,复制对象体(lock word 单独处理),再用 CAS 把 from-space 对象的 LockWord 写成 forwarding address;其他线程随后通过读屏障看到转发地址并改读 to-space。对象内的引用由 Scan/ProcessMarkStackRef 递归处理;根在翻转时已改写,运行期新根由读屏障/写屏障维护。弱引用访问在 SwitchToSharedMarkStackMode() 后关闭,ProcessReferences 处理软/弱/finalizer 引用并 SweepSystemWeaks,随后重新打开弱引用访问(concurrent_copying.cc:1600-1690)。完成条件:mark stack 为空、没有 from-space 引用(可选 VerifyNoFromSpaceReferences 会再暂停一次验证)、所有可达对象已在 to-space 或被标记为不发散。
  7. ReclaimPhase(并发/短暂停顿):CleanupClassLoaders;IssueEmptyCheckpoint 确保没有 mutator 还持有旧 mark stack 引用;sweep malloc 空间、交换/解绑位图;RegionSpace::ClearFromSpace 释放未搬走的区域并记账。到这里,from-space 的物理区域已可复用。
  8. FinishPhase:清 mark bits/cards、清空/归还 mark stack、结束统计。CC 的一轮结束。

mutator 如何继续访问:翻转后,线程根已经指向 to-space;字段/数组中的 from-space 引用在读取时由 Baker 读屏障检查转发地址并改写;对象本身在 from-space 的旧副本在 ClearFromSpace 前仍可读,但读屏障保证返回 to-space 指针。谁暂停:CC 的 STW 至少包括:① FlipThreadRoots 根翻转;② CopyingPhase 内 SwitchToSharedMarkStackMode() 关闭弱引用访问时触发的全 mutator 暂停(concurrent_copying.cc:1648-1653、:2068-2085,ScopedPause 在 :2078);③ 可选的 VerifyNoFromSpaceReferences 验证暂停。除此以外,Initialize、Marking、Gray、Copying 的复制/引用处理、Reclaim、Finish 允许 mutator 运行;不要写“Copying 全程并发”或“只有根翻转一次 STW”。CC 的代价:每个引用读取多一次 mark/forward 检查;复制是并发进行的,但暂停点必须完成全线程根翻转。

4.6.7 CMC 的一轮完整回收:标记、页压缩与 uffd 故障驱动

CMC 的阶段时序见 MarkCompact::RunPhases(mark_compact.cc:908-966),与 CC 不同:它有两个明确的 STW 暂停(MarkingPause、CompactionPause),压缩阶段本身是并发的,并且用 userfaultfd 把“尚未压缩的页”变成可阻塞的缺页。下面按同一组问题复述一轮 full、非 young 的 CMC:

  1. InitializePhase:mark_stack_ 清空、immune spaces 复位、black_allocations_begin_ = bump_pointer_space_->Limit()、from_space_slide_diff_ = from_space_begin_ - moving_space_begin_、moving_space_end_ 与回收页游标初始化(mark_compact.cc:821-856)。此时尚未暂停。
  2. MarkingPhase(并发):从 immune spaces、runtime roots、thread roots 出发标记;写屏障卡表记录跨代/跨区引用;mark stack 处理与 PrepareForMarking 并行。标记期间 mutator 继续分配,但这些对象仍在 MarkingPause 将要快照的边界之前,需按标记/压缩处理;InitializePhase 的 black_allocations_begin_ = bump_pointer_space_->Limit()(:834)只是初值。MarkingPause 在 :1609-1615 才把当前 bump 指针按页对齐写入 black_allocations_begin_,此后的新分配才是 black allocation,本轮不压缩。
  3. MarkingPause(STW):ScopedPause 暂停所有 mutator;逐个线程 VisitRoots、撤销 TLAB 与 thread-local allocation stack;ProcessMarkStack;把当前 bump 指针按页对齐记为 black_allocations_begin_;ReMarkRoots、扫描脏卡、SwapStacks、DisallowNewSystemWeaks、启用 reference slow path(mark_compact.cc:1573-1650)。暂停结束后 mutator 恢复,但新分配都在 black 区。
  4. ReclaimPhase(并发/读锁下):ProcessReferences 处理软/弱/finalizer 引用(软引用是否前移取决于 clear_soft_references_,见 4.6.8);SweepSystemWeaks;CleanupClassLoaders;Sweep(false) 回收未标记对象、交换/解绑位图(mark_compact.cc:1715-1748)。此时仍未移动任何对象。
  5. PrepareForCompaction(并发):为每个 moving-space 页计算 live bytes、首个对象、post_compact_end_、black_objs_slide_diff_;若没有可压缩页则直接跳过压缩。PostCompactOldObjAddr 的地址由“该地址之前的 live word 数 + 当前 bitmap word 内的 live bytes”累加得出(mark_compact-inl.h:365-374);black allocation 的地址用 old_ref - black_objs_slide_diff_(:360-363)。这解释了“压缩地址是显式计算并写回引用”,不是 uffd 自动把对象指针做地址翻译。
  6. FlipThreadRoots + CompactionPause(STW):ThreadFlipVisitor 更新线程根、清 interpreter cache、AdjustTlab;FlipCallback 进入 CompactionPause:UpdateMovingSpaceBlackAllocations/UpdateNonMovingSpaceBlackAllocations、置 compacting_、UpdateRoots、更新 immune spaces/class loader/arena pool 根;KernelPreparation 用 mremap(..., MREMAP_DONTUNMAP) 把 moving-space 的原始页搬到 from_space_begin_ 的影子映射,原 moving-space 变成 uffd 注册的空地址范围;linear-alloc 也做同样的 shadow + 注册(mark_compact.cc:4297-4470、:4476-4570)。
  7. CompactionPhase(并发):CompactMovingSpace<kUffdMode> 从低地址开始压缩:对每个页复制 live 区间、按对象遍历引用并 UpdateRef 写入压缩后地址,然后通过 UFFDIO_COPY/UFFDIO_MOVE 把压缩页安装到 moving-space 的对应地址;未使用页用 UFFDIO_ZEROPAGE。GC 线程处理页时用 DoPageCompactionWithStateChange 的 CAS 状态机保证一页只处理一次(mark_compact.cc:3204-3245)。mutator 访问尚未安装的页会收到 SIGBUS(UFFD_FEATURE_SIGBUS);SigbusHandler 校验 BUS_ADRERR,定位 moving-space/linear-alloc 页,调用 ConcurrentlyProcessMovingPage 就地完成该页压缩/安装后返回,mutator 重新执行 faulting 指令即可(:4605-4700、:4710-4800)。页状态由 DoPageCompactionWithStateChange 的 CAS 保证只处理一次:若 GC 线程已处理,handler 直接返回;若 handler 先取得处理权,GC 线程按状态跳过;compaction 结束时 GC 线程用 sigbus_in_progress_count_ 的 done 位等待所有已进入的 handler 退出(:3204-3245、:5249-5300)。完成条件:所有 used page 状态为 processed/mapped,compaction 计数归零,uffd 注销,from-space shadow madvise 释放(:5249-5330)。
  8. FinishPhase:更新 generation 边界、释放 linear-alloc 影子、清位图/arena 延迟释放、结束统计(:6236 起)。

mutator 如何继续访问:压缩期间 mutator 仍用原 moving-space 虚拟地址;已安装页直接可读,未安装页触发 uffd/SIGBUS,由 handler 或 GC 线程按需压缩;引用更新由 UpdateRef/PostCompactAddress 显式完成。谁暂停:MarkingPause 与 CompactionPause 是 STW;Marking、Reclaim、Prepare、Compaction 的页处理是并发/按需。fallback:uffd_ == kFallbackMode 时 CompactionPause 内直接 CompactMovingSpace<kFallbackMode>,没有并发故障驱动;正常 Android 路径是 uffd 模式(mark_compact.cc:1903、:4455-4465)。

CC 与 CMC 的对照(同图 7):CC 用读屏障 + LockWord forwarding,STW 至少两次(根翻转 + 弱引用访问关闭,可选验证另计);CMC 用两个暂停(标记暂停、压缩暂停)+ uffd 页故障,压缩阶段不需要读屏障,但需要内核 uffd 与页状态机。两者的“完成”都不是“GC 线程退出”这么简单:CC 要确认 mark stack 空且无 from-space 引用;CMC 要确认所有 used page 已处理/映射并注销 uffd。

4.6.8 引用、finalizer 与 OOM:前移与清理不是一回事

ReferenceProcessor 按引用类型分派。软引用的“前移(forward)”与“清理(clear/queue)”是相反方向的操作:ForwardSoftReferences 的注释明确“Only invoked if we are not clearing SoftReferences”(reference_processor.cc:171-178);它把软引用指向的 referent 标记/前移,并 ProcessMarkStack 保持其存活(:179-190)。ProcessReferences 在 !clear_soft_references_ 时还会把晚发现的软引用继续前移(:207-214)。只有当 clear_soft_references_ 为 true 时,软引用才会被清除/入队。

这与分配失败循环对应(4.6.2):第一次 GC 用 clear_soft_references=false;若回收不足,第二次完整 GC 用 true,此时 ForwardSoftReferences 不再执行,软引用被清理;循环最多 5 次后抛 OOM(heap.cc:2094、:2143)。因此“VM 规范要求 OOM 前清软引用”是对的,但不能把 ForwardSoftReferences 当成 OOM 时清理的依据——它恰恰是“保留软引用”的路径。finalizer 引用在 GC 中入队(EnqueueFinalizerReferences),ReferenceQueue 负责原子入队与被清引用收集;-XX:FinalizerTimeoutMs 默认 10000ms(runtime_options.def:68)。OOM 抛出点在 GC 循环之后(heap.cc:1546),消息中的分配大小、target footprint、growth limit 与碎片指标是现场定位线索 [S30]。

4.6.9 案例与成本边界

4.6.10 失败、回退与观察

失效边界与成本权衡:失效边界包括 uffd 不可用(回退 CC)、启用读屏障时的非 Baker 屏障(优化编译器 FATAL)、挂起线程未达安全点(SuspendAll 超时 FATAL)、以及 -Xgc 与构建配置冲突(heap CHECK 失败)。成本上,TLAB 快路径是无锁 bump,慢路径才进入 GC 世界;Baker 读屏障换并发移动能力但增加每次引用读取的入口检查;卡表写屏障把跨代引用记录成本摊到写操作;CMC 用 uffd 换并发压缩,代价是页故障与内核依赖;LOS/大对象减少复制但增加碎片管理。这些都是机制取舍,设备数值未测 [S30]。

下一章把线程状态、挂起/checkpoint 与 monitor、内存序串成一条链。

第 7 章(4.7)线程、挂起、Monitor 与内存序:JMM 到 RVWMO

本章回答:ART 的线程状态与 GC/调试挂起如何互锁;mutator_lock_ 为什么不是普通读写锁;薄锁/膨胀锁各在什么路径上;Java 的 volatile/CAS 在 RV64 上生成什么序列、哪些语义点源码并未论证。行号以 ART android17-release b753cf97 为准;未运行设备。

4.7.1 ThreadState 与 state+flags 打包

ThreadState(runtime/thread_state.h:37-64)把 Java 线程状态与 ART 内部挂起状态统一编码:kRunnable=0(历史值为 67,改为 0 后 state_and_flags 非零即代表有 flag;注释强调同一地址的所有原子访问必须同宽度),kTerminated=66,kTimedWaiting=68 起依次 kSleeping/kBlocked/kWaiting/.../kNative/kSuspended。关键定义在 thread_state.h:30-33:任何非 kRunnable 状态都算“已挂起”(suspended),只有 kRunnable 允许访问 Java 堆;kNative 对 JDWP 显示为 RUNNABLE,但对 ART 是挂起态(JNI 期间不持 mutator 锁共享权)。

状态与 flag 打包在一个 32 位字(Thread::StateAndFlags,thread.h:2188-2240):状态在最高位,flag 在低位;flag 包括 kSuspendRequest/kCheckpointRequest/kEmptyCheckpointRequest/kActiveSuspendBarrier/kPendingFlipFunction/kRunningFlipFunction/kSuspensionImmune/kMonitorJniEntryExit(thread.h:141-184),SuspendOrCheckpointRequestFlags() 是前三者之并。状态转换的关键入口:SetState(只允许挂起态之间,thread-inl.h:153-178)、TransitionFromRunnableToSuspended(:333-348:先跑 pending checkpoint,再 release-CAS 写新状态,再更新 MutatorLock 所有权与 suspend barrier)、TransitionFromSuspendedToRunnable(:351-444:无请求 flag 时 acquire-CAS 回 kRunnable;有 suspend 请求则在 thread_suspend_count_lock_ 下等待 resume_cond)。

4.7.2 挂起协议与 checkpoint

ThreadList::SuspendAll(thread_list.cc:910-978)先 SuspendAllInternal(:980-1101):持线程列表与挂起计数锁,对除自己外的每个线程 IncrementSuspendCount(thread-inl.h:522-555:把 suspend barrier 挂到链上、++suspend_count、用一次 fetch_or(kSuspendRequest|kActiveSuspendBarrier, release) 置位),自己置 kSuspensionImmune;目标线程主动挂起时 CheckActiveSuspendBarriers() → PassActiveSuspendBarriers()(thread.cc:1738-1787)以 release 递减 barrier 并 futex 唤醒。请求侧 WaitForSuspendBarrier(thread_list.cc:785-905)有分级超时(user build ×2、低优先级再放大、tracing stop 时放宽),最坏情况打印 culprit 线程后 LOG(FATAL) "SuspendAll timeout. Couldn't find holdouts."。然后 SuspendAll 才去拿 mutator 锁的排他权——注释写明“所有线程已知挂起,但可能仍有线程持有 mutator 锁”,这正是 4.7.3 的要点。

checkpoint 的语义(thread_list.h:131-182):RunCheckpoint 不等待运行中线程完成(:151);对已挂起线程由调用者代跑。返回本身不是完成门:调用者还须正确等待所有 callback 完成(通常通过 barrier)并观察完成,之后才可用“先前存在的线程执行过一段保持挂起的代码,且调用 happens-before 其结束”的保证(:168-173)。新线程范围按 :161-166,只覆盖先前存在的线程,不扩到所有未来线程。实现(thread_list.cc:327 起)持两把全局锁、可能单独 suspend 线程,代价不低。RunEmptyCheckpoint 当前实现明确不保证与 RunCheckpoint 相同的性质(thread_list.h:189-199 的待办注释 b/382722942),Thread::CheckSuspend 的 acquire 读也标注了同一条待办注释(thread-inl.h:84-112)。

riscv64 的安全点:nterp 的 DO_SUSPEND_CHECK(mterp/riscv64/main.S:149-155)读 THREAD_FLAGS_OFFSET(xSELF) 的 suspend/checkpoint 位,非零则 call art_quick_test_suspend;编译代码由 GenerateSuspendCheck(code_generator_riscv64.cc:1205-1249)生成 load-flags + 分支,慢路径 InvokeRuntime(kQuickTestSuspend) → artTestSuspendFromCode(quick_thread_entrypoints.cc:42-56)→ CheckSuspend + DeoptimizeIfNeeded(kKeepDexPc)。riscv64 没有隐式挂起检查:CanUseImplicitSuspendCheck() 返回 false(源码待办注释,:6227-6230),若强制走隐式路径则 LOG(FATAL) "Unimplemented ImplicitSuspendCheck";运行时只在 arm64 置 implicit_suspend_checks_,kRiscv64 与 arm/x86 同组只置 null/SO 检查(runtime.cc:2013-2027)。arm64 的 “suspend_trigger 置空触发 SIGSEGV” 机制在 riscv64 不参与。

4.7.3 MutatorLock:状态即所有权

Locks::mutator_lock_ 是 MutatorMutex(runtime/base/mutex.h:466-495),行为类似读写锁,但线程状态参与锁所有权:处于 kRunnable 的线程被视为持有共享权,因此“进入排他”前必须先让所有线程挂起。mutator_gc_coord.md:88-110 进一步说明:显式 mutator_lock_ 只在需要排他(GC)或少数 SharedTryLock 场景真正 acquire;TransitionFromSuspendedToRunnable 并不物理地以共享模式 acquire 底层 rwlock——所以 GC 的排他必须自己先完成挂起协议(这正是 SuspendAll 的顺序)。状态转换本身承担 JMM 的 acquire/release 边:转入挂起用 release,保证挂起前访存对挂起者可见;转回 kRunnable 用 acquire CAS 并检查无残留请求(mutator_gc_coord.md:312-345)。

4.7.4 Monitor 与 LockWord:薄锁、膨胀与 wait/notify

LockWord(runtime/lock_word.h:31-58)是对象 monitor_ 槽的 32 位复用字,四种状态:thin(owner tid 16 位 + count 12 位 + 读屏障/mark 位)、fat(28 位 MonitorId)、hash(28 位 identity hash;32 位字 = 2 位 state + 1 位 read barrier + 1 位 mark + 28 位)、forwarding(GC 转发)。状态值 kStateThinOrUnlocked=0/kStateFat=1/kStateHash=2/kStateForwardingAddress=3(:99-104),kThinLockCountOne = 1<<16(:91)。riscv64 的薄锁快路径 LOCK_OBJECT_FAST_PATH(asm_support_riscv64.S:804-839)用 lr.w.aq/sc.w 完成“无锁置位 / 同 owner 重入计数 / 计数或用例失败转慢路径”,宏内注释要求受约束 LR/SC 序列不超过 16 条指令(以取得前向进展保证),所以 riscv64 不能像其它架构那样把递归锁放到慢路径;这是该快路径的序列约束,不是 ISA 对所有 LR/SC 的普遍硬上限。解锁 UNLOCK_OBJECT_FAST_PATH(:843-878)用 lr.w + sc.w.rl(riscv64 除 SC 外没有 store-release)。

膨胀至 Monitor:Monitor::MonitorEnter(runtime/monitor.cc:1199-1358)按 LockWord 状态分派——无锁直接 CAS;同 owner 计数+1,溢出则 InflateThinLocked;他人持有先自旋(kExtraSpinIters=100,随后 CheckSuspend+sched_yield),再 suspend owner 做膨胀;fat 状态先 std::atomic_thread_fence(acquire) 再 mon->Lock(relaxed 初读需要 fence 才可见 Monitor 结构)。Monitor::Wait(:891-1021)把线程切到 kTimedWaiting/kWaiting(ScopedThreadSuspension),释放 monitor 锁等待条件变量,醒来后重新加锁并恢复计数;notify/notifyAll 只是把等待节点从 wait_set 挪到 wake_set,真正的唤醒在解锁路径 SignalWaiterAndReleaseMonitorLock(:839-889)。Object.wait 与 notify 都是 @FastNative(java_lang_Object.cc:59-61),因此 JNI 期间线程保持 kRunnable,真正的状态切换发生在 Monitor::Wait 内部 [S30][S43]。

类初始化就是 monitor 的典型用法,但 <clinit> 本身不在类 monitor 内执行:ClassLinker::InitializeClass(class_linker.cc:5938 起)先在 ObjectLock<mirror::Class> 下复查状态、设置 ClinitThreadId 与 kInitializing(初始 ObjectLock 作用域在 :6066 结束),随后释放锁,执行父类初始化与 <clinit>(Invoke 在 :6198-6202),最后在 :6210-6211 重新加 ObjectLock 发布完成/错误并通知等待者。等待者 WaitForInitializeClass(class_linker.cc:6303-6330)用 WaitIgnoringInterrupts(kWaiting),抢锁者处于 kBlocked——启动时大量 <clinit> 会把这条路径反复走一遍(4.2.4);不要写成 Java <clinit> 全程持类 monitor。

4.7.5 JMM → RVWMO:机器码与未论证点

4.7.6 失败、回退与观测

下一章进入 JNI 与 native 边界,说明解释态字符串比较的每轮两次 charAt 是“AOT intrinsic 方法体或 GenericJNI 回退”的条件路径(只有回退才建 JNI 帧/LRT),以及 System.arraycopy 的三条路径。

第 8 章(4.8)JNI 与 native 边界:从 Java 调用到 psABI

本章回答:一次 JNI 调用在 ART 内经过哪些表、谁生成 stub、@FastNative/@CriticalNative 到底改变了什么、native 库与符号怎么找到、异常与信号如何在边界上转换,以及 String.charAt 与 System.arraycopy 这两条案例热点在边界上的真实形态。行号以 ART android17-release b753cf97 为准;未运行设备。

4.8.1 两张函数表与 JNI stub 的生成

JNI 有两张函数表:JNIEnv 的 JNINativeInterface(每个函数一个槽,JNIEnvExt 构造时把它指向普通表或 CheckJNI 表,runtime/jni/jni_env_ext.cc:70-82、:115-124;普通表实体在 runtime/jni/jni_internal.cc 的 gJniNativeInterface)与 JavaVM 的 JNIInvokeInterface(只有 DestroyJavaVM/AttachCurrentThread/DetachCurrentThread/GetEnv/AttachCurrentThreadAsDaemon 等 8 项,java_vm_ext.cc:513-522)。JNI_CreateJavaVM 在 java_vm_ext.cc:1237。

stub 生成器:本 pin 中不存在名为 BuildJniFunctionTrampoline 的符号;native 方法的 JNI stub 由 ArtQuickJniCompileMethod()(compiler/jni/quick/jni_compiler.cc:770-781)→ ArtJniCompileMethodInternal()(:77-729)生成:用 JniCallingConvention 与 ManagedRuntimeCallingConvention 迭代参数(:146-159),通过 JNIMacroAssembler 发射指令(riscv64 实现在 compiler/utils/riscv64/jni_macro_assembler_riscv64.cc)。关键步骤:BuildFrame()(JNI 帧 = ArtMethod* + callee-save 槽,按 16B 对齐,calling_convention_riscv64.cc:218-235)、MoveArguments()(managed→native 参数搬移、引用转 jobject、防环,jni_macro_assembler_riscv64.cc:261-369)、native 调用(Call(method, EntryPointFromJniOffset),无栈参的 @CriticalNative 用尾调用)、返回引用解码与 RemoveFrame()。

JNI 局部引用帧:每次进入 native 建立一个 LRT 段,PushLocalReferenceFrame/PopLocalReferenceFrame 操作 JNIEnvExt::locals_(jni_env_ext.h:82-87);stub 把上一段 cookie 存到 JNIEnvExt::LrtPreviousStateOffset,返回前恢复(jni_compiler.cc:285-306、:518-524)。@CriticalNative 整段跳过;@FastNative 不跳过。

4.8.2 FastNative / CriticalNative:三种边界的精确差异

注解在链接期变成 access flag:dalvik.annotation.optimization.FastNative → kAccFastNative,CriticalNative → kAccCriticalNative(runtime/dex/dex_file_annotations.cc:1303-1322;ClassLinker 链接 native 方法时读入,class_linker.cc:4205-4211)。同时标注两者会触发 CHECK_NE(进程终止而非 Java 异常);ART_USE_RESTRICTED_MODE 下 IsCriticalNative() 恒 false(art_method.h:468-499)。

语义差异(源码可核):

形态线程状态JNIEnv/jclassLRT异常轮询挂起响应其它
普通 nativeRunnable→Native→Runnable(慢路径 artJniMethodStart/End)有建立有调用期间可挂起常规 JNI
@FastNative保持 Runnable(持 mutator lock)有建立(门是 !is_critical_native)有返回时一次 CheckSuspend()不做 Runnable↔Native 转换、不调 artJniMethodStart;ScopedFastNativeObjectAccess 的 DCHECK 要求 IsFastNative() 且 kRunnable
@CriticalNative不做转换无无无不检查执行期间该调用不进行 GC(运行时协议约束,不等于全进程停止);必须 static、非 synchronized、shorty 无引用;隐藏 ArtMethod* 放 T0

源码锚点:普通 native 的状态转换与 LRT 在 jni_compiler.cc:264-277、:464-477;@FastNative 跳过状态转换、仅在返回前 suspend check(:487-496、:562-568;quick_jni_entrypoints.cc:157-170),但 LRT push/pop 的门是 if (LIKELY(!is_critical_native)),FastNative 仍执行(jni_compiler.cc:285-305、:519-524;GenericJNI 侧 quick_trampoline_entrypoints.cc:2126-2135 同样在 !critical_native 时建立 LRT,只有 critical_native 跳过)。@CriticalNative 的“GC disabled”注释在 jni_compiler.cc:218-220,无栈参尾调用 :413-418。注解合法性由验证器检查(@FastNative 不得 synchronized;@CriticalNative 必须 static、不得 synchronized、shorty 不得含引用),非法组合得到 VerifyError(runtime/verifier/method_verifier.cc:1667-1697;测试 test/004-JniTest/src/.java 的 BadFastNative/BadCriticalNative 用例)。

riscv64 的 CriticalNativeAbiFixupRiscv64 只为第三种形态中超过 FA0-FA7、按 native 布局必须进入整数寄存器的那部分 FP 实参服务:managed ABI 的 FP 实参只用 FA0-FA7、超出上栈,不借用 GPR;psABI 在 FA0-FA7 用尽后把 FP 参数放入 A0-A7 再上栈。编译器直呼 @CriticalNative 时,第 9 个及以后的浮点实参在 native ABI 中要落到整数寄存器,而 managed 图里它仍是浮点值——fixup 仅为这些参数在 HIR 上插入 Float.floatToRawIntBits/Double.doubleToRawLongBits 等价转换(critical_native_abi_fixup_riscv64.cc:27-54);前 8 个 FP 实参仍走浮点寄存器,不是所有 FP 参数都转换。帧尺寸换算见 runtime/arch/riscv64/jni_frame_riscv64.h:60-91,stub 侧用 DCHECK_EQ 交叉核对(calling_convention_riscv64.cc:258-260)[S30][S43]。

4.8.3 入口与符号查找:GenericJNI、dlsym lookup 与 RegisterNative

镜像中 非 intrinsic 的 native 方法在 boot image 中写 JNI stub(image_writer.cc:1488-1495);intrinsic native 方法(如 String.charAt)经 OptimizingCompiler::JniCompile 的 intrinsic 路径生成 AOT 方法体(optimizing_compiler.cc:1244-1277,TryCompileIntrinsic 调用 :1269),只有 quick_code == nullptr 时才回退 GenericJNI trampoline(image_writer.cc:3530-3534)。EntryPointFromJni 指向 dlsym lookup trampoline:@CriticalNative 用 kJNIDlsymLookupCriticalTrampoline,其余用 kJNIDlsymLookupTrampoline(image_writer.cc:3643-3661)。riscv64 的 lookup stub 在 a17r 的 runtime/arch/riscv64/native_entrypoints_riscv64.S:普通 stub 读 TopQuickFrame 的 access flags,含 Fast/Critical 位时走 artFindNativeMethodRunnable(),否则 artFindNativeMethod(),找到后尾调用,找不到返回让调用者处理异常(:54-92);@CriticalNative 专用 stub 还要处理隐藏参数 bit0(GenericJNI 标记)、栈参搬运与寄存器保存(jni_entrypoints_riscv64.S 对应宏)。

GenericJNI trampoline(quick_entrypoints_riscv64.S:294-383)建立 224 字节 SaveRefsAndArgs 帧,再预留 5120 字节(GENERIC_JNI_TRAMPOLINE_RESERVED_AREA)作为参数/返回值中转区,调 C++ artQuickGenericJniTrampoline()(quick_trampoline_entrypoints.cc:2048-2151):按 shorty 摆放参数、写 ArtMethod*|kGenericJniTag 隐藏槽;只有普通 native 才 artJniMethodStart,而 LRT 只对 @CriticalNative 跳过(quick_trampoline_entrypoints.cc:2105-2135),@FastNative 仍压/弹 LRT;native 代码取自 GetEntryPointFromJni()(可能就是 lookup stub,首次调用后由 ClassLinker::RegisterNative() 写回真实函数指针)。返回走 artQuickGenericJniEndTrampoline() → GenericJniMethodEnd()(普通 native 的 artJniMethodEnd、@FastNative 的 CheckSuspend、同步方法解锁、引用结果解码、LRT 弹出)。

符号查找与错误:JavaVMExt::FindCodeForNativeMethod/Libraries::FindNativeMethod 先短名后长名,只搜索与 declaring class loader 一致的库;失败串为 "No implementation found for <method> (tried <short> and <long>) - is the library loaded, e.g. System.loadLibrary?",最终变成 UnsatisfiedLinkError(java_vm_ext.cc:276-318;jni_entrypoints.cc:115-119)。RegisterNatives 按 name+signature 沿父类查找并校验 native 位,失败分别抛 NoSuchMethodError(jni_internal.cc:2701-2859);ClassLinker::RegisterNative() 对 @CriticalNative 若类尚未 visibly initialized 会延迟写 entrypoint(class_linker.cc:483-540)。

4.8.4 managed ABI 与 RISC-V psABI 的分界(案例相关)

4.8.5 异常、信号与线程挂起

JNI 抛异常只是设置线程 pending exception(jni_internal.cc:804-826);stub 在返回路径轮询并投递(jni_macro_assembler_riscv64.cc:538-554);引用返回值若已有异常则按 null 处理(quick_jni_entrypoints.cc:181-184)。隐式空指针依赖 sentinel page 与 fault handler:NullPointerHandler::Action 读取 fault 地址、把返回地址压栈、将 PC 改为 art_quick_throw_null_pointer_exception_from_signal(fault_handler_riscv64.cc:48-85);哨兵页在 Runtime::Init 预留(runtime.cc:1660-1683,失败打印 "Could not reserve sentinel fault page")。栈溢出由 guard page 触发 StackOverflowHandler(fault_handler_riscv64.cc:92-126;栈溢出预留字节:sanitizer 16384 / 非 sanitizer 8192,判据是 build/art.go:124-137 的 SanitizeDevice/Host,不是 debug/non-debug)。SIGBUS 仅在 gUseUserfaultfd 时被注册(fault_handler.cc:138-166),由 CMC 使用。一个重要的 RV64 缺口:SuspensionHandler::Action() 在 riscv64 未实现,直接 LOG(FATAL) "SuspensionHandler::Action is not implemented for RISC-V"(fault_handler_riscv64.cc:87-90)——这与“riscv64 没有隐式挂起检查”一致(4.3、4.7)。

4.8.6 native 库加载

Java 侧 System.loadLibrary 最终进 JavaVMExt::LoadNativeLibrary(java_vm_ext.cc:952-1168):按路径去重(同一库换 ClassLoader 会报 "Shared library ... already opened by ClassLoader ...")、android::OpenNativeLibrary 负责 namespace/dlopen(属 libnativeloader/BIONIC 范围)、JNI_OnLoad 调用与版本校验(失败串 "JNI_ERR returned from JNI_OnLoad ..."、"Bad JNI version ...")。

生命周期不能只看 :1060 的注释:该注释描述的是加载路径“dlopen 后没有配对的 dlclose”,结论只适用于“类卸载不发生时库常驻”。当前实现确实有类卸载驱动的卸载链:JavaVMExt::UnloadNativeLibraries 在 jni_libraries_lock_ 下扫描 libraries_,对 class_loader 非空且弱引用已清除(IsJWeakCleared)的库从表中摘除(java_vm_ext.cc:350-389);释放锁后调用 UnloadLibraries 依次执行 JNI_OnUnload(:398-412),再 delete SharedLibrary;SharedLibrary::~SharedLibrary 调 android::CloseNativeLibrary(即 dlclose 语义,:89-99)。boot classloader 的库不在这条类卸载路径:class_loader == nullptr 表示 boot classloader,UnloadNativeLibraries 明确不卸载它(:379-381);boot 库在 VM 销毁阶段由 UnloadBootNativeLibraries 统一 JNI_OnUnload(:235-249)。因此不能写“ART 没有类卸载通路”或“只靠未说明的引用计数归零”;准确说法是:加载路径不做配对 dlclose,普通 ClassLoader 的库在类卸载时经 UnloadNativeLibraries 卸载,boot 库留到 VM 关闭;旧注释只覆盖了加载路径的范围 [S43][S44]。

4.8.7 arraycopy:内建、native、memmove 三层

System.arraycopy 在 libcore 是 native(System.java:448),ART 的 runtime/intrinsics_list.h:188-191 同时登记 4 个内建(SystemArrayCopy 与 Byte/Char/Int 类型化)。通用 System.arraycopy(引用数组)内建的阈值是 128 个元素,不是 128 字节:intrinsics_riscv64.cc:1634 定义 kSystemArrayCopyThreshold=128,CreateSystemArrayCopyLocationSummary 只在常量长度 0 <= len < 128 时建立内建位置(code_generator.cc:1718-1727);运行期长度在 :1711-1715 直接与 128 比较,>=128 走 IntrinsicSlowPath,全程没有乘元素宽度。类型化 Byte/Char/Int 内建另有 kSystemArrayCopyPrimThreshold=384(字节数)除以元素大小得到的元素阈值:Byte 384、Char 192、Int 96(intrinsics_riscv64.cc:2000-2007)。因此“arraycopy 统一 128 字节阈值”是错的;应写成“通用引用路径 128 元素、typed 路径按 384B/元素宽度换算,常量与运行长度分别判定”。

VisitSystemArrayCopy 先做空指针/重叠方向、源/目标边界、类型系统检查,引用数组带 Baker 读屏障慢路径,其余转 IntrinsicSlowPath(:1636-1830 区段,4.5.5)。

未内建时进入 native 实现:runtime/native/java_lang_System.cc:50 System_arraycopy——同类型 primitive 数组按宽度走 Memmove();引用数组走 ObjectArray::AssignableMemmove()(元素级搬移 + 写屏障);类型不兼容抛 ArrayStoreException;不同引用类型且可证明不重叠时用 AssignableMemcpy,否则逐元素检查。mirror::PrimitiveArray::Memmove(array-inl.h:134-173)对 byte 用 libc memmove,对 16/32/64 位元素按 copy_forward 选择前向/后向拷贝——注释明确不能用 libc memcpy/memmove 搬非 byte 元素,否则可能破坏“不得 word tearing”的数组语义。同一调用因此有三条路径:内建快路径、内建慢路径→native、未编译时的 JNI native,与 String.equals 的两条路径同构 [S35][S44]。

4.8.8 案例连接与观测

下一章转入上游演进与支持分层,把“源码落地/测试/发布/产品”四层逐项对齐。

第 9 章(4.9)上游演进与支持状态分层:从提交到产品可用

本章回答:RV64 支持在公开 ART 里怎样一步步落地;每条能力目前停在哪一层(源码/测试/发布/产品);哪些说法是事实、哪些是判断。历史事实来自公开 git(有界范围复核),上游测试记录只作为“外部记录”转述,不是本报告实测。

4.9.1 分支模型与 pin(2026-10-19 复核)

线reftiptip 提交日期tag
冻结主干main6484611f2025-03-26无
25Q2android16-releaseed6c006b2025-04-09android-16.0.0_r1=r2
25Q3android16-qpr1-releasee5c642b52025-07-18android-16.0.0_r3
25Q4android16-qpr2-release1690c6912025-10-12android-16.0.0_r4
26Q2android17-releaseb753cf972026-04-30android-17.0.0_r1
26Q2 安全android17-security-releaseecd0dfa42026-07-10无

关键拓扑(本轮复核):main 是 17 线的祖先(merge-base --is-ancestor 6484611f b753cf97 为真),但 android17-security-release 不是 android17-release 的后代——两者 merge-base 为 d9ee3abab2(2026-03-27 的 Snapshot),安全线在其上只多两个提交:70be2043e6("Make the runtime-generated app image read-only",2026-05-15)与 ecd0dfa4(2026-07-10 的 cherry-pick 合并),均与 RISC-V 无关;android16-security-release 同样不是 android16-qpr2-release 的后代。公开 refs 中仍无 android18/26Q4 分支(2026-10-19 ls-remote 与本地 for-each-ref 双重核对)[S01][S33]。

【判断】“最新公开开发前沿”是 26Q2 release 分支与其安全线,不是 main;引用“上游当前支持”必须写 release 分支 + commit。把 main 当 AOSP17 会系统性低估 2025-04 之后的能力。

4.9.2 RV64 的四个阶段(代表提交与外部记录)

阶段 1:骨架(2023-01–04,Android 14 窗口)。Lifang Xia(linux.alibaba.com)与 Ulya Trafimovich/Jaeheon Yi(google.com)交替提交:寄存器/features/context/callee-save(394551feac/910c95fe2d/7923ae2c8f/e2c7f90282)、初始支持(4184f23701)、入口与 JNI lookup stub(57990cc39e/9e8a95c5fc)、影子栈从 X18 改为 GP(5c9937d48e)、nterp 以 unimp 建桩(e98bd2994b)[S33]。

A14 的实态(负例):android14-release 树中 riscv 相关文件 27 个,没有汇编器、intrinsics、JNI 调用约定与 code_generator_riscv64.cc(只有头文件);IsNterpSupported() 明确排除 riscv64(nterp_impl.cc:29-32,kRuntimeISA != InstructionSet::kRiscv64),且 a14 tip 不包含 nterp 启用提交 993bcb1f02。因此 A14 的 RV64 只是源码接入骨架,本报告未证明其可启动或可用;用“目录里有 mterp/riscv64”推断支持是典型的“目录存在≠能力”错误 [S33][S38]。

阶段 2:成为后端(2023-05–12)。汇编器 part 1–6(60fdcb2b27 等,Test 仅 host-gtest)、codegen 全量构建(bd3d6bb079→59dd25827b→12e1aff4d3→4ffd7596ff 与 Visit* 系列)、JNI 约定与编译器(0a9a7f4083→8838bbf1f5)、intrinsics 文件建立(303d061d79)、relative patcher(e4e39b2b1e)、反汇编器(06f1fc0117)。两个关键外部记录:993bcb1f02(2023-06-15,nterp 带方法过滤启用;提交信息给出 Cuttlefish riscv64 启动后检查 zygote 无 SIGILL 的步骤)、8e47d43161(2023-10-24,“enable codegen”;提交信息给出 testrunner.py --target --64 --optimizing --jit 的失败计数 optimizing 3→1、jit 18→4)。这两条是“RV64 的 AOT/JIT 双路径被上游实际运行过”的公开锚点,但属于外部测试记录,本轮未复跑 [S33]。

阶段 3:语义与内建(2024–2025 初)。C 扩展汇编/反汇编(fddd6a4c78、0537cf4696)与 auto-compression 系列;Zba/Zbb(b1a0d10552 等;Zbs 进入 a16);InstructionSimplifier ShiftAdd(d923c47f74,带 458-checker 用例);nterp 异常捕获(f1e2ce6390,测试 839-clinit-throw,附 VM over SSH 步骤);字符串/数学/原子内建(91bb8a6fac StringEquals、8159940d6c StringCompareTo、53da12c60f、bda2905666 等);MethodHandle invokeExact 四类补齐(9f681acb57/5d16fdff4f/54710ff675)[S33]。

阶段 4:维护与前沿基础设施(2025–2026)。bit-manip IR(022f0bd86b,Revert 后 d66489916b Reland,带 checker 测试)、nterp 维护(02c0770682、f0ed2f1ea3)、Orange Pi RV2 测试载体(c2c3494e65)、arraycopy 收尾(394dc10e12/8cc27702db)、内建构建器统一(6e0315e779)、向量寄存器分配 part 1(dde31eea21,仅 host-gtest)。按作者日期,a17r 线上最后一条 riscv 改动是 34220d8f43(2026-03-24,通用优化);此后到安全线(2026-07-10)没有 RISC-V 提交 [S33]。

4.9.3 支持状态分层:四层口径,不跳级

能力① 源码落地② 构建/测试验证(上游记录)③ 首次进入 release④ 产品可用
寄存器/上下文/入口骨架2023-01 批无 Test 行(早期原型)a14(仅骨架)无公开承诺
汇编器 + C/Zba/Zbb/Zbs60fdcb2b27…、fddd6a4c78、b1a0d10552、b17478f8e8多为 host-gtest;部分 run-gtestsa15(Zbs 为 a16)无公开承诺
codegen 全指令 + JNI + nterp8e47d43161、8838bbf1f5、993bcb1f02target --optimizing --jit;Cuttlefish zygote 冒烟;VM over SSHa15无公开承诺
字符串/数学/原子内建、MethodHandle303d061d79 起、91bb8a6fac、bda2905666、54710ff675 等host-gtest 与 target run-test 混合a15→a17r 逐步增加无公开承诺
RVV 汇编/反汇编fb0a1bb8e6、036526f5f1、5512b4e610汇编器测试(无向量 codegen 测试)a15无公开承诺
向量寄存器分配 part 1dde31eea21仅 host-gtesta17r无公开承诺
fast baseline compiler仅 ARM64 实现(SupportsFastCompiler 硬编码 kRuntimeISA == kArm64;FastCompiler::Compile 无 RV64 分支)无 RV64 记录不适用无
Orange Pi 测试载体c2c3494e65run-gtests.sh + target --optimizing;TEST_ONLYa17r测试设施,非产品能力

④ 层(产品可用)在本轮可查的公开来源与 2026-10-19 截点内未见承诺:公开仓能查到的运行载体只有 Cuttlefish aosp_cf_riscv64_phone 与 Orange Pi RV2 测试支持;本轮未找到产品配置、CTS/VTS 结果或兼容性声明,但这不等于全域/未来不存在。② 层大量是 host-only:m test-art-host-gtest 只证明 host 编译与单元测试,target 运行记录集中在少数提交。③ 层的“包含”用 git merge-base --is-ancestor 判定,不是“树里有同名文件”;本轮抽查中 62ff25fbd7 只进 25Q4、b17478f8e8 只进 a16、c2c3494e65/dde31eea21/394dc10e12/6e0315e779 只进 26Q2 [S33]。

4.9.4 待合并、厂商分支与预测(分开写)

在审候选快照(2026-10-19,查询式记录)

方法:urllib GET 官方 changes API,n=100、_more_changes=false,UTC 时间 2026-10-19T15:13:22Z;raw SHA 分别为 bfdb6fae…(riscv)、266da2f2…(riscv64)、de556a54…(file:riscv64)。以下 18 条为去重后的 riscv64 查询结果(file: 与 riscv 查询是其子集),分支为 17 条 main + 1 条 master。机器表字段 merged_equivalent 只表示“按 Change-Id + branch --contains 找到的合入记录”,不表示当前 patchset 逐字节等价;本报告不把它升级为语义等价结论。

change最后更新主题(截断)合入谱系与语义核对状态
36939912025-08-11riscv64: Optimize IR using bit manipulation相同 Change-Id 的合入谱系已找到(022f0bd86b,在 a17r);当前 patchset 与合入版的逐 diff/完整语义等价未核
35807722025-04-23riscv64: Call target method in accessor MHs when it is set相同 Change-Id 的合入谱系已找到(1c7e13a9b9,在 a17r);当前 patchset 语义等价未核
35565062025-07-18riscv64: Optimize IR using bit manipulation与 3693991 同 subject、不同 Change-Id;未找到同 Change-Id 合入记录,语义等价未核,不能据此断言该优化功能不存在
35565052025-05-22riscv64: Optimize Div and Rem with any constant未找到同 Change-Id 合入记录;语义等价未核,不能断言“缺该功能”
32380382024-08-23riscv64: Correction of comment to funct3 ROR instruction注释修正类,无机制影响
31057602024-07-25[NOT FOR MERGE] Provide stable base for non-rebased patches排除项(显式 NOT FOR MERGE)
30762282024-05-07x86-64: Clean up locking in JNI stubs非 RV64(查询噪声)
28169762024-08-13riscv64: update vm img and qemu path测试环境类
28137312023-11-03riscv64: Guard Zbb usage with __riscv_zbb门控类;a17r 已有特性消费链;未找到同 Change-Id 合入记录,语义等价未核(“同上”同此口径)
27971482023-11-03riscv64: Guard Zbs usage with __riscv_zbs同上
27971472023-11-03riscv64: Guard Zba usage with __riscv_zba同上
27971442023-11-01riscv64: Gate Zba usage behind the instruction set feature同上
27969332023-11-01riscv64: Gate Zbb usage behind the instruction set feature同上
27565312024-08-13riscv64: boot VM with vector extensionVM/测试启动项,不是 RVV codegen;与 26Q2 codegen FATAL 不矛盾
26731352023-09-29arm64: store tlab information in the marking register非 RV64;作为“arm64 有 marking register”的对照项
25172962023-07-12riscv64: Let odrefresh run in verification mode旧候选;与 26Q2 odrefresh 默认 speed-profile 的关系未核
23188222024-06-10Minimal riscv64 support for running tests on the interpreter旧解释器测试支持;不改变 26Q2 能力判断
22417502022-10-06Revert "Fix panics in art build code when target arch is riscv64"旧 master 目标的 open/NEW 快照;不按日期/分支推断已失效

结论边界:这些 NEW 候选的最后更新集中在 2023–2025,不能当作 2026 的活跃路线或发布承诺;3693991/3580772 只核到“相同 Change-Id 的合入谱系”,3556506 只核到“同 subject、不同 Change-Id”,三者的当前 patchset 语义等价都未核。这再次说明“在审 NEW”“已合并”与“发行分支缺功能”不是同一个命题。未做逐条 detail/diff 精读,因此仍不算完整枚举(G1 仍保留,但已从“无法获取”降为“查询式候选已得、谱系/细节未全核”)。

4.9.5 版本差异实证(与案例/后续优化直接相关)

4.9.6 失败/反例与观测方法

成本与权衡(史料口径):release 分支“包含”某 commit 的判定成本低(祖先关系),但不能说明产品可用;host gtest 成本低、覆盖有限;target 运行记录更可信,但依赖 Cuttlefish/VM/SBC,样本少;在审 Gerrit 信息最完整,但本轮的通道只支持查询式快照。把这些不同成本的信号混为一谈,就会得出“上游已支持产品”的错误结论 [S33][S46]。

下一章把这些机制收束为可执行的优化地图:每个现象对应哪类假设、需要什么观测、改哪一层、用什么正确性门验收。

第 10 章(4.10)优化地图:从现象到可验证假设

本章不给“提升百分比”,只给可执行的研究路径:现象→候选假设→需要什么观测→改动边界→正确性门。每条都标注“事实”或“假设”,并与前文章节互链。设备测量本报告未做。

4.10.1 现象→假设表

#现象(如何观察)候选假设(含依据章节)需要的观测改动边界正确性门状态
M1同一字符串密集型启动路径在 RV64 与 arm64 上差异明显,但代码/Android 版本相同① 执行模式不同(解释态 charAt 是逐字符 quick 调用:AOT intrinsic 方法体或 GenericJNI 回退,见 4.1/4.3);② RV64 String.equals 内建缺 const-string 特化(4.5.3);③ AOT 覆盖不同(4.4)先固定模式:debuggable=0、过滤器、profile 快照;oatdump 看 equals 调用点方法是否 AOT;-verbose:jit 看编译;PMU 看指令数/分支 missART:intrinsics_riscv64.cc(M1-②可改);libcore:不改语义;构建:过滤器/profile458-checker 用例 + 随机字符串对拍(Latin1/UTF16/空串/常量串);A/B 同构建①事实 ②事实(缺特化)③机制解释;收益=假设
M2System.arraycopy 在数组拷贝密集路径耗时内建 vs native 路径选择;通用引用路径 128 元素、typed 路径 384B/元素宽度换算(4.5.5/4.8.7)oatdump 看内建是否生效;simpleperf 看 System_arraycopy 符号占比;typed arraycopy 用例ART:intrinsics_riscv64.cc;bionic:memmove 质量(BIONIC 范围)类型化 arraycopy 测试(越界/重叠/引用数组读写屏障)+ GC 压力事实(三路径);收益=假设
M3并发分配/字段访问在 GC 活跃期变慢riscv64 无 marking register,读屏障逐寄存器 load(4.5/4.6)PMU:读屏障相关 load 比例(需能归因到方法);GC 日志的暂停/并发阶段;A/B 同 workloadART:codegen/ABI(寄存器预算,需评估);不可改 libcoreCC 的 to-space 不变量断言 + CMC 页故障测试 + GC stress + 全部 run-test事实(无 marking reg);收益与可行性=假设
M4高争用 CAS/锁场景性能与 arm64 差异CAS 用 LR/SC 循环;get-and-update 完整字宽走 AMO、带 mask 的 32 位路径走 LR/SC;约束序列 16 条(4.5.4)高争用 microbench;PMU:LR 失败/分支计数;futex/锁慢路径计数ART:assembler/intrinsics;硬件:Zacas 可用性(数据)内存模型 litmus(自建)+ concurrency tests;不得改变语义事实(LR/SC+AMO);差异=假设
M5调试构建/-Xint 下启动显著慢-Xint 禁 nterp/强制 switch;debuggable 禁 nterp 与 AOT Java 代码,但 JIT 仍可生成带 hook 的代码(4.3/4.4)对比 debuggable 与 -Xint 两种开关的 logcat/JIT 状态、oatdump无需改代码;测试口径修正同构建、同过滤器、同 profile 的 A/B事实(机制);影响=需测
M6RVV 向量化机会(字符串/数组批量操作)当前 codegen 无向量指令(FATAL),自动向量化未落地(4.5.6)上游提交雷达(part 2+ 是否进入 release);汇编器测试覆盖ART:编译器(前沿方向,不能自行实现于本报告)向量化 vs 标量等值对拍事实(未实现);节奏=观测
M7目标硬件与 AOSP 构建 ISA 假设不一致(SIGILL/非法指令)运行期探测未实现(FromCpuInfo/Hwcap=UNIMPLEMENTED→FromCppDefines,4.5.1)读取 /proc/cpuinfo/auxv 与二进制 -march 声明对照;在目标硬件跑最小 ELF构建配置/工具链(短期);ART 运行期探测(长期,需上游)不同 ISA 组合的启动/回退测试(可能不存在回退)事实(探测缺省);风险=按目标核
M8AOT 覆盖不佳导致“启动后仍在解释/刚 JIT”profile 质量/过滤器/odrefresh 工件(4.4.1/4.9.5)profman --dump-info-for、oatdump 方法级 AOT 状态、开机 odrefresh 日志构建/配置/odrefresh;ART 不改为先启动 macrobench 同样本 A/B(需设备)事实(机制);覆盖率=需测
M9JIT 热方法达峰速度与 arm64 不同无 fast tier/pattern matcher(4.4.2)-verbose:jit 时间线;启动期编译事件;PMUART:JIT(跨架构特性,优先级需上游)run-test jit + 同用例 ns/op 稳定性事实(arm 专属);幅度=假设

4.10.2 观测栈的最小集(条件与口径)

4.10.3 问题归属

归属典型问题说明
ART 编译器/内建M1-②、M2、M3、M4、M6改 intrinsics_riscv64.cc/code_generator_riscv64.cc/assembler;需遵守 ABI
ART 运行时/JIT/profilesM1-③、M8、M9入口点、过滤器、profile、odrefresh 工件
libcoreM1-①(语义源头)、字符串/数组 API 形态本报告不改 libcore;改动需评估兼容性
bionic/工具链M2(memmove)、M7(ISA/ABI/构建)Bionic 内部机制由独立专题说明;本报告只给接口点
硬件/PMU 数据M3、M4、M6、M7需要核/板卡数据与事件支持;无数据则只能标记假设

4.10.4 正确性门与实验模板

  1. A/B 模板:同一构建参数、同一设备状态,只改一个变量(内建路径或模式),交替运行 ≥3 轮,记录中位数与离散度;报告“效应量+条件”,不写无条件的提升。
  2. 功能门:任何 ART 编译器/内建改动,先跑 test/458-checker-riscv64-* 与相关 run-test(解释/JIT/AOT 三模式);字符串/数组另加随机对拍(compressed/UTF16、空串、常量串、越界与 NPE)。
  3. 并发门:原子/锁/屏障改动需跑 GC stress、并发测试与自建 litmus;CC 场景验证 to-space 不变量。
  4. 性能与正确性分开陈述:性能数字不得暗示正确性;正确性未验证时不得写“优化完成”。
  5. 版本门:RV64 结论固定到 release commit;若上游后续 QPR 带新提交,按 4.9.6 的核查链增量复核。

4.10.5 与既有工作/数据的边界

本表的失效边界与成本口径:每条假设都依赖前文的机制事实(内建是否存在、屏障是否可用、特性是否可探测);一旦这些前提在目标 pin/QPR 变化,假设需要重排。观测成本从低到高为:日志/oatdump(低)→ run-test/checker(中)→ PMU/simpleperf(高,需权限与事件支持)→ litmus/并发压力(高,需自建)。涉及 ART 编译器的改动必须同时过 458-checker-riscv64-* 与 intrinsics_riscv64.cc 相关 run-test;涉及屏障/原子的改动必须过 GC stress 与 litmus;这些门的执行成本应计入计划,不能只看收益 [S11][S30][S38][S43]。

本报告未使用任何用户私有性能数据、内部实现或未公开结果;M1–M9 是公开源码推导出的假设清单,不代表已经发生的优化。若后续把其中某项做成改动,必须按上面的模板重新测量与验收,不能引用本报告的“机制解释”当成绩效结论。

回到源码的核对入口举例:IsNterpSupported() / CanRuntimeUseNterp()(nterp.cc)、VisitStringEquals()(intrinsics_riscv64.cc:1152)、GenerateMemoryBarrier()(code_generator_riscv64.cc:6232)与 System_arraycopy()(java_lang_System.cc:50)——每条 M 项都应能落到这类入口及其条件 [S11][S30][S38][S43]。

第 11 章(4.11)Benchmark 与测量方法:从测试设施到 目标 RISC-V64 平台/Arm64 对照平台 前置实验

本章回答三个问题:ART 里到底有哪些 benchmark 与测试设施,它们分别测性能还是只测正确性;libcore 只作代表链交叉引用(完整范围由 libcore 作者负责,不在此重复枚举);怎样把一条 benchmark 的调用点、执行模式与机器码身份固定下来;面向下一阶段 目标 RISC-V64 平台 不同 可重构平台/硬件配置 与 Arm Arm64 对照平台 的对照,应记录哪些条件、怎样把标准原始 harness 的 ns/op 与 Fixed-N 研发归因分开。所有 ART 清点以 android17-release b753cf97 的固定 pin 为准;libcore 只按公开 pin 4ebfb391… 做代表链交叉引用,完整范围归 libcore 作者。机制连接沿用 [S12][S13][S17][S20][S28][S30][S35][S37][S43];本报告没有在设备上运行任何 benchmark,本章不含实测成绩。

4.11.1 先分清“测试通过”与“性能结果”

ART 与 libcore 的测试设施可以分成四层,不能混读:

设施典型入口主要目的是否可作为性能依据
ART Java microbenchmark 源benchmark/*/src/*.java(time*(count) 方法、info.txt)微基准源码,按调用/字节码/内建组织只有在固定 harness、模式、输入与重复规则后才是性能数据;本 pin 的 Java runner 不在树内
ART 临时 perf run-testtest/2239-varhandle-perf-vh-*(17 组)VarHandle/原子访问的临时微基准是性能微基准,但不是通用 suite;run.py 默认清空 stdout 以让 diff 通过,输出需另行保存
ART run-test / gtesttest/run-test、1210 个 test/ 目录;24 个 art_standalone_*_tests正确性、回归、异常/边界、GC/编译检查否;run-test 通过不等于性能达标
libcore Caliper / Jetpack(交叉引用)benchmarks/src 的代表性类(StringEquals/MethodInvocation/StringIteration/ArrayCopy/ReferenceGet);完整家族清点由 libcore 作者负责Java API/算法/并发/反射等微基准是性能 benchmark;本章只引用代表链,不重复全量枚举

test/run-test 的默认模式是 --optimizing,同时提供 --interpreter、--jit、--baseline、--no-image、--prebuild/--no-prebuild、--gcstress、--gcverify、--continuous-gc、--64、--simpleperf 等开关(test/run-test:253-315)。这些开关改变的是执行体与观测条件,不是“测试通过”的同义词;同一个用例在 --interpreter 与 --optimizing 下的耗时不可混为一个 cell。24 个 standalone gtest target 覆盖 compiler、dex2oat、runtime、GC、oatdump、profman、odrefresh 等(*/Android.bp 的 art_standalone_*_tests),它们是单元/正确性门,不产出 ns/op。

4.11.2 ART 全 scope 与 libcore 代表链:家族、文件、方法与包含排除

共同方法 R1 的三层范围(方法学参考,非新目标平台 结果):L1 是历史输入/旧账本,已重算但只是历史行数;L2 是公开固定 pin 的源码/机制枚举(本章 ART 部分属于此层);L3 是未来 目标平台 新 run 的实际 eligible manifest/harness/AOT/odex/runtime 门,当前 unknown。三层不能互换:本章不拿历史行数当新目标平台 scope,不拿 vendor/历史包当公开 pin 依据,也不把旧历史协议值当新协议。标准原始 harness 与 Fixed-N 分轨;AOT 是否真正生效必须由镜像/odex/入口/机器码确认,不能因源码登记了 intrinsic 就推断。

ART Java benchmark:benchmark/ 下 8 个家族(const-class、const-string、jni-perf、jobject-benchmark、scoped-primitive-array、stringbuilder-append、string-indexof、type-check),8 个 src/*.java,按 time*(...) 声明计 57 个方法;其中 jobject-benchmark 的 7 个 time* 是 native,实现在 jobject-benchmark/jobject_benchmark.cc。benchmark/Android.bp 只构建 libartbenchmark/libgolemtiagent 的 C++ 库,Java 侧 runner、warmup 与 reps 不在本 pin 的树内;因此本章只把它列为“有源码的微基准家族”,不把某个历史 harness 的默认值当作 pin 事实。按 void time*(...) 声明逐家族清点(含 native 声明,按方法名去重;参数轴不是运行参数矩阵):

家族方法数参数轴(方法名/输入类别)
const-class2timeConstClassWithConflict / WithoutConflict
const-string2timeConstStringsWithConflict / WithoutConflict
jni-perf4timeFastJNI / timeEmptyCall / timeSOACall / timeSOAUncheckedCall
jobject-benchmark7local/global/weak global 的 add-remove/decode + handle scope decode(7 个 native)
scoped-primitive-array12Byte/Short/Int/Long × small/medium/large
stringbuilder-append8String、long String、String+int/double/float、huge double/float、混合
string-indexof12目标字符 0/1/2/3/4/7/8/F/G/V/W/_
type-check10checkcast/instanceof × 1→1、2→1、3→1、9→1、9→2

这些方法名是源码声明计数与参数线索,不是运行次数;源码声明数不会因重复测量或参数展开增加。运行 cell 须以方法、参数/输入配置和执行模式标识,reps/N 另记工作量;不能把不同输入配置合并成同一 cell。jobject-benchmark 的 native 方法还依赖 jobject_benchmark.cc 的 JNI 实现;本地未构建该库,不能把声明数当已执行数。

ART VarHandle perf run-test:test/2239-varhandle-perf-vh-* 共 17 个目录、51 个 Java 源文件,覆盖 VarHandle 的 get/set/CAS/weak CAS/反射 get/set/Unsafe 对照等。src/BenchmarkBase.java 的默认 measure() 是 100ms warmup + 1000ms 计时,MicroBenchmark 每次 exercise() 跑 1000 次 run();run.py 调 default_run,随后打印并清空 stdout。它是 run-test 内的临时性能微基准,不是 AOSP 的完整性能 suite,也不能用“run-test 通过”代替分数。

ART 正确性设施:test/ 本 pin 有 1210 个目录(其中 1145 个数字编号目录),TEST_MAPPING 登记 art_standalone_* 等 presubmit;test/458-checker-riscv64-* 是本报告相关的 RV64 checker 用例(458-checker-riscv64-bit-manipulations、458-checker-riscv64-shift-add 等)。这些用例可以证明指令/IR 语义与回归,不提供性能结论;其中少数用例(如 055-enum-performance、133-static-invoke-super、2029-contended-monitors)含计时打印,也只是临时观测,不是 suite 级 benchmark。

libcore Caliper(代表链交叉引用,不重复全量清点):benchmarks/Android.bp 构建 benchmarks(java_test,依赖 Caliper)与 LibcoreBenchmarkTests(Jetpack);README 的运行入口是 vogar --benchmark <file>(需 m vogar com.android.art.testing、root/remount,必要时 adb shell stop 降噪)或 atest LibcoreBenchmarkTests:...。本章只引用 StringEqualsBenchmark、MethodInvocationBenchmark、StringIterationBenchmark、ArrayCopyBenchmark、ReferenceGetBenchmark 等代表文件来连接 ART 机制;libcore 的完整 benchmark 家族、方法与参数清点由 libcore 作者负责,本报告不把任何本地镜像计数或抽样当成公开 pin 4ebfb391 的全清单。Caliper 的 warmup/reps 由 harness/参数控制,源码只给出 time*(int reps) 形态;必须记录实际 harness 版本与参数。

libcore Jetpack(交叉引用):benchmarks/src_androidx 的 Jetpack 入口走 LibcoreBenchmarkTests/atest;其计时/温控由 Jetpack Benchmark 控制,不能与 Caliper 分数直接拼接。完整范围同样由 libcore 作者维护。

未取得公开来源的其它 harness:本报告不纳入;不与 AOSP 公开 benchmark 混在同一张分数表,也不引用未公开历史结果。

包含/排除:本章包含 ART benchmark/run-test/gtest 与 libcore 代表链、harness 入口、执行模式与观测字段;排除未公开的内部套件、设备原始成绩、未公开路径、未固定 pin 的第三方 benchmark,以及把 run-test/gtest 通过当作性能结论的做法。

4.11.3 深链 A:调用派发与 String.equals 内建

libcore StringEqualsBenchmark 是最贴近本报告纵切案例的公开 benchmark:8 个 time* 方法,分别覆盖 null、short、medium、long、very-long、non-word-aligned、end-different、non-String 八类;每个方法内部对 5 组字符串对做 String.equals,外层按 Caliper 传入的 reps 循环。benchmarks/src/benchmarks/regression/StringEqualsBenchmark.java:197-266 的 timeEquals* 只做比较,不分配结果、不写 checksum,返回值被丢弃;:185-193 的 @BeforeExperiment setUp() 只用 Assert 验证常量/new String/intern/常量折叠假设,不证明完整 equals 算法正确,也不排除 DCE。AOT/JIT 下结果未被消费时,调用是否保留要由机器码/结果 sink 对照确认;本报告未做该对照。若加 checksum/sink,只能作为新 diagnostic variant,不能悄悄替换标准 benchmark 后仍按同一 cell 报成绩;本报告也未做随机对拍。性能仍由 Caliper 计时。

它的调用链必须按两条轴拆开:调用点是否被编译并识别为 kStringEquals 内建;callee String.equals 的 ArtMethod 入口是 AOT 整方法 intrinsic body、普通 DEX graph 编译代码、nterp 还是 bridge。StringEqualsBenchmark 的 equals 调用点位于 timeEquals* 方法内;若 timeEquals* 被 AOT/JIT 编译且该 invoke 被内建,执行体是调用点内的 VisitStringEquals 序列(RV64:两次 Loadwu 比较 klass_、count 比较、8 字节 Ld 循环);若未内建,则进入 callee 的入口。callee 自身若是 boot-image 已生成的整方法 intrinsic body,就不再执行 libcore 的 Java 循环;若走普通 DEX graph 编译,才是 charAt 循环 + HArrayGet;若 callee 未编译,才是 nterp Java 循环。不能从“String.equals 登记为 intrinsic”推出本次调用点已内建,也不能从“callee 有 AOT 代码”推出调用点内建。

MethodInvocationBenchmark 提供同一链的“普通派发”对照:13 个 time* 声明覆盖内部 getter/字段、static/virtual/interface 空调用、private/protected/public/final 方法等;其中 timeInternalGetter/timeInternalFieldAccess 两个声明在嵌套 helper 类 C 内(由其顶层同名方法调用),顶层 benchmark entry 为 11 个,按 runner cell 计应写 11。这些方法可用于把“调用点内建/去虚化/内联”与“callee 入口”分开。ReflectionBenchmark 的 Method.invoke/Field.getInt 等方法则把反射入口与普通 invoke 分开;它们不是同一成本模型,不能合并成一个“调用开销”数字。

正确性门:StringEqualsBenchmark 的 setup Assert 只覆盖对象/常量假设,不能替代完整等于语义验证;若要用它做性能 cell,应另做独立对拍(Latin1/UTF16、空串、常量串、同引用、不同长度、非 String),并明确 checksum/sink 变体只作诊断、不是原 benchmark 的同一 cell。任何内建或去虚化改动先过 458-checker-riscv64-* 与相关 run-test,再谈性能;本报告未做随机对拍,也不把结果未消费的计时当作 DCE 已排除。优化候选只列机制假设:调用点内建覆盖率、callee AOT intrinsic body 是否实际生成、const-string 特化缺失(ARM64 有、RV64 无)等;不把“源码有内建”写成“本轮调用点已内建” [S12][S13][S20]。

4.11.4 深链 B:编译模式与 intrinsic/arraycopy

StringIterationBenchmark 有 4 个 time*:两种 charAt 循环(缓存/不缓存 length())、toCharArray 后数组遍历、单独 toCharArray。它把 charAt 内建(compiler/optimizing/instruction_builder.cc:2125-2132 生成 HArrayGet(..., kUint16, is_string_char_at=true),:2225-2236 追加 HArrayLength(is_string_length=true) 与 HBoundsCheck(is_string_char_at=true))、length 内建、数组分配与遍历放在同一文件,但每个方法应作为独立 cell;toCharArray 同时测分配/复制,不能与纯读取混在一起。这里的 IR 是编译器把 String 当作不可变数组处理,不是把 String 写成普通 byte[] 布局,也不能据此推断 callee 的 String.charAt native/AOT 入口。

ArrayCopyBenchmark 有 4 个方法:手写 8192 元素循环、System.arraycopy、Arrays.copyOf、Arrays.copyOfRange;src 在循环外分配一次,dst 每 rep 分配新的 char[8192],因此同时包含分配与复制。SystemArrayCopyBenchmark 只覆盖 8 个 primitive typed 方法(Boolean/Byte/Char/Double/Float/Int/Long/Short),没有 reference-array 方法;引用数组由通用 System.arraycopy/ART SystemArrayCopy intrinsic 用例另行覆盖;该类的 src/dst 都在循环外一次分配,每 rep 只做复制。这里必须沿用本报告已修正的量纲:通用引用路径 128 元素;typed 路径 384B/元素宽度(Byte 384、Char 192、Int 96);不是统一 128 字节。StringIndexOfBenchmark 的 12 个 time* 覆盖不同目标字符与 $noinline$indexOf 包装,用来观察 String.indexOf 的 callee/内建路径;StringDexCacheBenchmark 则把字符串常量/DEX cache 访问单独列出。

执行模式:ART benchmark 类本身不含 runner;若用外部 harness,必须记录其编译模式、warmup/reps、输入长度和是否预编译。libcore Caliper 通过 vogar --benchmark 在设备上运行;Caliper 的 VM 选项用 -J 传入(README 的示例),默认执行体由设备 boot image/odex/filter/profile 决定。ART run-test 则用 --interpreter/--jit/--optimizing/--baseline/--no-image 显式切换模式。同一个 cell 只能比较同模式、同输入、同 callee 身份的结果;--optimizing 与 --interpreter 的 ns/op 不是“架构差异”。

正确性门:System.arraycopy 的越界/重叠/引用数组写屏障、typed 元素宽度、String.charAt 越界异常、String.indexOf 返回值与异常路径,应通过 checker/run-test 与独立对拍验证(本报告未执行);run-test 通过只是功能门,不能代替性能测量。优化候选:charAt/length 内建覆盖、arraycopy 阈值命中率、typed 内建是否被调用、编译模式切换对 callee body 的影响 [S12][S17][S20][S35];这些都需要在 目标 RISC-V64 平台/Arm64 对照平台 上按 cell 验证。

4.11.5 深链 C:分配、GC 与 read barrier

ArrayCopyBenchmark 的 src 在循环外分配一次、dst 每 rep 分配 char[8192];StringIterationBenchmark.timeStringToCharArray 每 rep 分配 char[];StringBuilder/Reference 家族也含分配。它们不是纯 CPU 微基准:TLAB 用尽会进入 AllocateInternalWithGc,GC 类型(CC/CMC)、read barrier、卡片写屏障与暂停都会进入时间。ReferenceGetBenchmark 有 3 个方法(soft/weak/non-preserved weak),并用 @Param boolean intrinsicDisabled 控制 Reference.get 的 intrinsic 开关;ReferenceBenchmark 进一步覆盖引用对象生命周期。它们适合观察 Reference.get 快路径、GC 后 referent 清理/保留与 GetReferent 的屏障。

按本报告已核的 GC 条件,解释结果时必须区分:CC 的 STW 至少两次(FlipThreadRoots 与 CopyingPhase 内关闭弱引用访问的 ScopedPause;可选 no-from-space 验证另计);CMC 的 black allocation 边界在 MarkingPause 的页对齐快照之后,而不是 MarkingPhase 期间;RV64 启用读屏障时只支持 Baker 的逐寄存器入口,CMC 的 uffd 路径不使用读屏障。分配/引用 benchmark 的耗时不能只归因到“分配指令”,要同时记录 GC 日志/阶段、TLAB/慢路径、GC 类型与是否发生 compaction。

正确性门:GC stress/verify、引用保留/清理语义(soft/weak/finalizer)、ArrayStore/Bounds、Reference.get 返回 null/对象与异常路径;任何“优化”如果改变引用可见性或清软引用时机,直接拒绝。成本口径:分配速率、TLAB 慢路径次数、GC 暂停/并发阶段、屏障指令占比分别记录;不把 ns/op 直接解释为 GC 时间 [S30][S43]。

4.11.6 深链 D:JNI、FastNative、原子与 monitor

benchmark/jni-perf 有 4 个 time*:timeFastJNI、timeEmptyCall、timeSOACall、timeSOAUncheckedCall;C++ 侧 perf_jni.cc 提供 native 实现。jobject-benchmark 的 7 个 native time* 覆盖 local/global/weak global 引用与 handle scope 的 add/remove/decode;scoped-primitive-array 覆盖 ScopedPrimitiveArray 的获取/释放。它们直接对应 JNI 边界,但不能把所有方法都当成同一种 JNI 形态:普通 native 会做 Runnable→Native 与 artJniMethodStart/End;@FastNative 保持 Runnable、不调用 artJniMethodStart,但仍建立/弹出 LRT;@CriticalNative 才跳过 JNIEnv/LRT/异常轮询;它改变的是该调用线程的 JNI 状态转换/挂起检查边界,不能推成整个 VM 停止并发 GC 工作。FastNative 的 LRT 是已核事实,不能回退成“FastNative 不建 LRT”。

test/2239-varhandle-perf-vh-* 的 17 组用例把 VarHandle get/set/CAS/weak CAS 与 Unsafe/反射路径并列;MicroBenchmark/UnsafeMicroBenchmark 用 1000 次 exercise 与 100ms+1000ms 计时。它们适合观察 AMO/LR/SC、强/弱 CAS 重试与 fence 组合;但这是 run-test 临时 benchmark,输出在不同构建间不是 ABI 稳定格式,必须保存原始 stdout 与模式。ThreadLocalBenchmark 只有 1 个 timeThreadLocal_get(benchmarks/src/benchmarks/regression/ThreadLocalBenchmark.java:20-29),循环调用 BUFFER.get();固定 pin 的 ThreadLocal.get() 走当前线程 get(Thread.currentThread()) → getMap(t) → ThreadLocalMap.getEntry(this),未命中时 setInitialValue(t)(ojluni/src/main/java/java/lang/ThreadLocal.java:171-194)。它测的是 Java 线程局部存储/map 查找与初始化,不是 JNI 基准;即使 Thread.currentThread 可能存在内建,也不能据此声称跨 JNI 或把它当 JNI 边界 cell。AOSP 公开树中没有与 ART monitor 等价的 suite 级 benchmark,test/2029-contended-monitors 是正确性/压力用例,不能当 monitor 性能分数。

正确性门:JNI 局部引用表/LRT、异常/空指针/引用返回、ScopedPrimitiveArray 的临界区、VarHandle 的强/弱 CAS 语义、monitor 的 wait/notify 与死锁边界;perf 变化不得以弱化这些语义换取。优化候选:JNI stub/LRT 指令数、FastNative 调用点选择、VarHandle 原子序列与锁快路径;这些需要结合机器码与 PMU,不以“调用次数”代替 [S11][S30][S43]。

4.11.7 目标 RISC-V64 平台/Arm64 对照平台 前置实验计划(离线设计,未执行)

目标不是现在给出达标结论,而是为下月“目标 RISC-V64 平台 不同 可重构平台/硬件配置 对照 Arm Arm64 对照平台”固定同一 cell 与身份。每个 cell 至少记录:

维度必记字段
源码/构建ART pin、libcore pin、构建类型(user/userdebug)、dex2oat 过滤器、profile、boot image/odex/vdex SHA、是否 prebuild/relocate
执行身份caller 是否 AOT/JIT/解释、callee 入口(AOT 整方法 intrinsic body/普通 DEX graph/JIT/nterp/GenericJNI)、是否调用点内建;用 oatdump、-verbose:jit、profile 与 run-test flags 交叉确认
模式--optimizing/--jit/--baseline/--interpreter/--no-image、-Xint/-Xusejit、debuggable/instrumentation、GC 类型与 uffd 条件
平台目标 RISC-V64 平台的可重构平台/硬件配置、Arm64 对照平台 SoC、hart/CPU affinity、SMT/频率策略、内核/ISA 扩展、页大小;Arm64 对照平台 与 目标 RISC-V64 平台的镜像/Android 用户态版本
计时/采样标准原始 harness 与 Fixed-N 分轨;两端以独立 VM(Java)或独立 OS 进程(native)运行,单 VM 内 trial 不当作独立进程;记录原始值、离散度与不确定性,不只报最好值;实际样本数、补样与停止规则须在新目标平台 运行计划中事先写明,本轮未执行
PMU目标是避免 multiplex;无缩放、完整 runtime 时 cycles+instructions 可同采;cache/TLB/branch 等按假设逐项增加并证明;empty baseline 先验证可加性;记录 multiplex/valid ratio/raw,不换算不存在的计数
正确性checksum/返回值/异常/边界对拍;功能失败即作废该 cell 的性能结果;AOT 实效/callee 入口未确认时只标 unknown
比较层旧 Arm64 对照平台 Android12 vs 目标平台 Android16 历史成绩不能隔离 CPU 架构;未来基线须对齐源码/版本/配置(image/Android/kernel/runtime/compile/GC);跨 ISA 对齐的是构建输入与配置,不要求二进制镜像相同;做不到就显式标为混杂

历史协议边界:本报告不复述历史研发的固定阈值;未来比较须以独立 VM/进程样本、离散度/置信区间和运行前写明的补样/停止规则为准,历史方法不作为新目标平台的达标线。本轮未执行。

标准原始 harness ns/op 与 Fixed-N 分开:标准原始 harness 结果(Java 如 Caliper/Jetpack;ART 树外 runner 待取得)与 Fixed-N 分账;不要求 Java 以 native 执行,native 若出现在历史账本名称中只作旧账本标签,不说明编译、JNI 或执行模式。两端可自适应 N 不同,但同平台签名/参数/sink 稳定;Fixed-N 要求两端相同 class/method/params/thread/N。ns/op 是每 op 的时间单位(time/op);该时间列是 wall-clock 还是 thread/process CPU 时间由 harness 的计时定义决定,不能从单位反推钟源。吞吐应单独用 ops/s(或 harness 定义的单位)表达。reps 下的 batch 除法与多线程指标必须按 harness 定义解释,不能把所有结果反推成吞吐。Fixed-N 下的 cycles、instructions 是研发归因。ns/op * GHz 只有在实际测量到该 cell 的运行频率时才是周期代理;不能用标称频率或另一台设备的频率补算。cycles/instructions 是 CPI 类指标,不等于 ns/op,也不等于频率。可重构平台 的频率归一化只能说明“在该 硬件配置/频率下的相对变化”,不能等同真实同频硬件,更不能承诺全面达标。

可重构平台与比较层:目标 RISC-V64 平台的不同硬件配置 可能改变频率、cache/内存时序或可用扩展;Arm64 对照平台是不同于可重构平台的微架构。旧 Arm64 对照平台 Android12 与 目标平台 Android16 的历史成绩不能隔离 CPU 架构;未来必须对齐源码/版本/配置(image/Android/kernel/runtime/compile/GC),或显式标为混杂;跨 ISA 对齐构建输入与配置,不要求二进制镜像相同。把 硬件配置、频率、hart、内存配置与 PMU 可用性列为条件,报告“在何条件下”的差值,不把 可重构平台 结果外推为 Arm64 对照平台 或产品结论。

离线队列(按序):固定 pin/构建与 boot image/odex/profile 身份 → 选 3 条代表链(StringEquals、ArrayCopy/charAt、JNI/VarHandle)各 1–2 个 cell → 正确性门与 mode 身份核验 → 按新目标平台 运行前计划确定的独立进程/VM 与补样规则,采标准原始 harness ns/op 与 Fixed-N(两轨分账,不默认套用历史协议阈值)→ PMU/机器码定位 → 才讨论优化候选。任何一步缺数据,标记 gap,不写 PASS。

4.11.8 来源身份与公开边界

本章的 ART suite/方法/入口均来自公开固定 pin [S37];vendor/历史包只作方法线索,不进入公开 pin 计数或结论;方法计数是源码声明计数,不是运行次数;libcore 仅代表链交叉引用,不给出全量计数;time* 名称不是默认 reps;info.txt 不是测量协议。公开树没有 ART Java benchmark runner、没有未公开 harness 源码、没有设备成绩;这些缺口如实保留。外部测试记录与 Gerrit 查询快照只作来源线索,不升级为“本报告实测”;共同方法 R1 只消费其范围分层/标准原始 harness 与 Fixed-N 分账/PMU/比较层口径,不把历史协议或重算行数当新目标平台 成绩;源码登记 intrinsic 也不等于 AOT 方法体已生成或本轮调用点已内建。性能结论必须带模式、callee 身份、输入、重复与正确性门;设备原始成绩、未公开路径与设备配置数据不进入公开正文。下一阶段的 目标 RISC-V64 平台/Arm64 对照平台实验按 4.11.7 的字段表执行,先固定条件再比较,不以单次最好值下结论。

故障定位速查

症状验证动作根因线索与机制
启动即 SIGILL / zygote 崩溃对 pin 核对 mterp/riscv64 是否为可用实现(不是 unimp 桩);logcat 找 SIGILL 进程;确认 nterp 判定解释器代码与 IsNterpSupported()/CanRuntimeUseNterp() 条件不匹配;2023-04–06 的 riscv64 正是此状态 [S33][S38]
同一 Java 用例跨设备/构建差异大先对齐六项条件:commit、过滤器和 profile、debuggable、-Xint/-Xusejit、GC 选项、设备属性;oatdump 看目标方法是否 AOT模式差异(解释 vs 内建)往往大于 ISA 差异;案例 String.equals 的两条执行体见 4.1.4 [S12][S21]
方法长时间停在解释器-verbose:jit 看是否入队/被拒绝;检查方法是否 native/代理/超帧限;确认 JIT 阈值与 JitAtFirstUseCanMethodUseNterp 限制、IgnoreSamplesForMethod(clinit、boot image MH/VH native)、ProfilingInfo 不可分配 [S23][S28]
高 CPU、低吞吐,疑似 GC先确认 collector 条件(uffd 属性/内核支持,否则 CC+Baker);PMU 看读屏障相关 load;GC 日志看阶段RV64 无 marking register,读屏障逐寄存器入口;SuspensionHandler 在 RV64 未实现 [S11][S30][S45]
镜像校验失败/开机回退内存编译变慢日志搜 ValidateOatFile found checksum mismatch、Failed to match oat file checksum、Failed to match oat boot class path;核对镜像版本 118/119 与 BCP 变更boot classpath/checksum 不匹配、跨版本镜像不兼容;失败分支与兜底见 4.2.2 [S07][S08]
运行期 VerifyError/延迟访问错误查 -Xverify 模式与 AOT 软失败映射(kRetryVerificationAtRuntime/kVerifiedNeedsAccessChecks)软失败类的验证被推迟到运行期;访问检查在首次解析触发 [S39]
线程卡死/GC 停不下来kill -3 取转储,找 kWaitingForCheckPointsToRun/kWaitingForGcToComplete;看 suspend timeout 日志长期 FastNative/CriticalNative 或未正确状态转换/检查的路径;普通 JNI 转 kNative,不按“native 长驻”泛化;checkpoint/suspend 协议见 4.7.2 [S30]
volatile/CAS 并发偶发错误做最小 litmus(volatile 读写、Unsafe/VarHandle CAS);对照 4.7.5 的机器码与内存序约束LR/SC 未重试、fence 组合错误、AMOSWAP 仅 Release 的语义待查;LR/SC 16 条指令/扩展限制 [S30][S11]
System.arraycopy 慢或行为异常用 oatdump 看内建是否生效;用类型化 arraycopy 用例验证;simpleperf 看 System_arraycopy 占比通用引用路径 128 元素、typed 路径 384B/元素宽度换算;引用数组屏障、重叠方向拷贝三层路径;见 4.8.7 [S35][S44]
JNI 崩溃/局部引用溢出/库加载失败-Xcheck:jni;看 "JNI DETECTED ERROR IN APPLICATION"、"local table overflow"、"No implementation found for ..."见 4.8.1/4.8.6 的失败串与条件;@CriticalNative 的静态/无引用约束由验证器强制 [S43][S44]

总结与展望

机制结论。 ART 的行为可以统一表述为“产物 + 过滤器 + 入口点”的三层条件模型:产物决定可用的代码与数据(DEX/VDEX/OAT/image,其中 vdex 已无 quickening);过滤器与 profile 决定哪些方法有 AOT;入口点改写与 nterp/JIT 状态决定未编译方法实际跑什么。任何脱离这三层条件的“ART 性能结论”都不可复核。这条模型贯穿了本报告的全部章节,也在案例上给出了可判定的两条执行体 [S05][S23][S28][S32]。

案例结论。 对 ZygoteInit.java:856 的 "start-system-server".equals(argv[i]):当调用点被编译并识别为 kStringEquals 内建时,调用点生成 RV64 VisitStringEquals 的 8 字节批量比较(字符串数据内联,读路径无引用屏障);当 callee 确由解释器(nterp/switch)执行时,是 libcore 的 charAt 循环:每轮两次 charAt 按 callee quick entry 落到 AOT intrinsic 方法体(标准 boot image 若已为该 intrinsic 生成方法体;普通 quick 调用,无 JNI/LRT)或 GenericJNI 回退(无 AOT intrinsic 代码时;FastNative 不做 Runnable↔Native 转换、不调 artJniMethodStart,但仍建立/弹出 LRT)。是否已有 AOT intrinsic 方法体取决于实际 boot image/profile/过滤器/产物,本报告未测,不作“标准镜像必然如此”的断言。两者都没有锁与分配,差距来自“是否被编译”和“是否有内建”,而不是来自某条 ISA 指令的强弱;ARM64 在同一内建上还多一个 const-string 特化,RV64 没有 [S12][S13][S21][S43]。

RISC-V64 的判断。 公开面的事实是:自 2023-01 的骨架起步,Android 15 形成完整后端,26Q2 处于“维护 + 向量基础设施”阶段;缺口集中在 RVV codegen 未实现、GC 无 marking register、JIT fast/pattern 路径仅 arm64、intrinsics 缺 CRC32/FP16、运行期 ISA 探测未实现。支持的层级必须分开说:源码落地充分,上游测试记录以 host 为主、target 为少数外部记录,release 分支包含若干能力,但在本轮公开来源与截点内未见产品/兼容性承诺;公开 main 冻结后,“最新”只能以 release 分支与安全线为准 [S01][S33][S34][S30]。

边界与后续。 本报告未在设备上运行 ART、未构建 Android、未完整枚举 Gerrit 在审变更;本稿为公开研究稿,持续修订,不把作者自检当作独立复核。因此所有性能判断都是待验证假设。下一步按 4.10 的 M1–M9 执行:先固定六项执行条件得到可对照的基线,再用 PMU/机器码/日志验证假设,最后以 458-checker、run-test、GC stress、litmus 与 A/B 同构建作为正确性门。上游若在后续 QPR 引入 “vector register allocation part 2+” 或新的 RV64 修复,按 4.9.6 的核查链增量更新,不改写已固定的历史结论。

参考来源

(ART 来源均为 android.googlesource.com/platform/art 的固定 commit/文件;+/<sha> 为提交页、+/<sha>/<path> 为文件页。行号以 android17-release b753cf97 工作树为准。)

附录 A:主要结论与依据对照

#结论主要依据(pin)所在章节
A1公开 main 冻结于 2025-03-26;26Q2 的 android17-release 是最新公开开发前沿,android17-security-release 不是其后代[S01][S33];merge-base --is-ancestor 与 ls-remote 复核背景、4.9.1
A2a17r 的 VDEX 没有 quickening 段;quicken 过滤器名只作兼容告警并映射 verifyruntime/vdex_file.h:80-86、compiler_filter.cc:181-184 [S32][S36]4.2.3
A3String 字符数据内联在对象中(count_/hash_code_/value_ 联合);String.equals/charAt/length 的 Java 形态与内建映射mirror/string.h:316-326、String.java:1660/1273、intrinsics_list.h:112-114/214 [S16][S21][S13]4.1、4.5
A4调用点与 callee 分两轴:调用点被编译并内建时为 kStringEquals(RV64 8 字节 Ld 批量比较);未内建则按 callee 入口。callee 在 nterp 执行时每轮两处 charAt,其 quick entry 可为 boot-image AOT intrinsic 方法体,或 quick_code==nullptr 时的 GenericJNI 回退;不能一概写成 JNI 帧/LRT/进入 C++intrinsics_riscv64.cc:1152、optimizing_compiler.cc:1244-1277(JniCompile,TryCompileIntrinsic :1269)、image_writer.cc:1488-1495/3530-3534、thread.cc:4368-4381 [S12][S20]4.1.4、4.3、4.5、4.8
A5ARM64 的 StringEquals 有 const-string 特化,RV64 没有intrinsics_arm64.cc:2539/2633(cutoff 32B)、RV64 无 GetConstString/GetFlaggedCount [S12][S34]4.5.3
A6riscv64 的 ISA 特性探测无运行期实现(FromCpuInfo/Hwcap/Assembly/CpuFeatures→UNIMPLEMENTED→FromCppDefines)instruction_set_features_riscv64.cc:52-90 [S34]4.5.1
A7RVV 只有汇编器/反汇编器与向量寄存器基础设施;向量指令 codegen 为 FATALcode_generator_riscv64.cc:6492/6630/6640;SBC workaround 仅测试构建 [S11][S33]4.5.6
A8Java 堆内对象引用恒为 32 位压缩;JNI jobject/native 指针/ArtMethod* 仍是 64 位。默认 collector 由构建 CMC 与运行期条件共同决定;SysPropSaysUffdGc() 的 OR 项含 !IsAtLeastV(),U 及以下未 force_disable 且内核支持时默认 uffd/CMC,V+ 依赖显式属性;独立 CMS 已删,仅余选项映射的并发 MarkSweepobject_reference.h:220-224、build/art.go:50、mark_compact.cc:310-325、heap.cc:861-866 [S30]4.6.1、4.6.3
A9启用读屏障时,RV64 无 marking register、读屏障按寄存器入口,非 Baker 屏障在优化编译器中 FATAL;CMC 的 uffd 路径不使用读屏障,不在此限code_generator_riscv64.cc:1329-1367/1996-2025、mark_compact.cc:310-325 [S11][S30]4.6.3、4.6.4
A10volatile 写 ≥4B 用 AMOSWAP(Release);该组合对 JMM volatile 写的 SC 语义源码未论证(待查)code_generator_riscv64.cc:1033-1064 [S11]4.7.5
A11@FastNative 保持 kRunnable、不调 artJniMethodStart,但仍建立/弹出 LRT;@CriticalNative 无 JNIEnv/LRT/异常轮询,且该调用期间不进行 GC(非全进程停止)jni_compiler.cc:264-306/487-524、quick_trampoline_entrypoints.cc:2105-2135、quick_jni_entrypoints.cc:157-170 [S43]4.8.2
A12System.arraycopy 有三条路径:内建快路径(通用引用路径 128 元素;typed 路径 384B/元素宽度)、内建慢路径→native、未编译时 JNI native;primitive 用方向拷贝,引用数组带屏障intrinsics_riscv64.cc:1634/2000-2007/1723、java_lang_System.cc:50-240、array-inl.h:134-173 [S35][S44]4.5.5、4.8.7
A15CC 一轮:Initialize→可选 Marking→FlipThreadRoots(STW)→Copying(内含 weak-ref-access-disable STW)→Reclaim→Finish;STW 至少两次(根翻转+弱引用关闭,可选验证另计),读屏障/LockWord forwarding 维持 to-space 不变量concurrent_copying.cc:214-269/3375/1600-1690 [S30]4.6.6
A16CMC 一轮:Initialize→Marking→MarkingPause(STW)→Reclaim→Prepare→Flip+CompactionPause(STW)→Compaction(uffd)→Finish;地址由 PostCompactAddress 显式计算,未安装页由 SIGBUS/uffd 按需压缩mark_compact.cc:908-966/1573-1650/1715-1748/4297-4470/5249-5330、mark_compact-inl.h:360-405 [S30]4.6.7
A17Zygote main 与 zygoteInit 是不同函数:案例调用在 main:856,nativeZygoteInit 在 zygoteInit:993 → AppRuntime::onZygoteInit → Binder threadpool;不是 main 的 fork 循环ZygoteInit.java:824/856/909-926/983-1008、AndroidRuntime.cpp:284-315、app_main.cpp:92-97 [S23][S24][S48]4.1
A18debuggable 不等于禁止所有机器码:nterp/AOT Java 不可用、fast tier 被拒,但 JIT 仍可生成带 entry/exit hooks 的代码;-Xint 才强制解释instrumentation.cc:232-238/331-335、optimizing_compiler.cc:1440-1474 [S38]4.4
A19RV64 原子:CAS 用 LR/SC;get-and-update 完整字宽用 AMO,带 mask 的 32 位 set/add 用 LR/SC;“16 条”是受约束 LR/SC 的序列约束而非普遍硬上限intrinsics_riscv64.cc:2256-2348/1276、asm_support_riscv64.S:804-878 [S11][S12]4.5.4、4.7.5
A13RV64 的 SuspensionHandler 未实现;riscv64 无隐式挂起检查fault_handler_riscv64.cc:87-90、code_generator_riscv64.cc:6227-6230 [S45][S11]4.7.2/4.8.5
A14支持状态分层:源码落地充分;上游验证多为 host;release 分支有包含;无产品可用承诺4.9.3 表;merge-base --is-ancestor 抽查 [S33]4.9.3

附录 B:检查与验收清单

检查项结果说明
版本 pin 完整、tag 与分支分开通过 [S01][S33]主 pin b753cf97;android-17.0.0_r1 复核;security 拓扑已更正
章节结构(摘要/背景/技术详解/速查/总结/来源/附录)通过按 document 技术调研报告结构;附录 A–E
每机制含调用链、字段/状态、失败/回退、成本口径、验证方法通过(逐章)4.1–4.10 每章均给源码行号与失败路径;成本只给口径
观察身份分级(源码/可复现未执行/外部记录/本人实测)通过附录 C;正文外部记录已改写标注;无本人实测
支持状态分层(落地→测试→发布→产品)通过4.9.3 四层表;待合并/厂商/预测单列
覆盖矩阵与缺口登记作者自检附录 D;各项覆盖状态见附录 D,未完成项均列补齐路径
图:数量/可读性/字形/正文引用通过(已独立渲染复核)7 张竖版图,PNG+SVG(文字转路径);公开文字版图与 CSS 字节未变
HTML 响应式与三档宽度检查(1440/1280/390)已独立渲染复核;公开文字版作者回归通过三档根宽相等、7 图/档、0 broken/0 外部请求;本次作者回归不冒充新一轮独立浏览器测试
设备与性能实验未进行本稿仅源码与公开记录研究;设备与性能实验未进行
源码独立复核已完成已按独立源码反例复核及后续修订复核修正;设备与性能实验未进行
性能数字/私有数据通过正文无实测数字;未使用内部树实现或用户私有数据
未决标记(源码待办类词)通过全文清理;源码内的待办注释以“待办注释”描述

附录 C:研究方法与局限

来源与通道(2026-10-19):android.googlesource.com 的 git 协议可用(ls-remote、filtered clone、按需懒取);通过固定 commit 读取 ART、libcore 与 frameworks/base 的相关文件;网页与 curl 通道超时,未作为依据。

方法:历史查询一律用有界范围(--first-parent/--full-history --diff-filter=A A..B -- path)与 merge-base --is-ancestor 判定 release 包含;日期用提交者日期并注明作者日期差异;每个机制结论给出文件+符号+行号;正文观察按四级身份标注(4 级见 4.1.2 与上述清单),上游提交里的 Test: 只作外部记录转述。案例选择经过源码核对,纠正了旧稿的两处关键过时(String 非 byte[] 字段、String.equals 是直接内建而非 StringLatin1 内建)。

局限与缺口:①无设备运行、无 Android 构建,镜像默认 collector、uffd 可用性、LR/SC 争用、safepoint 时延等设备侧行为未实测;②Gerrit open/NEW 变更已取得 2026-10-19 的 3 条 REST 查询式候选(去重 18 条:17 main + 1 master;原始响应已本地留存;未查询 abandoned),但未做完整枚举与逐条 detail 精读;3693991/3580772 只核到同 Change-Id 合入谱系,3556506 只核到同 subject/不同 Change-Id,当前 patchset 语义等价均未核;③跨仓仅取案例相关文件,framework 侧全链细节以公开代码为准未逐层展开;④RVV 的后续节奏只能观测;⑤源码已完成独立反例复核及后续修订核对;图与布局已完成独立渲染检查。公开文字版另做作者回归;作者自检不作为独立复核。设备与性能实验未进行。

承重结论清单:1) String 内联布局与 equals/charAt 双路径(A3/A4);2) a17r VDEX 无 quickening(A2);3) ISA 特性运行期探测缺失(A6);4) RVV codegen FATAL 与支持分层(A7/A14);5) GC 默认 collector 的条件链与 CC/CMC 完整回收链(A8/A15/A16);6) volatile/AMOSWAP 的 JMM 语义待查点(A10);7) security 线拓扑与 tag 映射(A1);8) arraycopy 三路径与元素阈值(A12);9) FastNative LRT 与 CriticalNative GC 协议(A11);10) debuggable 与 JIT 的边界(A18)。

正文未证点汇总:全文出现的“待查”仍为有意保留的未证点(volatile 写的 SC 语义、设备默认 collector/uffd 可用性、suspend 时延、逐指令反汇编、Gerrit 当前 patchset 语义等价等),各自已在所在章节与附录 D 给出补齐路径,不当作已证事实。

修订记录:v2.0(2026-10-19)重写版:以 String.equals 案例纵切,覆盖启动/类加载/解释器/AOT/JIT/GC/JNI/ABI 与 RISC-V64 支持分层。v2.0-r7(2026-10-20):公开研究稿;补齐 charAt 的 AOT intrinsic/GenericJNI 条件、FastNative LRT、CC/CMC 完整回收链、compareTo nullable 条件、checkpoint 与 <clinit> 锁作用域等机制表述。

附录 D:调研问题×覆盖矩阵(终态)

#问题级别覆盖位置状态依据/缺口
Q1分工与端到端启动链核心4.1/4.2/图1-2已覆盖源码确证([S05][S23][S24][S25]);framework 侧仅接口面
Q2DEX/类加载/验证/分派/异常/反射/BCP核心4.2已覆盖[S08][S39];反射/MethodHandle 以入口级覆盖
Q3解释器谱系与 RV64 真实性核心4.3已覆盖[S18][S19][S40][S41]
Q4AOT/dex2oat/IR/regalloc/stack map/产物核心4.2/4.4已覆盖[S31][S32][S38]
Q5JIT/热度/OSR/deopt/profiles/过滤器核心4.4已覆盖[S28][S29][S36];设备阈值未测
Q6GC/分配/collector/屏障/引用/OOM核心4.6已覆盖(源码级)[S30];CC/CMC 各一轮完整链与图 7;设备默认未测
Q7线程/挂起/Monitor/JMM→RVWMO核心4.7已覆盖[S30][S43][S46];volatile SC 语义待查
Q8JNI/Fast/CriticalNative/ABI/信号核心4.8已覆盖[S43][S44][S45];FastNative 仍建 LRT、native 库卸载链已补
Q9RV64 后端/ABI/指令/intrinsics/RVV核心4.5/图6已覆盖[S11][S12][S34];逐指令反汇编未做
Q10热点映射与 A64 对照核心4.5/4.8已覆盖(机制级)[S12][S21][S35];性能差异未测
Q11调试/诊断/测试设施核心4.9/4.10已覆盖[S37];设备侧命令未执行
Q12上游演进(逐版本/合并状态)核心4.9部分交付已合并与 release 包含已核 [S33];在审候选已获查询式快照(18 条:17 main + 1 master,[S47]);同 Change-Id 谱系与语义等价分层,完整枚举/逐 diff 仍未做(G1)
Q13趋势与时间截点核心4.9/总结已覆盖提交统计+负向核对 [S01][S33];预测标注为判断
Q14优化落地地图核心4.10已覆盖(假设)M1–M9;测量未做
E1libcore/OpenJDK 上游同步流程扩展背景/4.5部分交付[S21][S22];同步流程不在 art 仓
E2Bionic/linker 内部扩展4.8 接口面部分交付(边界外)Bionic 内部机制见独立 Bionic 专题,本报告只覆盖接口边界
E3设备实测(启动/GC/争用)扩展4.10 观测口径未进行本报告未做设备实验;后续可按专项实验补充
E4源码独立复核扩展附录 C已完成已按独立源码复核及后续修订复核修正;设备与性能实验未进行
E5反汇编/机器码对照扩展4.5部分交付机制与源码序列已给;未产出反汇编
E6浏览器三档宽度检查(1440/1280/390)扩展HTML 交付物已独立渲染复核;公开文字版作者回归通过三档根宽相等、7 图/档、0 broken/0 外部请求;不冒充新一轮独立浏览器测试

附录 E:术语与缩略语

术语说明
nterpART 的快速解释器(computed-goto、按架构汇编实现;riscv64 目录在 runtime/interpreter/mterp/riscv64)
mterp2021 年删除的旧解释器;本报告只作历史引用
managed ABIART 私有调用约定:A0=ArtMethod、实参自 A1、FP 只用 FA0–FA7、xSELF=s1
psABIRISC-V 平台标准 ABI:FP 参数 FA0–FA7 用尽后进整数寄存器再上栈
VDEX / OAT / ART image原始 DEX+验证依赖+类型查找表 / 编译代码与元数据 / 堆对象快照
quickening旧版 DEX 预解析优化;a17r 的 vdex 已无该段,过滤器名仅兼容
Baker 读屏障并发复制 GC 的对象读屏障;riscv64 无 marking register,用逐寄存器入口
CMC / CC并发 mark-compact(uffd)/ 并发复制;默认由构建与运行期条件共同决定
TLAB线程本地分配缓冲(bump 指针),RV64 有手写快路径汇编
LR/SCRISC-V 原子原语对;CAS/锁的实现基础;受约束序列有 16 条指令/扩展限制(不是所有 LR/SC 的普遍上限)
FastNative保持 Runnable、不调 artJniMethodStart,但仍建立/弹出 JNI 局部引用帧(LRT)
RVVRISC-V Vector 扩展;本报告限定为“汇编器/基础设施可用,codegen 未实现”
26Q2 / 25Q4发布列车(年+季度);android17-release 的合并目标为 26Q2-release
支持四层源码落地 → 构建/测试验证 → release 包含 → 产品可用(无公开承诺)