Android Runtime(ART)机制与 RISC-V64 支持研究(v2.1-public)
| 项目 | 内容 |
|---|---|
| 文档类型 | 技术调研报告 |
| 版本 | 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 的运行体系可以沿一条真实调用串起来:zygote 建立
Runtime并装载 boot image/vdex/oat,类加载把 DEX 链接成可执行入口,未编译方法落 nterp、按热度进入 JIT/AOT;本报告以ZygoteInit.java:856的"start-system-server".equals(argv[i])为纵切案例,逐层追到 RISC-V64 机器码与回退路径 [S05][S23][S32][S39]。 - 该案例必须先分清两条正交的分派轴:调用点是否被编译并内建(
kStringEquals,RV64 以 8 字节Ld批量比较),以及 callee 的ArtMethod入口(AOT/JIT/nterp/bridge)。只有“未内建且 callee 未编译”时才由解释器逐条执行 libcore 的 Java 循环,且循环每轮有两处@FastNative的charAt(不是“每字符一次”);性能差异主要由这两轴共同决定 [S12][S13][S17][S21]。 - RISC-V64 自 2023-01 进入公开 ART:Android 14 只有“解释器被显式排除”的骨架,Android 15 才形成完整后端;android17-release(26Q2)处于维护期,剩余缺口集中在 RVV codegen 未实现、GC 无 marking register、JIT 的 fast/pattern 路径仅 arm64、intrinsics 缺 CRC32/FP16 [S30][S33][S34]。
- “支持”必须分四层陈述:源码落地→构建/测试验证(公开记录多为 host-only,少数 target)→release 分支包含(以祖先关系判定)→产品可用(本轮未见公开承诺);公开
main自 2025-03-26 冻结,把 main 当“最新”会系统性低估 2025 年之后的能力 [S01][S33]。 - 全部结论锚定公开 commit;上游测试记录只作外部记录转述,本报告不含本人实测;性能内容均为机制解释与待验证假设(M1–M9),无虚构数字 [S01][S33][S34]。
背景与现状
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 为准;本报告未运行设备。
手机可横向滑动查看本图
图 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 是启动与日常运行中常见的字符串比较方法(本报告不引用“频率排名”这类无公开依据的说法)。选择它作为样本,是因为它同时满足四个可验证条件:
- 有真实调用点:
ZygoteInit.main解析 zygote 启动参数时执行"start-system-server".equals(argv[i])(frameworks/base android17-release,core/java/com/android/internal/os/ZygoteInit.java:856)[S23]。 - 同一个调用点存在“内建/非内建”两种可能:ART 把
Ljava/lang/String;.equals(Ljava/lang/Object;)Z登记为内建(runtime/intrinsics_list.h:214)[S13];是否在本调用点生效取决于调用者是否被编译以及编译器是否把该 invoke 识别为内建(4.1.4)。 - callee 本身有“编译/未编译”两种状态:libcore 的
String.equals源码是纯 Java 循环(String.java:1660起,循环在:1675-1682)[S21];但 boot-image 编译可尝试StringEquals的整方法 intrinsic body,因此不能从源码形态推断 callee 一定执行 Java 循环;它是否以 Java 循环执行取决于 callee 的入口点,而不是调用点是否内建。 - 能落到 RV64 的两类真实代码:内建路径有 riscv64 专属 codegen(
VisitStringEquals,intrinsics_riscv64.cc:1152)[S12];解释路径中的charAt是@FastNativenative 方法(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 = b753cf97923c3695338d21466fa14c57b480a59a | 26Q2 线;ART 源码行号以此工作树为准 [S02] |
| ART 对照 | android16-qpr2-release = 1690c6912a7972c9e62c39b48c706de9b8b18b4a(=android-16.0.0_r4) | 25Q4 [S04] |
| ART 公开 main | 6484611fd45e69db9f33f98bfd6864014b030ecf | 2025-03-26 起冻结 [S03] |
| libcore | android17-release = 4ebfb391ee0109d16abe6e2bc965724b940928c5 | String.java、StringLatin1.java、StringUTF16.java、System.java [S21][S22] |
| frameworks/base | android17-release = 94b4c163b7dfe5ce3607f7bb8456f9573f7de57d | ZygoteInit.java、RuntimeInit.java、AndroidRuntime.cpp [S23][S24] |
观察身份与研究方法统一见附录 C(源码确证/可复现未执行/外部测试记录/本人实测四级;本报告无本人实测)。
4.1.3 端到端链路:从进程入口到这次调用
把这次 equals 放回启动链(图 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]。 - 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]。 - 镜像映射的真实权限:非压缩镜像走
MemMap::MapFileAtAddress(..., PROT_READ | PROT_WRITE, MAP_PRIVATE, ...)(image_space.cc:1383-1396);压缩镜像先匿名映射 RW 再解压拷贝(:1407-1420)。因此镜像是可写私有映射(写时复制),不是“整块只读共享”:fork 后未写的物理页共享,重定位与 fork 前入口点修正会弄脏私有副本;这解释了下文“同一镜像、不同进程可写状态不同”的现象 [S07]。 - Runtime::Start 与最初的 Java 执行:
Runtime::Start依次做 root clinits、注册运行期 native、按条件创建 JIT(:1149-1150);Thread::FinishStartup创建 main 线程 peer 并执行 root clinits,这是运行时内部最早的 Java 方法执行 [S05]。 - 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]。 - 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]。 - 调用点本身:
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 回退)。
手机可横向滑动查看本图
图 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 失败与回退
- 参数为 null:语义返回 false;内建路径
Beqz(arg, return_false),解释路径instanceof String为假,同样返回 false,都不抛异常。 - 长度或编码不同:返回 false;内建先比较
count,解释先比较length()。 - 越界异常:
equals循环以length()为上界,正常不触发;charAt自身的越界检查在被其它调用者使用时抛StringIndexOutOfBoundsException(mirror::String::CharAt,string-inl.h:45起;native 入口java_lang_String.cc:36)[S16][S17]。 - 执行模式回退:
CanRuntimeUseNterp()为假(debuggable/-Xint/entry-exit stubs/异步异常等)→ 未编译方法起步于 switch 解释器(JIT 之后仍可能替换入口);JIT 编译失败或去优化 → 回到解释器;callee 未编译时即使 caller 已编译,普通 invoke 也只会进入 callee 的解释入口(轴 2)。具体条件见 4.3/4.4。 - 内建失效:只影响“本次调用点”;callee 的编译状态与入口点独立(轴 2)。
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 字节到“可执行入口点”经过哪些状态。所有行号以 ARTandroid17-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)。按源码顺序,它做四件事:
Jit::PreZygoteFork()(:871)——通知 JIT 进入 fork 前的安全状态;- 收紧线程列表:等待其他线程完成
Unregister,并WaitUntilSingleThreaded()(:876-900区段),这是“fork 只能发生在单线程进程”的运行时侧保证; - 首次 fork 时修正所有已初始化类的 ArtMethod 入口点:
MakeInitializedClassesVisiblyInitialized+UpdateMethodsPreFirstForkVisitor(:902-911)——把类初始化完成后的入口点(trampoline → 真实代码/nterp)在共享页上一次性写好,避免每个子进程各自弄脏; 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::Heap | Runtime 持有;boot image 的 ImageSpace 由 Heap 持有(heap.cc:520 起) | 字符串对象与内联字符数据都在 Heap 的空间里;镜像为 RW+MAP_PRIVATE 映射,fork 后未写页共享物理页 |
ClassLinker | Runtime 持有;持有 class_table/class_roots/boot_class_path | String 类与 String.equals/charAt 的 ArtMethod 由它链接 |
ThreadList | Runtime 持有;每线程 Thread 持有 JNI/quick 入口点表 | 安全点/挂起以线程为粒度;fork 要求单线程 |
Jit/JitCodeCache | 条件创建(Runtime::Start :1149-1150);zygote 共享映射 + 私有区 | 编译后的 String.equals 调用点可能来自 JIT code cache |
boot image 的 ImageSpace | fork 后共享同一 COW 映射(未写页共享物理页) | 镜像内的入口点被 fork 前修正,子进程直接复用 |
4.2.2 Runtime::Init 的顺序与 boot image 的装载/校验
启动装载与校验的整体关系见图 3。读图要点:磁盘上的四类产物(DEX/VDEX/OAT/ART image)各司其职;装载时先做镜像/oat/boot classpath 三重校验,再重定位并交给 Heap/ClassLinker;失败有三条分支(内存编译、跳过扩展 chunk、无镜像路径)。图下方的“案例落点”提醒:类在镜像里不等于方法已编译。
手机可横向滑动查看本图
图 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 已给出骨架;这里补“装载与校验”的细节,因为它决定了案例方法在启动后处于什么执行状态。
- 镜像发现与映射: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起)。 - 三类失败与兜底:镜像版本/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]。 - 入口点的两次改写:镜像生成时,未编译的 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]。 - 首个 Java 执行:
Runtime::Start→Thread::FinishStartup→RunRootClinits(4.1.3 的链条)。此处的类初始化是“真实 Java 代码第一次跑”的位置,String等核心类的状态机也从这里开始进入kInitialized/kVisiblyInitialized[S05]。
4.2.3 DEX、VDEX、OAT、ART image 的当前关系(含一处重要纠错)
四种产物的职责经常被写混。按 a17r 源码,准确关系是:
- DEX:输入字节码。标准 DEX 版本 035/037/038/039/040/041(
libdexfile/dex/standard_dex_file.cc:29-40);String.equals的字节码就来自这里。CompactDex 已不在主线:25Q2 的树中仍有compact_dex*实现(4 个文件),25Q3(a16-qpr1 tipe5c642b5)已为 0——移除发生在 25Q2→25Q3 窗口 [S39]。 - VDEX:a17r 的
VdexFile只有四个 section:kChecksumSection/kDexFileSection/kVerifierDepsSection/kTypeLookupTableSection(runtime/vdex_file.h:80-86)。kDexFileSection存原始 DEX 字节,kVerifierDepsSection存验证依赖(跨 dex 的可赋值性/字符串),kTypeLookupTableSection是类型查找表 [S32]。 - OAT:编译后的机器码 + 元数据(方法头、stack map、boot classpath 键与 checksum 等),由
OatWriter写出,relative_patcher负责跨方法/跨 oat 的相对调用修补。 - ART image:堆对象快照(类对象、字符串常量、ArtMethod 等)+ 入口点与 stub 地址;不是代码本身,而是“已初始化好的对象图”。
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 失败、回退与观察
- 镜像不匹配:版本/checksum/BCP 校验失败 → 内存编译或解释执行;错误串以
ValidateOatFile found checksum mismatch、Failed to match oat file checksum/Failed to match oat boot class path为线索(image_space.cc)。 - 无镜像:
InitWithoutImage路径(class_linker.cc:699)以 boot classpath dex 建立类表,nterp trampoline 直接设为 nterp 入口(:914)。 - 类初始化失败:
<clinit>抛出异常 → 类置kErrorResolved,后续使用得到NoClassDefFoundError(由状态检查与异常传播共同保证)。 - VDEX 相关误判:不要再用 quickening 解释解释器性能;当前 pin 下应按“原始 DEX + verifier deps + 类型查找表 + OAT 代码/元数据 + 镜像对象图”理解产物。
- 可复现观察(未执行):
oatdump --image=<boot.art>查看镜像/oat 与方法入口;logcat搜索镜像校验相关错误串;profman查看 profile 是否覆盖案例方法。设备与同版本工具是前提。
后续第 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 回退再回来;解释器在什么条件下根本不启用。行号均以 ARTandroid17-release b753cf97为准;未运行设备。
4.3.1 解释器的现状:只有一个快解释器
公开 ART 中的经典 mterp 已于 2021 年被整体删除(提交 e4f983c808 "Remove mterp.",Nicolas Geoffray;该提交不在 android12 分支、在 android13 及以后)[S41]。当前解释执行只有两种形态:
- nterp:按架构提供的汇编快解释器(
runtime/interpreter/mterp/<arch>/;riscv64 目录因从未有过经典 mterp 而直接用架构名),通过 python 模板生成,handler 以 computed-goto 组织; - C++ switch 解释器:
runtime/interpreter/interpreter_switch_impl0.cc起的实现,作为 nterp 不可用时的回退。
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)。
手机可横向滑动查看本图
图 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]:
- 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 解析。 - 全引用参数(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 位置。 - 零/单参数:窥探下一 opcode 判断返回值类型,必要时只解析 shorty;标量返回要处理
fa0 → a0的搬运。 - 完整 shorty 路径:多参数混合 float/double/long、或 invoke-polymorphic/custom 时,按 shorty 做完整的 managed ABI 参数布置,再
jalrquick 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 异常、挂起与热度
- 异常落地:nterp 所有异常共用一个 landing pad——
NterpGetCatchHandler()返回artNterpAsmInstructionEnd(nterp_helpers.cc:223-227);在longjmp前运行时会写好dex_pc_ptr,落地后据此决定跳转的 catch handler。nterp_deliver_pending_exception(main.S:674区段)以DELIVER_PENDING_EXCEPTION完成统一分派。异常帧的引用/寄存器数组由上面的双数组协议提供,QuickExceptionHandler可据此还原 [S19][S40]。 - 挂起检查:方法入口与回边都插
DO_SUSPEND_CHECK(main.S的热度宏内),读THREAD_FLAGS_OFFSET(xSELF)的 suspend/checkpoint 位后调art_quick_test_suspend。riscv64 没有隐式挂起检查(CanUseImplicitSuspendCheck()为 false,arm64 可隐式化),因此每次安全点都是显式的 load+branch [S11][S19]。 - 热度与 OSR:
START_EXECUTING_INSTRUCTIONS与NterpHotnessCheck在计数器归零时调用nterp_hot_method(NterpHotMethod);对回边,C++ 侧准备OsrData,返回非空时 nterp 自己丢弃当前帧、保持 callee-saves 与 OSR 帧一致、把 OsrData 的 spill 区倒序拷贝到新栈、free后jr到编译代码(main.S:610-634)。注释明确“nterp 的 callee-saves 与 OSR 帧一致”是这条快速迁栈的前提 [S19][S18]。
4.3.6 失败与回退
- nterp 不可用:debuggable、
-Xint、entry/exit stubs、JIT 首用编译等条件满足CanRuntimeUseNterp() == false→ 所有方法回退 switch 解释器;-Xint由Runtime::Init的Opt::Interpret触发ForceInterpretOnly()(runtime.cc:1831-1833)[S05][S18]。 - 方法级不可用:native、代理、
MustCountLocks、帧过大 → 该方法的入口不是 nterp(native 若无 AOT intrinsic 代码则走 GenericJNI;代理走代理入口;普通 Java 方法若帧过大则不能用 nterp,按其入口状态执行)[S40]。 - 未实现 opcode 的历史坑:2023 年 riscv64 的 nterp 曾是
unimp桩,2023-06 起以“方法过滤器(排除 try 块/未实现 opcode)”方式启用,2024-01 补齐异常捕获后进一步放开(过滤器退场的精确节点未逐一核);今天这段历史仍是“目录存在≠可执行”的最好反例 [S33]。 - 可复现观察(未执行):
logcat中解释器相关日志、kill -3查看解释器帧的 dex pc、run-test --interp用例集;设备前提与版本一致。
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 与去优化如何串起来。行号均以 ARTandroid17-release b753cf97为准;阈值只按 pin 与条件陈述,不把默认值当设备实际值。
4.4.1 两条编译入口与编译过滤器
ART 有两条编译路径,共用优化编译器的后半段:
- AOT(dex2oat):
dex2oat/dex2oat.cc:3248 main→Dex2oat()(:3157)→ParseArgs→Setup(:1496)→DoCompilation(:3109)→Compile()(:1889)→CompilerDriver(构造 :1952)→PreCompile(dex2oat/driver/compiler_driver.cc:861)→CompileAll(:314)→ 单方法CompileMethodQuick(:430)。产物写入 oat/vdex/image(4.2)。 - JIT(运行期):
runtime/jit/。Jit::MaybeEnqueueCompilation(jit.cc:1747)决定任务类型,编译线程池只有 1 个 worker(jit.cc:1135),任务队列优先级为 generic→OSR→fast→baseline→optimized;JitCodeCache上限 64 MB(jit_code_cache.h:185),分为 zygote 共享区与进程私有区(ProfilingInfo 只能放私有区)。
编译过滤器(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 拉回解释器。
手机可横向滑动查看本图
图 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 与案例相关的是:
HInvoke*(虚/静态/接口调用,带 intrinsic 信息);HArrayGet/HArrayLength/HBoundsCheck——String.charAt的内建把 String 当作数组:kStringCharAt在 builder 阶段生成HArrayGet(kUint16,is_string_char_at=true,SideEffects::None) +HArrayLength(is_string_length)+HBoundsCheck(is_string_char_at)(instruction_builder.cc:2125、:2225-2241;简化器版本在instruction_simplifier.cc:2859-2881),长度读取直接映射到String::CountOffset()(code_generator.cc:233-236);HDeoptimize(编译期插入的去优化点,例如 CHA/边界检查保护失败);HNullCheck/HInstanceOf/HLoadClass(String.equals内建的 null/类型分支对应物)。
调用点内建: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 与去优化
- OSR(栈上替换):nterp 回边由
NterpHotMethod触发,Jit::PrepareForOsr用CodeInfo::GetOsrStackMapForDexPc找到 OSR 入口并把解释器 vreg 写入OsrData,随后 nterp 自己按 OsrData 拷贝栈并跳到 native pc(4.3.5,main.S:610-634);switch 解释器路径则调用art_quick_osr_stub(riscv64 实现存在于quick_entrypoints_riscv64.S:946起)。 - 去优化:编译代码命中去优化点 →
art_quick_deoptimize_from_compiled_code(riscv64 汇编:716-722)→artDeoptimizeFromCompiledCode;Thread::Deoptimize→QuickExceptionHandler::DeoptimizeStack/DeoptimizeSingleFrame→DeoptimizeStackVisitor逐帧重建解释器帧。nterp 帧直接拷贝双数组;optimized 帧按 stack map 的GetRegisterMaskOf/GetStackMaskOf/GetDexRegisterMapOf写回ShadowFrame。instrumentation(debugger 挂起/方法 tracing)可强制整栈去优化(Instrumentation::Deoptimize(*))。不要把它写成“debuggable 根本不生成机器码”:debuggable 下 JIT 仍可生成带 entry/exit hooks 的代码;真正强制解释的是-Xint/InterpretOnly,且 CriticalNative 在 debuggable 下是 JIT 的明确例外(optimizing_compiler.cc:1352-1365、:1457-1474)[S29][S38]。
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 失败、回退与观察
- JIT 失败:fast→baseline 回退(
JitCompileTask内,jit.cc:709-721);baseline/optimized 编译失败不改变方法入口(仍解释执行)。 - 去优化回退:CHA 失配、inline cache 失配、BCE 保护失败等会按 stack map 回到解释器(4.4.6);这是“跑一会儿变慢/变快”的常见来源。
- AOT 缺失:过滤器为 verify/无 profile 覆盖时无 AOT 代码,全靠 nterp+JIT。
- 可复现观察(未执行):
-verbose:jit看编译入队/完成、oatdump看 AOT 代码、profman --dump-info-for看 profile 是否覆盖、-Xjitshutdown-dump类开关在停进程时打印 JIT 统计;设备与同版本工具是前提 [S37]。
成本与权衡: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 可用。行号以 ARTandroid17-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 不用浮点寄存器”。
手机可横向滑动查看本图
图 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 形态
- volatile 写:
StoreSeqCst(code_generator_riscv64.cc:1033-1074)对 ≥4 字节类型用AMOSWAP.D/W(AqRl::kRelease)丢弃返回值充当顺序一致的 store(这是源码的当前实现;该组合是否满足 JMM 对 volatile 写的 SC 要求,源码未给论证,列为待查,见 4.7.5);<4 字节用fence+store+fence组合。 - volatile 读:读前
GenerateMemoryBarrier(kAnyAny)、读后kLoadAny(HandleFieldGet路径)。 - 屏障映射:
GenerateMemoryBarrier(:6232-6246)把kAnyAny/kAnyStore/kLoadAny/kStoreStore映射到fence rw,rw / rw,w / r,rw / w,w。 - CAS:
GenerateCompareAndSet用LrW/ScW(32 位)或LrD/ScD(64 位)组成“LR→比较→SC→失败重试”循环;强 CAS 必须重试,弱 CAS 只尝试一次并把 SC 结果取反(intrinsics_riscv64.cc:1276、:1298-1314)。本 pin 没有 Zacas 类单指令 CAS。 - AMO get-and-update:完整字宽的
set/add/and/or/xor直接用AMOSWAP/AMOADD/AMOAND/AMOOR/AMOXOR(GenerateGetAndUpdate,:2256-2348);只有 32 位且带 mask 的set/add需要“读-改-写”部分位,使用 LR/SC 循环(:2280-2293、:2302-2322)。因此“原子读改写一律 LR/SC”不成立。 - 锁:
LOCK_OBJECT_FAST_PATH(asm_support_riscv64.S:804-839)用lr.w.aq/sc.w实现 thin lock;解锁用lr.w+sc.w.rl。宏内 16 条指令的限制是 ART 为受约束 LR/SC 序列获得前向进展保证而维护的序列约束(同时受ScopedLrScExtensionsRestriction只允许 Zca 等约束),不是“所有 LR/SC 指令合法性的普遍硬上限”。
这条组合的语义边界是: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]。
- String 链:
String.equals(kStringEquals)、charAt(kStringCharAt→HArrayGet)、length/isEmpty(kStringLength/kStringIsEmpty→HArrayLength)、compareTo(kStringCompareTo)、indexOf家族(kStringIndexOf 等)、StringFactory.newStringFrom*均有内建入口;libcore 侧的StringLatin1/StringUTF16.indexOf是另一套(用于非 String 容器与部分路径),不要与 String 内建混为一谈 [S13][S21][S22]。 - arraycopy 链:
System.arraycopy在 libcore 是public static native(System.java:448),运行期 native 实现位于runtime/native/java_lang_System.cc:50 System_arraycopy(另有System_arraycopyTUnchecked等类型化快路径)。编译期内建VisitSystemArrayCopy(intrinsics_riscv64.cc:1723)先做空指针/重叠方向检查(CheckSystemArrayCopyNullOrOverlap)、源/目标越界检查,再做类型系统检查(Class/ComponentType/PrimitiveType/Monitor字段),对引用数组支持 Baker 读屏障慢路径(DCHECK_IMPLIES(codegen_->EmitReadBarrier(), kUseBakerReadBarrier)),其余情况转IntrinsicSlowPath回落 native。同一System.arraycopy调用因此有三条路径:内建快路径、内建慢路径→native、以及未编译时的 JNI native——与String.equals的两条路径同构 [S12][S17][S35]。
4.5.6 RVV/向量的现状(只写源码可见的事实)
- 汇编器层支持 RVV 指令编码与 CSR(
kVlenb等)与向量 opcode 常量(assembler_riscv64.h:2195-2199),并有 RVV 汇编器测试; - 编译器层有向量寄存器的基础设施:
AllVectorRegisters()、慢路径保存向量寄存器(code_generator_riscv64.cc:127/2627/5358等),来自上游 “Add vector register allocation, part 1”; - 但向量 HIR 的指令生成未实现:
HasSIMD()图在 Move 与 Save/RestoreFloatingPointRegister 处LOG(FATAL)/UNIMPLEMENTED "Vector extension is unsupported"(:6492/:6630/:6640),另有 VTMP 待办注释;HasVector()在编译器里只出现在汇编器扩展掩码(4.5.1),没有 codegen 消费者; - 测试构建有一个针对 SBC 的 SIGBUS/ADRALN 变通(
libartbase/arch/riscv64/sbc_v_adraln_workaround.cc,宏ART_TEST_ON_SBC_RISCV64_V_ADRALN_WORKAROUND),注释说明“部分 SBC 几乎符合 RVA22+V,但 V 扩展不满足 Zicclsm,向量非对齐访问触发 SIGBUS”,通过信号处理模拟vse64.v——仅测试构建,不是产品通路; - 本 pin 未发现针对 riscv64 的自动向量化路径(
loop_optimization的向量化开启与 arch 相关;RV64 codegen 的向量 FATAL 已足够说明“生成端不可用”)。因此“RVV 已支持”的说法应限定为“汇编器/基础设施 + 测试可用,优化代码生成不可用” [S11][S33]。
4.5.7 失败、回退与观察
- 特性不匹配:因为运行期探测未实现(4.5.1),若二进制按
-march携带 C/V/Zb* 而目标硬件/内核不提供,执行到对应指令才会暴露(SIGILL 等);ART 不会在启动时降级特性集。这是“构建期 ISA 假设”的直接风险,需在目标平台构建/部署层控制。 - 不支持的向量图:一旦构造出含 SIMD 的图,codegen 直接 FATAL;正常构建下该路径不可达(没有 RVV HIR 生成器)。
- 内建缺失:CRC32/FP16 等走 libcore 通用实现(不是错误,但路径不同);const-string 特化缺失只影响编译期可节省的指令。
- 可复现观察(未执行):
disassembler工具或oatdump --disassemble查看方法机器码(需同版本工具);用-XX:.../镜像 ISA 字符串核对特性声明;对特性不匹配的复现应在目标硬件上进行(本报告无板卡)。
成本与权衡:内联字符数据让 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 的顺序。行号以 ARTandroid17-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 形态与写屏障卡表
- Baker 读屏障:对象路径
ReadBarrier::Barrier(read_barrier-inl.h:35-104)先判IsGray(:49),灰则慢路径Mark;根路径BarrierForRoot(:107、:156)用Thread::GetIsGcMarking()+ Mark。riscv64 没有 marking register:编译代码对每个引用寄存器都从 Thread 载入“该寄存器对应的 mark entrypoint 指针”再判空(槽位由ReadBarrierMarkEntrypointOffset计算,T5/T6 映射到 0/1 槽;code_generator_riscv64.cc:164-174)(art_quick_read_barrier_mark_regNN,入口宏READ_BARRIER_MARK_REG在quick_entrypoints_riscv64.S;codegen 侧EmitBakerReadBarierMarkingCheck与GenerateFieldLoadWithBakerReadBarrier在code_generator_riscv64.cc:1329-1994区段)。代码内有明确待办注释:“Introduce a marking register…”。当构建启用读屏障时,非 Baker 的读屏障在 riscv64 codegen 未实现(AddReadBarrierSlowPath/GenerateReadBarrierSlow直接LOG(FATAL) "Unimplemented",:1996-2025)——所以“RV64 只支持 Baker”限定在读屏障路径;CMC 的 uffd 路径不使用读屏障,不能被这句话排除 [S11][S19][S30]。 - 写屏障=卡表:
WriteBarrier::ForFieldWrite等直接CardTable::MarkCard(dst)(write_barrier-inl.h:30-43),没有 SATB 日志;riscv64 的art_quick_aput_obj在存引用后按对象地址右移写卡表一字节(quick_entrypoints_riscv64.S相应区段),属“编译器内联 + 必要时慢路径”的共同结果 [S30]。 - 范围:读屏障只覆盖需要移动/标记的堆引用读取;字符串内联字符、primitive 字段、native 指针不经过它。JNI 边界还要看线程状态(4.6.5/4.8),不能把“某个字段无屏障”推广成“整条 native 边界无屏障”。
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 清空
手机可横向滑动查看本图
图 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:
- 触发与串行化:分配失败、显式
System.gc()或后台策略进入Heap::CollectGarbageInternal;它设置当前 GC、选择 collector,并调用collector->Run(gc_cause, clear_soft_references)。此后同一时刻只有一个 GC 在执行(kWaitingPerformingGc),其他分配线程等待。 - InitializePhase(并发):记录分配量、清空/绑定位图、计算 immune spaces,标记 zygote large objects;此时 mutator 继续运行,from/to 语义尚未翻转。
- 可选 MarkingPhase(并发,仅 generational 的非 young full GC 且非 force_evacuate_all):扫描 immune spaces、runtime roots、thread roots 与 mark stack,计算 live bytes;这是“先标记、后复制”的两阶段模式。非 generational 的 full CC 可以跳过独立 MarkingPhase,直接在翻转后靠读屏障复制。
- GrayAllDirtyImmuneObjects(并发,Baker 条件):在翻转前把 immune 空间的脏卡对象置灰;
ActivateReadBarrierEntrypoints()先把读屏障入口切到“标记中”版本,避免 mutator 看到灰位却走到旧入口(concurrent_copying.cc:231-240)。 - FlipThreadRoots(STW 暂停):
FlipThreadRoots让所有 mutator 停在安全点;ThreadFlipVisitor对每个线程SetIsGcMarkingAndUpdateEntrypoints(true)、撤销 TLAB/分配栈、用Mark()把线程根从 from-space 改写到 to-space;FlipCallback调RegionSpace::SetFromSpace、交换 live/mark 栈、置is_marking_、灰化新脏的 immune 对象。暂停结束后 mutator 恢复,从此读屏障保证“看不到 from-space 引用”。 - 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 或被标记为不发散。 - ReclaimPhase(并发/短暂停顿):
CleanupClassLoaders;IssueEmptyCheckpoint确保没有 mutator 还持有旧 mark stack 引用;sweep malloc 空间、交换/解绑位图;RegionSpace::ClearFromSpace释放未搬走的区域并记账。到这里,from-space 的物理区域已可复用。 - 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:
- 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)。此时尚未暂停。 - 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,本轮不压缩。 - 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 区。 - ReclaimPhase(并发/读锁下):
ProcessReferences处理软/弱/finalizer 引用(软引用是否前移取决于clear_soft_references_,见 4.6.8);SweepSystemWeaks;CleanupClassLoaders;Sweep(false)回收未标记对象、交换/解绑位图(mark_compact.cc:1715-1748)。此时仍未移动任何对象。 - 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 自动把对象指针做地址翻译。 - 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)。 - 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 shadowmadvise释放(:5249-5330)。 - 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 案例与成本边界
String.equals内建路径:读klass_/count/value;klass_是引用字段(两次Loadwu),因 String 类不可移动而免读屏障,value 是内联字节;该比较路径没有分配,成本是纯比较指令与可能的常量特化缺失(4.5.3)。- 解释路径中的
charAt是kStringCharAtintrinsic;callee quick entry 可能是 AOT intrinsic 方法体(普通 quick 调用,无 JNI 边界),也可能是无代码时的 GenericJNI 回退(此时才建 JNI 帧/LRT)。FastNative 保持kRunnable,若走到 JNI 返回时做 suspend check(4.8)。 - 真正的 GC 相关成本出现在:
new String(...)/字符串拼接(TLAB 分配与可能的 slow path)、对象/接口数组写入(卡表写屏障)、引用字段读取(Baker 读屏障,RV64 无 marking register 时每次字段访问都多一次 entrypoint load),以及 CMC 的页故障处理。 - 成本口径:暂停时间、吞吐、分配速率、屏障指令占比、uffd fault 次数是不同指标,必须分开测;本报告不给数值(未测)。
4.6.10 失败、回退与观察
- uffd 不可用 → 组合退为 Baker+CC(不是错误,是条件分支);
-Xgc:显式指定与构建默认冲突时 Heap 侧有一致性 CHECK(heap.cc的 foreground/background 校验区段),可能直接失败——这是现场“设置了 GC 选项却启动崩溃”的常见根因之一。 - 非 Baker 读屏障:在启用读屏障的配置下,riscv64 直接 FATAL;若构建配置改成 TableLookup 类屏障,RV64 不可用。CMC 的 uffd 路径不使用读屏障,不在此限。
- 挂起超时/卡死:
kill -3取线程转储,关注kWaitingForCheckPointsToRun/kWaitingForGcToComplete等状态;普通 JNI 已转kNative,不是“在 native 里不检查 flag”的典型 holdout;优先查长期运行的 FastNative/CriticalNative、持锁/checkpoint holdout、以及 native 重入路径。 - OOM:看错误消息中的分配大小、target footprint、growth limit 与碎片提示;SoftReference 清理顺序、LOS 阈值(12KB)与 TLAB 用尽是三个优先排查点。
- a17r 相对 a16q2 的可核差异:a17r 新增构建标志
ART_FORCE_CMC_STW_COMPACTION与属性ro.dalvik.vm.force_cmc_stw_compaction(强制 CMC STW 压缩,build/art.go:107-109、mark_compact.cc:117-124);非 sticky GC 时新增PruneDexCacheAndBssStringEntries(heap.cc:2988-2993);reference slow-path 开关改为 volatile 访问(reference_processor.cc:65-83)。这些是行为差异,但不改变“默认 collector 由构建+运行期条件决定”的结论。 - 可复现观察(未执行):
logcat过滤 GC 日志;kill -3;dumpsys meminfo(系统侧);perfetto的 Java heap/GC 轨;ftrace 的userfaultfd/页故障事件;均需设备与版本一致。
失效边界与成本权衡:失效边界包括 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 上生成什么序列、哪些语义点源码并未论证。行号以 ARTandroid17-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:机器码与未论证点
- volatile 读:
HandleFieldGet(code_generator_riscv64.cc:2634-2680)生成fence rw,rw→ load →fence r,rw(kAnyAny/kLoadAny),没有使用 AMO/acquire load;对照 arm64 是单条LoadAcquire。 - volatile 写:
StoreSeqCst(:1033-1064)对 ≥4 字节用AMOSWAP.D/W(…, AqRl::kRelease)丢弃返回值,对 <4 字节用fence rw,w+ store +fence rw,rw。源码在这里只给了 Release 标注,没有注释论证它满足 JMM 对 volatile 写的顺序一致要求;这一语义点列为待查(需要对照 RISC-V 内存模型规范与后续补丁,或做 litmus)。 - fence 映射:
GenerateMemoryBarrier(:6232-6252)四种:rw,rw / rw,w / r,rw / w,w;FenceTso存在但 JMM 路径未调用。 - Unsafe/VarHandle:
GetLrScAqRl(intrinsics_riscv64.cc:1085-1098)把 acquire/release/seq_cst 映射到lr.aq/sc.rl/lr.aqrl;GetAmoAqRl(:1101-1111)映射 AMO 的 aq/rl。强 CAS 是“LR→比较→SC→失败重试”循环;weak 版本只尝试一次并把 SC 结果取反(:1298-1314)。get-and-update 不是一律 LR/SC:完整字宽的 set/add/and/or/xor 用AMOSWAP/AMOADD/AMOAND/AMOOR/AMOXOR,只有带 mask 的 32 位 set/add 用 LR/SC(:2256-2348)。LR/SC 区间受ScopedLrScExtensionsRestriction限制(只允许 Zca,assembler_riscv64.h:80-82),区间内分支必须是 bare 分支以免 clobber TMP;16 条指令是 ART 对受约束 LR/SC 前向进展保证的序列约束,不是所有 LR/SC 指令的普遍合法上限。 - 正确性边界:SC 允许伪失败——不重试会丢写;把 Java 强 CAS 降级成单次尝试是明确错误。普通字段无任何顺序保证;acquire/release 单边不能替代另一边。litmus 现状:仓库内检索不到
litmus文件或 harness;相关测试只有功能/回归(test/719-varhandle-concurrency、test/438-volatile、test/872-volatile-fp;1004/635是 ARM64 的 checker 用例)——因此 RV64 的 JMM 正确性目前只有代码审查与功能测试,没有可复现的 litmus 依据 [S30][S46]。
4.7.6 失败、回退与观测
- 挂起超时:
UnsafeLogFatalForThreadSuspendAllTimeout()(thread_list.cc:308-323)打印 “Thread suspend timeout” 与锁/线程转储后 FATAL;WaitForSuspendBarrier的 culprit 信息是定位“谁没到安全点”的直接线索。 - 空 checkpoint 的已知弱保证:
RunEmptyCheckpoint的 over-optimization(待办注释 b/382722942)意味着不能把它当作与RunCheckpoint等价的同步点。 - volatile 语义待查:见 4.7.5;在获得规范/补丁依据或 litmus 结果前,不写“RV64 volatile 写已证明满足 JMM”。
- 可复现观察(未执行):
kill -3查看线程状态与 monitor 归属;logcat看 suspend timeout/checkpoint 相关日志;-Xint让执行退回解释器以排除 JIT;VarHandle/volatile 的并发回归用test/719-varhandle-concurrency类用例(功能级)。 - a17r 与 a16q2:本章引用的状态机、挂起、锁与 JMM 代码在两棵树中未发现行为差异(线程笔记的逐文件对照结论),但 litmus/设备依据两版都没有。
下一章进入 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这两条案例热点在边界上的真实形态。行号以 ARTandroid17-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/jclass | LRT | 异常轮询 | 挂起响应 | 其它 |
|---|---|---|---|---|---|---|
| 普通 native | Runnable→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 的分界(案例相关)
- managed:
A0=ArtMethod*、实参从A1起;浮点只用FA0-FA7,超 8 个上栈,不溢出到整数寄存器;S1=xSELF(TR,不保存);GP留给影子栈;TMP/TMP2汇编保留;帧尺寸 112/128/208/224/480B、16B 对齐(registers_riscv64.h、asm_support_riscv64.h)。 - native(psABI):浮点先
FA0-FA7,用尽后进A0-A7再上栈;栈参 8 字节;窄整数先扩到 32 位再符号扩展到 XLEN;float采用 NaN-boxing(高 32 位置 1)(calling_convention_riscv64.cc:336-390;quick_trampoline_entrypoints.cc:1601-1609)。 - 转换点:JNI stub 的
MoveArguments()完成 managed→native 的寄存器/栈重排与引用转jobject;返回时FA0/A0回到 managed 位置(calling_convention_riscv64.cc:138-147)。@CriticalNative 的隐藏ArtMethod*与 synchronized 普通 native 的锁对象都放T0(:393-419)。
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 案例连接与观测
String.charAt是@FastNative(java_lang_String.cc:36、注册:137),同时是kStringCharAtintrinsic(intrinsics_list.h:112):解释执行时循环每轮有两处charAt(String.java:1675-1682)。callee 若在 boot image 中有 AOT intrinsic 方法体,两次都是普通 managed quick 调用(count/bounds/字节加载),没有 JNI 帧、MoveArguments、JNIEnv 或 LRT;只有quick_code == nullptr回退 GenericJNI 时,才走 JNI 帧 +MoveArguments+ LRT,并由String_charAt执行。thread.cc:4368-4381明确以“PC 在 boot image”区分 AOT String.charAt 与 GenericJni 路径。是否已有 AOT intrinsic 代码取决于实际 boot image/profile/过滤器/产物,本报告未测,不能写成通则。String.compareTo的调用点内建是kStringCompareTo(intrinsic 注册见intrinsics_list.h:213;:138是 native 方法注册),但其 callee 是否有 AOT intrinsic body 未确认:BuildSimpleIntrinsic没有kStringCompareTocase;BuildIntrinsic会退化为对方法自身的HInvokeStaticOrDirect;RV64VisitStringCompareTo(intrinsics_riscv64.cc:3186-3201)对 nullable 参数会选择kCallOnSlowPath,而TryCompileIntrinsic拒绝非 leaf 的 intrinsic 方法体(optimizing_compiler.cc:1079-1084)。因此本例可写“nullable 图→CanCall→NotLeaf→拒绝”的条件链,但不能推广为所有 compareTo 必走 JNI;可证明非 null 图下的 callee AOT body 本文未核。callee 若实际落 JNI stub/GenericJNI,@FastNative仍建立/弹出 LRT;调用点内建与 callee 入口是两条轴,不能因登记 intrinsic 就推断 callee 已有 AOT 方法体。System.arraycopy的 9 个入口都以FAST_NATIVE_METHOD注册(java_lang_System.cc:242-252);若实际走到 JNI stub/GenericJNI,它享受 FastNative 的轻量边界(跳过状态转换与artJniMethodStart,但不跳过 LRT)。@FastNative的代价是挂起响应延迟到返回点:短小调用可接受;若 native 实现长时间不返回,会拖慢 GC/调试挂起——这是注释里明确的风险(jni_internal.cc:2754-2781,并说明旧!前缀已废弃)。- 可复现观察(未执行):
-Xcheck:jni打开 CheckJNI(错误串"JNI DETECTED ERROR IN APPLICATION: ...");-Xjnitrace:<lib>/VLOG(third_party_jni)打印"GenericJNI: <method> -> <native_code>";库加载失败串见 4.8.6。设备与版本一致是前提。 - a17r vs a16q2:JNI stub、calling convention、@CriticalNative fixup、String/System native 表相同;a17r 的差异集中在 simulator 包装(
art_quick_generic_jni_trampoline_simulator为unimp)与 TI agent 查找新增 shorty/JNICallType 参数 [S43][S44][S45]。
下一章转入上游演进与支持分层,把“源码落地/测试/发布/产品”四层逐项对齐。
第 9 章(4.9)上游演进与支持状态分层:从提交到产品可用
本章回答:RV64 支持在公开 ART 里怎样一步步落地;每条能力目前停在哪一层(源码/测试/发布/产品);哪些说法是事实、哪些是判断。历史事实来自公开 git(有界范围复核),上游测试记录只作为“外部记录”转述,不是本报告实测。
4.9.1 分支模型与 pin(2026-10-19 复核)
| 线 | ref | tip | tip 提交日期 | tag |
|---|---|---|---|---|
| 冻结主干 | main | 6484611f | 2025-03-26 | 无 |
| 25Q2 | android16-release | ed6c006b | 2025-04-09 | android-16.0.0_r1=r2 |
| 25Q3 | android16-qpr1-release | e5c642b5 | 2025-07-18 | android-16.0.0_r3 |
| 25Q4 | android16-qpr2-release | 1690c691 | 2025-10-12 | android-16.0.0_r4 |
| 26Q2 | android17-release | b753cf97 | 2026-04-30 | android-17.0.0_r1 |
| 26Q2 安全 | android17-security-release | ecd0dfa4 | 2026-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/Zbs | 60fdcb2b27…、fddd6a4c78、b1a0d10552、b17478f8e8 | 多为 host-gtest;部分 run-gtests | a15(Zbs 为 a16) | 无公开承诺 |
| codegen 全指令 + JNI + nterp | 8e47d43161、8838bbf1f5、993bcb1f02 | target --optimizing --jit;Cuttlefish zygote 冒烟;VM over SSH | a15 | 无公开承诺 |
| 字符串/数学/原子内建、MethodHandle | 303d061d79 起、91bb8a6fac、bda2905666、54710ff675 等 | host-gtest 与 target run-test 混合 | a15→a17r 逐步增加 | 无公开承诺 |
| RVV 汇编/反汇编 | fb0a1bb8e6、036526f5f1、5512b4e610 | 汇编器测试(无向量 codegen 测试) | a15 | 无公开承诺 |
| 向量寄存器分配 part 1 | dde31eea21 | 仅 host-gtest | a17r | 无公开承诺 |
| fast baseline compiler | 仅 ARM64 实现(SupportsFastCompiler 硬编码 kRuntimeISA == kArm64;FastCompiler::Compile 无 RV64 分支) | 无 RV64 记录 | 不适用 | 无 |
| Orange Pi 测试载体 | c2c3494e65 | run-gtests.sh + target --optimizing;TEST_ONLY | a17r | 测试设施,非产品能力 |
④ 层(产品可用)在本轮可查的公开来源与 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 通过官方
android-review.googlesource.com/changes做了 3 条有界 REST 查询(HTTP 200;原始响应已本地留存):status:open riscv命中 1、riscv64命中 18、file:riscv64命中 7;按_number去重后 18 条,分支分布为 17 条main+ 1 条master(2241750)。下节给出快照表。这是查询结果而不是完整枚举;NEW也不等于 release 分支缺功能;同 Change-Id 的合入谱系与“当前 patchset 语义等价”是两件事,表中分别标注。[S47] - 厂商/下游分支:
riscv-android-src等公开协作仓库只作对照(本轮未联网复核其分支/提交);下游可能领先于 AOSP,也可能只是镜像,未逐 commit 对照前不计入“AOSP 官方能力”。aml_*是内部 automerger 遗留,不构成厂商分支依据 [S33]。 - 预测(均为判断):26Q2 之后公开 RV64 提交维持低密度(修复/小优化为主);RVV 走向 codegen 需 “part 2+” 且进入 release 分支;fast baseline 短期不覆盖 RV64;产品可用性继续由厂商 BSP/下游树决定 [S33]。
在审候选快照(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 | 最后更新 | 主题(截断) | 合入谱系与语义核对状态 |
|---|---|---|---|
| 3693991 | 2025-08-11 | riscv64: Optimize IR using bit manipulation | 相同 Change-Id 的合入谱系已找到(022f0bd86b,在 a17r);当前 patchset 与合入版的逐 diff/完整语义等价未核 |
| 3580772 | 2025-04-23 | riscv64: Call target method in accessor MHs when it is set | 相同 Change-Id 的合入谱系已找到(1c7e13a9b9,在 a17r);当前 patchset 语义等价未核 |
| 3556506 | 2025-07-18 | riscv64: Optimize IR using bit manipulation | 与 3693991 同 subject、不同 Change-Id;未找到同 Change-Id 合入记录,语义等价未核,不能据此断言该优化功能不存在 |
| 3556505 | 2025-05-22 | riscv64: Optimize Div and Rem with any constant | 未找到同 Change-Id 合入记录;语义等价未核,不能断言“缺该功能” |
| 3238038 | 2024-08-23 | riscv64: Correction of comment to funct3 ROR instruction | 注释修正类,无机制影响 |
| 3105760 | 2024-07-25 | [NOT FOR MERGE] Provide stable base for non-rebased patches | 排除项(显式 NOT FOR MERGE) |
| 3076228 | 2024-05-07 | x86-64: Clean up locking in JNI stubs | 非 RV64(查询噪声) |
| 2816976 | 2024-08-13 | riscv64: update vm img and qemu path | 测试环境类 |
| 2813731 | 2023-11-03 | riscv64: Guard Zbb usage with __riscv_zbb | 门控类;a17r 已有特性消费链;未找到同 Change-Id 合入记录,语义等价未核(“同上”同此口径) |
| 2797148 | 2023-11-03 | riscv64: Guard Zbs usage with __riscv_zbs | 同上 |
| 2797147 | 2023-11-03 | riscv64: Guard Zba usage with __riscv_zba | 同上 |
| 2797144 | 2023-11-01 | riscv64: Gate Zba usage behind the instruction set feature | 同上 |
| 2796933 | 2023-11-01 | riscv64: Gate Zbb usage behind the instruction set feature | 同上 |
| 2756531 | 2024-08-13 | riscv64: boot VM with vector extension | VM/测试启动项,不是 RVV codegen;与 26Q2 codegen FATAL 不矛盾 |
| 2673135 | 2023-09-29 | arm64: store tlab information in the marking register | 非 RV64;作为“arm64 有 marking register”的对照项 |
| 2517296 | 2023-07-12 | riscv64: Let odrefresh run in verification mode | 旧候选;与 26Q2 odrefresh 默认 speed-profile 的关系未核 |
| 2318822 | 2024-06-10 | Minimal riscv64 support for running tests on the interpreter | 旧解释器测试支持;不改变 26Q2 能力判断 |
| 2241750 | 2022-10-06 | Revert "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 版本差异实证(与案例/后续优化直接相关)
- VDEX/quickening:a17r 的 vdex 只有 checksum/dex/verifierdeps/typelookup 四段,无 quickening;
quicken过滤器名只作兼容告警并映射 verify(4.2.3)[S32][S36]。 - CompactDex:25Q2 树中仍有 4 个
compact_dex*文件,25Q3(e5c642b5)为 0——移除发生在 25Q2→25Q3 窗口 [S39]。 - 镜像格式:a17r 为 119、a16q2 为 118,跨版本互不兼容(4.2.2)[S07]。
- JIT 的 fast 路径:
TryPatternMatch覆盖 arm/arm64,SupportsFastCompiler仅 arm64(FastCompiler::Compile无 RV64 分支),RV64 走 baseline→optimized 通用阶梯(4.4.2)[S28]。 - odrefresh 过滤器:a17r 的主镜像与 mainline 默认均为
speed-profile(odrefresh.cc:130/133);a16q2/main 时代 mainline 为verify(旧记录)。这条差异会改变“哪些方法有 AOT 代码”,是版本对比时必须固定的条件 [S32]。 - 字符串内建:StringEquals/StringCompareTo 等从 a15 起存在;但 libcore 的
String.equals当前是 charAt 循环(4.1.4),内建的调用点匹配对象是String.equals本身,不要按旧资料写成“StringLatin1.equals 内建” [S13][S21]。
4.9.6 失败/反例与观测方法
- 反例 1(目录≠支持):a14 有
mterp/riscv64/*.S与code_generator_riscv64.h,但没有可用解释器与 codegen——必须看IsNterpSupported、文件实现与 release 祖先。 - 反例 2(单个 commit≠完备):RVV 汇编器存在不等于向量 codegen;4.5.6 的 FATAL 是直接反证。
- 反例 3(cherry-pick 可见性):release 分支只反映被 cherry-pick 的提交;main 冻结后,“主线上没有”不等于“内部没有”。
- 日期口径:作者日期与提交日期可差数周/数月(automerger);本文历史描述默认用提交日期,引用提交时两者都可复核。
- 可复现核查(未执行的部分只在本地 git 完成):
git merge-base --is-ancestor <commit> <tip>判 ③ 层;git log -1 <tag>判 tag→commit;git ls-tree -r <tip> | grep -ci riscv判文件面;在审记录的范围以 [S47] 的三条查询与固定截点为限,不用未保留的命令输出补写“已核实”。
成本与权衡(史料口径):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 看指令数/分支 miss | ART:intrinsics_riscv64.cc(M1-②可改);libcore:不改语义;构建:过滤器/profile | 458-checker 用例 + 随机字符串对拍(Latin1/UTF16/空串/常量串);A/B 同构建 | ①事实 ②事实(缺特化)③机制解释;收益=假设 |
| M2 | System.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 同 workload | ART:codegen/ABI(寄存器预算,需评估);不可改 libcore | CC 的 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 | 事实(机制);影响=需测 |
| M6 | RVV 向量化机会(字符串/数组批量操作) | 当前 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 组合的启动/回退测试(可能不存在回退) | 事实(探测缺省);风险=按目标核 |
| M8 | AOT 覆盖不佳导致“启动后仍在解释/刚 JIT” | profile 质量/过滤器/odrefresh 工件(4.4.1/4.9.5) | profman --dump-info-for、oatdump 方法级 AOT 状态、开机 odrefresh 日志 | 构建/配置/odrefresh;ART 不改为先 | 启动 macrobench 同样本 A/B(需设备) | 事实(机制);覆盖率=需测 |
| M9 | JIT 热方法达峰速度与 arm64 不同 | 无 fast tier/pattern matcher(4.4.2) | -verbose:jit 时间线;启动期编译事件;PMU | ART:JIT(跨架构特性,优先级需上游) | run-test jit + 同用例 ns/op 稳定性 | 事实(arm 专属);幅度=假设 |
4.10.2 观测栈的最小集(条件与口径)
- 先固定执行条件:构建 commit、过滤器(
compiler_filter)、profile、debuggable、-Xint/-Xusejit、GC 选项与设备属性;任何跨设备对比先对齐这六项,否则测到的是模式差异。 - 方法级入口:
oatdump --oat-file/--image看 AOT 与入口点;-verbose:jit看编译;profman看 profile;kill -3看线程/锁状态。 - 硬件计数:perf/simpleperf(需内核/权限);优先看指令数、分支失败、cache miss、LLC、原子相关事件;若内核不支持某事件,记录为缺口,不换算。
- 机器码:同版本
disassembler/oatdump --disassemble;没有产物就写“未执行”,不伪造。 - 启动/系统:atrace/boot trace 看阶段;GC 日志看暂停与并发阶段;属性
dalvik.vm.*记录实际值(不是默认值)。
4.10.3 问题归属
| 归属 | 典型问题 | 说明 |
|---|---|---|
| ART 编译器/内建 | M1-②、M2、M3、M4、M6 | 改 intrinsics_riscv64.cc/code_generator_riscv64.cc/assembler;需遵守 ABI |
| ART 运行时/JIT/profiles | M1-③、M8、M9 | 入口点、过滤器、profile、odrefresh 工件 |
| libcore | M1-①(语义源头)、字符串/数组 API 形态 | 本报告不改 libcore;改动需评估兼容性 |
| bionic/工具链 | M2(memmove)、M7(ISA/ABI/构建) | Bionic 内部机制由独立专题说明;本报告只给接口点 |
| 硬件/PMU 数据 | M3、M4、M6、M7 | 需要核/板卡数据与事件支持;无数据则只能标记假设 |
4.10.4 正确性门与实验模板
- A/B 模板:同一构建参数、同一设备状态,只改一个变量(内建路径或模式),交替运行 ≥3 轮,记录中位数与离散度;报告“效应量+条件”,不写无条件的提升。
- 功能门:任何 ART 编译器/内建改动,先跑
test/458-checker-riscv64-*与相关 run-test(解释/JIT/AOT 三模式);字符串/数组另加随机对拍(compressed/UTF16、空串、常量串、越界与 NPE)。 - 并发门:原子/锁/屏障改动需跑 GC stress、并发测试与自建 litmus;CC 场景验证 to-space 不变量。
- 性能与正确性分开陈述:性能数字不得暗示正确性;正确性未验证时不得写“优化完成”。
- 版本门: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 只按公开 pin4ebfb391…做代表链交叉引用,完整范围归 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-test | test/2239-varhandle-perf-vh-*(17 组) | VarHandle/原子访问的临时微基准 | 是性能微基准,但不是通用 suite;run.py 默认清空 stdout 以让 diff 通过,输出需另行保存 |
| ART run-test / gtest | test/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-class | 2 | timeConstClassWithConflict / WithoutConflict |
const-string | 2 | timeConstStringsWithConflict / WithoutConflict |
jni-perf | 4 | timeFastJNI / timeEmptyCall / timeSOACall / timeSOAUncheckedCall |
jobject-benchmark | 7 | local/global/weak global 的 add-remove/decode + handle scope decode(7 个 native) |
scoped-primitive-array | 12 | Byte/Short/Int/Long × small/medium/large |
stringbuilder-append | 8 | String、long String、String+int/double/float、huge double/float、混合 |
string-indexof | 12 | 目标字符 0/1/2/3/4/7/8/F/G/V/W/_ |
type-check | 10 | checkcast/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 阈值与 JitAtFirstUse | CanMethodUseNterp 限制、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 工作树为准。)
- [S01] ART 仓库与版本 pin 总表 — 公开 refs 复核(git 协议)— https://android.googlesource.com/platform/art
- [S02] ART 主 pin —
refs/heads/android17-release=b753cf97923c3695338d21466fa14c57b480a59a— https://android.googlesource.com/platform/art/+/b753cf97923c3695338d21466fa14c57b480a59a - [S03] ART main(冻结) —
6484611fd45e69db9f33f98bfd6864014b030ecf— https://android.googlesource.com/platform/art/+/6484611fd45e69db9f33f98bfd6864014b030ecf - [S04] ART 25Q4 对照 —
android16-qpr2-release=1690c6912a7972c9e62c39b48c706de9b8b18b4a(r4 同点) — https://android.googlesource.com/platform/art/+/1690c6912a7972c9e62c39b48c706de9b8b18b4a - [S05]
runtime/runtime.cc(Init/Start/PreZygoteFork/InitNonZygoteOrPostFork) — 行号见正文 - [S06]
runtime/jni/java_vm_ext.cc(JNI_CreateJavaVM :1237) - [S07]
runtime/gc/heap.cc(:520 LoadBootImage)与runtime/gc/space/image_space.cc(:3016) - [S08]
runtime/class_linker.cc(InitFromBootImage :1351;AddImageSpace/入口改写 :2332-2342;无镜像 :699/:914) - [S09]
runtime/arch/riscv64/quick_entrypoints_riscv64.S(入口/OSR/deopt/分配/锁宏挂载) - [S10]
runtime/arch/riscv64/registers_riscv64.h、asm_support_riscv64.h/.S(TR/SCS/帧尺寸/锁宏) - [S11]
compiler/optimizing/code_generator_riscv64.cc(VisitArrayGet is_string_char_at :2881 起;屏障/原子/向量 FATAL 等) - [S12]
compiler/optimizing/intrinsics_riscv64.cc(VisitStringEquals :1141/:1152;其余内建) - [S13]
runtime/intrinsics_list.h(StringCharAt :112、StringIsEmpty :113、StringLength :114、StringEquals :214、StringCompareTo :213 等) - [S14]
compiler/optimizing/instruction_builder.cc(kStringCharAt → HArrayGet+BoundsCheck :2125/:2225) - [S15]
compiler/optimizing/instruction_simplifier.cc(SimplifyStringCharAt :2859;StringLength 简化);compiler/optimizing/intrinsics.h(:182-187 补偿说明);compiler/optimizing/code_generator.cc(GetArrayLengthOffset/GetArrayDataOffset :233-243) - [S16]
runtime/mirror/string.h(count_/hash_code_/内联 value 联合 :316-326;CountOffset/ValueOffset :61-76);runtime/mirror/string-inl.h(CharAt :45 起) - [S17]
runtime/native/java_lang_String.cc(String_charAt :36;String_compareTo :41;FAST_NATIVE 注册 :137-138) - [S18]
runtime/interpreter/mterp/nterp.cc(IsNterpSupported/CanRuntimeUseNterp/NterpGet*/NterpHotMethod) - [S19]
runtime/interpreter/mterp/riscv64/main.S与invoke.S(寄存器约定、native 调用注释 :19、computed-goto、OSR 自跳) - [S20]
dex2oat/linker/image_writer.cc(boot image 非 intrinsic native 写 JNI stub :1488-1495;intrinsic native 经 JniCompile :1244-1277(TryCompileIntrinsic :1269)生成 AOT 方法体;quick_code==nullptr 才回退 GenericJNI :3530-3534);compiler/optimizing/optimizing_compiler.cc:1244-1277、runtime/thread.cc:4368-4381(AOT String.charAt 与 GenericJni 区分) - [S21] libcore
android17-release4ebfb391ee0109d16abe6e2bc965724b940928c5:String.java(equals :1660;length :1209-1221;isEmpty :1229-1240;charAt native :1273;compareTo native :1898;字段区注释 :172-183) — https://android.googlesource.com/platform/libcore/+/4ebfb391ee0109d16abe6e2bc965724b940928c5/ojluni/src/main/java/java/lang/String.java - [S22] libcore
StringLatin1.java/StringUTF16.java/System.java(arraycopy 声明 :448) - [S23] frameworks/base
android17-release94b4c163b7dfe5ce3607f7bb8456f9573f7de57d:ZygoteInit.java(main :824;参数解析 :856;preload :892;ZygoteServer :909;forkSystemServer :912;runSelectLoop :926;zygoteInit :983;nativeZygoteInit :993/:1008;childZygoteInit :1003) - [S24] frameworks/base:
RuntimeInit.java(main :352;commonInit :234;nativeFinishInit :367);AndroidRuntime.cpp(startVm :657;JNI_CreateJavaVM :1208;nativeZygoteInit 回调 :284-287/注册 :311-315;CallStaticVoidMethod :1344);cmds/app_process/app_main.cpp(AppRuntime::onZygoteInit :92-97 → ProcessState::startThreadPool) - [S25]
runtime/native/dalvik_system_ZygoteHooks.cc(nativePreFork :255→PreZygoteFork :259;PostZygoteFork :265;nativePostForkChild :294→InitNonZygoteOrPostFork :434/436) - [S26] libcore
dalvik/src/main/java/dalvik/system/ZygoteHooks.java(preFork :198;nativePreFork :202;postForkChild :230/232) - [S27] 技术写作与引用规范(作者本地执行)
- [S28]
runtime/jit/jit.cc(1 worker :1135;fast/pattern arm64 :138/:1262;MaybeEnqueueCompilation :1747;baseline/optimized :1819-1824)、jit_options.cc:27/34、parsed_options.cc:514、jit_code_cache.h:185 - [S29]
runtime/oat/stack_map.h、runtime/quick_exception_handler.cc:583/612、runtime/deoptimization_kind.h、compiler/optimizing/stack_map_stream.cc:49/84/318/382 - [S30] GC:
runtime/gc/collector/concurrent_copying.cc:214-269/1600-1690/3375、mark_compact.cc:908-966/1573-1650/1715-1748/3204/4297-4470/4605/5249、mark_compact-inl.h:251/360-405、reference_processor.cc:171-214、heap.cc:2002-2160、thread_list.cc:910/980、read_barrier-inl.h:35-156、write_barrier-inl.h:30-43、lock_word.h:94-155 - [S31]
compiler/optimizing/optimizing_compiler.cc:620-716/782/943-981/1079-1084/1142-1154(非 native Compile 的 intrinsic 整方法体路径与 leaf 拒收)、instruction_builder.cc:494-525(无 simple IR 的自 invoke)、intrinsics_riscv64.cc:1141-1149(StringEquals CreateNoCall)、builder.cc:90、register_allocator_linear_scan.cc:393/1238/1470、stack_map_stream.cc:49/84/318/382 - [S32]
runtime/oat/oat_quick_method_header.*、runtime/vdex_file.h:80-86、dex2oat/dex2oat.cc:1496/1889/1952/3109/3157/3248、dex2oat/driver/compiler_driver.cc:314/430/861、odrefresh/odrefresh.cc:130/133、artd/* - [S33] RISC-V64 公开提交序列(2023-01 骨架 → 2026 维护) — 由公开提交页/refs 复核后引用
- [S34] RV64 汇编/特性/对照:
compiler/utils/riscv64/assembler_riscv64.{h,cc}(扩展掩码 :1909-1923、压缩编码 :1956-1973/:2496、ScopedNoC :2893)、runtime/arch/riscv64/instruction_set_features_riscv64.cc:40-145(FromCpuInfo/Hwcap/Assembly/CpuFeatures=UNIMPLEMENTED→FromCppDefines;AddFeaturesFromSplitString)、compiler/optimizing/intrinsics_arm64.cc:2539(String.equals 对照) - [S35]
runtime/native/java_lang_System.cc(System_arraycopy:50;System_arraycopyTUnchecked:175;typed unchecked :189/:195)与 libcoreSystem.java:448(native 声明) - [S36]
libartbase/base/compiler_filter.h:34(8 档过滤器)与compiler_filter.cc:175-190(obsolete 名映射,含 quicken→verify 告警) - [S37] 测试与工具目录(
test/run-test、test/458-checker-riscv64-*、TEST_MAPPING、tools/buildbot-*、oatdump/、profman/) — 目录级引用,未逐用例执行 - [S38] 本报告图表与覆盖矩阵(作者自检记录)
- [S39] DEX/类状态/验证组:
libdexfile/dex/standard_dex_file.cc:29-40(DEX 版本表)、runtime/class_status.h:111-129(ClassStatus)、runtime/class_linker.cc(FindClass/DefineClass/LoadClass/LinkClass/InitializeClass/VerifyClass)、runtime/verifier/method_verifier.cc - [S40] nterp 支持与帧:
runtime/nterp_helpers.cc(帧尺寸 :152-169、Reference/Registers/DexPC :179-227、NterpGetCatchHandler :223)、runtime/interpreter/interpreter_cache.h:54(256 项直接映射) - [S41] mterp 删除提交
e4f983c808(Nicolas Geoffray,2021-07-13;android13+ 生效) - [S43] JNI 编译器与 stub:
compiler/jni/quick/jni_compiler.cc(ArtQuickJniCompileMethod :770;ArtJniCompileMethodInternal :77-729;MoveArguments/Call/尾调用 :212-425;LRT cookie :285-306/518-524)、compiler/utils/riscv64/jni_macro_assembler_riscv64.cc(BuildFrame :62-96;MoveArguments :261-369)、compiler/jni/quick/riscv64/calling_convention_riscv64.cc(FrameSize :218-235;psABI 溢出 :336-383)、runtime/arch/riscv64/native_entrypoints_riscv64.S(dlsym lookup :54-92)、runtime/arch/riscv64/jni_entrypoints_riscv64.S(critical lookup)、runtime/entrypoints/quick/quick_trampoline_entrypoints.cc(GenericJNI :2048-2200) - [S44] JNI 运行时与 arraycopy:
runtime/jni/java_vm_ext.cc(JNI_CreateJavaVM :1237;LoadNativeLibrary :952-1168;FindNativeMethod :276-318)、runtime/jni/jni_internal.cc(Throw/Exception* :804-862;RegisterNatives :2701-2859)、runtime/native/java_lang_System.cc(System_arraycopy :50;typed unchecked :175-240;FAST_NATIVE 注册 :242-252)、runtime/mirror/array-inl.h(Memmove :134-173)、runtime/mirror/object_array-inl.h(AssignableMemmove) - [S45] 信号/故障:
runtime/arch/riscv64/fault_handler_riscv64.cc(NPE :48-85;SuspensionHandler 未实现 :87-90;StackOverflow :92-126)、runtime/fault_handler.cc(SIGSEGV/SIGBUS 分发 :301-395;sigchain 注册 :138-166)、build/art.go:126-137(栈溢出预留) - [S46] 本报告机制笔记(作者本地记录,不公开)
- [S47] Gerrit 查询式记录(3 条 REST 查询,HTTP 200,2026-10-19T15:13:22Z;去重 18 条=17 main+1 master;原始响应本地留存;
merged_equivalent仅为 Change-Id+branch 匹配,语义等价未核) - [S48] frameworks/base
cmds/app_process/app_main.cpp(AppRuntime::onZygoteInit:92-97)与core/jni/AndroidRuntime.cpp(nativeZygoteInit回调 :284-287/注册 :311-315)
附录 A:主要结论与依据对照
| # | 结论 | 主要依据(pin) | 所在章节 |
|---|---|---|---|
| A1 | 公开 main 冻结于 2025-03-26;26Q2 的 android17-release 是最新公开开发前沿,android17-security-release 不是其后代 | [S01][S33];merge-base --is-ancestor 与 ls-remote 复核 | 背景、4.9.1 |
| A2 | a17r 的 VDEX 没有 quickening 段;quicken 过滤器名只作兼容告警并映射 verify | runtime/vdex_file.h:80-86、compiler_filter.cc:181-184 [S32][S36] | 4.2.3 |
| A3 | String 字符数据内联在对象中(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 |
| A5 | ARM64 的 StringEquals 有 const-string 特化,RV64 没有 | intrinsics_arm64.cc:2539/2633(cutoff 32B)、RV64 无 GetConstString/GetFlaggedCount [S12][S34] | 4.5.3 |
| A6 | riscv64 的 ISA 特性探测无运行期实现(FromCpuInfo/Hwcap/Assembly/CpuFeatures→UNIMPLEMENTED→FromCppDefines) | instruction_set_features_riscv64.cc:52-90 [S34] | 4.5.1 |
| A7 | RVV 只有汇编器/反汇编器与向量寄存器基础设施;向量指令 codegen 为 FATAL | code_generator_riscv64.cc:6492/6630/6640;SBC workaround 仅测试构建 [S11][S33] | 4.5.6 |
| A8 | Java 堆内对象引用恒为 32 位压缩;JNI jobject/native 指针/ArtMethod* 仍是 64 位。默认 collector 由构建 CMC 与运行期条件共同决定;SysPropSaysUffdGc() 的 OR 项含 !IsAtLeastV(),U 及以下未 force_disable 且内核支持时默认 uffd/CMC,V+ 依赖显式属性;独立 CMS 已删,仅余选项映射的并发 MarkSweep | object_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 |
| A10 | volatile 写 ≥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 |
| A12 | System.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 |
| A15 | CC 一轮: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 |
| A16 | CMC 一轮: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 |
| A17 | Zygote 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 |
| A18 | debuggable 不等于禁止所有机器码:nterp/AOT Java 不可用、fast tier 被拒,但 JIT 仍可生成带 entry/exit hooks 的代码;-Xint 才强制解释 | instrumentation.cc:232-238/331-335、optimizing_compiler.cc:1440-1474 [S38] | 4.4 |
| A19 | RV64 原子: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 |
| A13 | RV64 的 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 侧仅接口面 |
| Q2 | DEX/类加载/验证/分派/异常/反射/BCP | 核心 | 4.2 | 已覆盖 | [S08][S39];反射/MethodHandle 以入口级覆盖 |
| Q3 | 解释器谱系与 RV64 真实性 | 核心 | 4.3 | 已覆盖 | [S18][S19][S40][S41] |
| Q4 | AOT/dex2oat/IR/regalloc/stack map/产物 | 核心 | 4.2/4.4 | 已覆盖 | [S31][S32][S38] |
| Q5 | JIT/热度/OSR/deopt/profiles/过滤器 | 核心 | 4.4 | 已覆盖 | [S28][S29][S36];设备阈值未测 |
| Q6 | GC/分配/collector/屏障/引用/OOM | 核心 | 4.6 | 已覆盖(源码级) | [S30];CC/CMC 各一轮完整链与图 7;设备默认未测 |
| Q7 | 线程/挂起/Monitor/JMM→RVWMO | 核心 | 4.7 | 已覆盖 | [S30][S43][S46];volatile SC 语义待查 |
| Q8 | JNI/Fast/CriticalNative/ABI/信号 | 核心 | 4.8 | 已覆盖 | [S43][S44][S45];FastNative 仍建 LRT、native 库卸载链已补 |
| Q9 | RV64 后端/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;测量未做 |
| E1 | libcore/OpenJDK 上游同步流程 | 扩展 | 背景/4.5 | 部分交付 | [S21][S22];同步流程不在 art 仓 |
| E2 | Bionic/linker 内部 | 扩展 | 4.8 接口面 | 部分交付(边界外) | Bionic 内部机制见独立 Bionic 专题,本报告只覆盖接口边界 |
| E3 | 设备实测(启动/GC/争用) | 扩展 | 4.10 观测口径 | 未进行 | 本报告未做设备实验;后续可按专项实验补充 |
| E4 | 源码独立复核 | 扩展 | 附录 C | 已完成 | 已按独立源码复核及后续修订复核修正;设备与性能实验未进行 |
| E5 | 反汇编/机器码对照 | 扩展 | 4.5 | 部分交付 | 机制与源码序列已给;未产出反汇编 |
| E6 | 浏览器三档宽度检查(1440/1280/390) | 扩展 | HTML 交付物 | 已独立渲染复核;公开文字版作者回归通过 | 三档根宽相等、7 图/档、0 broken/0 外部请求;不冒充新一轮独立浏览器测试 |
附录 E:术语与缩略语
| 术语 | 说明 |
|---|---|
| nterp | ART 的快速解释器(computed-goto、按架构汇编实现;riscv64 目录在 runtime/interpreter/mterp/riscv64) |
| mterp | 2021 年删除的旧解释器;本报告只作历史引用 |
| managed ABI | ART 私有调用约定:A0=ArtMethod、实参自 A1、FP 只用 FA0–FA7、xSELF=s1 |
| psABI | RISC-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/SC | RISC-V 原子原语对;CAS/锁的实现基础;受约束序列有 16 条指令/扩展限制(不是所有 LR/SC 的普遍上限) |
| FastNative | 保持 Runnable、不调 artJniMethodStart,但仍建立/弹出 JNI 局部引用帧(LRT) |
| RVV | RISC-V Vector 扩展;本报告限定为“汇编器/基础设施可用,codegen 未实现” |
| 26Q2 / 25Q4 | 发布列车(年+季度);android17-release 的合并目标为 26Q2-release |
| 支持四层 | 源码落地 → 构建/测试验证 → release 包含 → 产品可用(无公开承诺) |