在 macOS (Apple M 系列) 上编译 Debug 版 JDK
本文记录在 Apple Silicon (M 系列, aarch64) 上编译 JDK 25 fastdebug 版本的完整过程, 包括遇到的工具链兼容问题及解决方案。
一、构建环境
| 项目 | 值 |
|---|---|
| 架构 | arm64 (Apple Silicon) |
| 系统 | macOS 26.5.1 (25F80) |
| CPU 核心 | 18 |
| 内存 | 64 GB |
| Boot JDK | OpenJDK 25.0.2 (aarch64) |
| 工具链 | Xcode 26.5 / Apple clang 21.0.0 |
| autoconf | GNU Autoconf 2.73 |
| 目标版本 | JDK 25 (25-internal) |
说明:building.md 推荐 macOS 14 + Xcode 15.4,本机的 Xcode 26.5 远新于推荐版本, 因此触发了下面的编译器兼容问题。
二、debug 级别说明
--with-debug-level 可选四种级别:
release------ 默认,发布版,无调试代码。fastdebug------ 带断言和部分调试代码,性能接近 release。日常开发调试推荐。slowdebug------ 全部调试代码、无优化,最适合用调试器单步,但运行很慢。optimized------ release 的变体,额外带 HotSpot 调试代码。
--enable-debug 是 --with-debug-level=fastdebug 的简写。本次构建采用 fastdebug。
三、遇到的问题:Apple clang 21 新警告导致编译失败
首次构建在 HotSpot 阶段失败,报错如下:
css
src/hotspot/share/oops/resolvedFieldEntry.cpp:49:10: error: first argument in call
to 'memset' is a pointer to non-trivially copyable type 'ResolvedFieldEntry'
[-Werror,-Wnontrivial-memcall]
49 | memset(this, 0, sizeof(*this));
src/hotspot/share/oops/resolvedMethodEntry.cpp:43:12: error: ... 'ResolvedMethodEntry'
[-Werror,-Wnontrivial-memcall]
原因
Apple clang 21 (Xcode 26.5) 新增了 -Wnontrivial-memcall 警告:对非平凡可拷贝类型 调用 memset 会告警。而 JDK 构建默认开启 -Werror(警告即错误),JDK 25 源码尚未 适配这个新警告,导致编译中断。这属于「源码 + 过新工具链」的兼容问题,并非配置错误。
解决方案
通过 --with-extra-cxxflags 向 C++ 编译追加标志,把该警告降级为非致命:
ini
--with-extra-cxxflags=-Wno-error=nontrivial-memcall
这样改动最小,不触碰 HotSpot 源码,仅豁免这一个新警告,其余警告仍保持 -Werror。
四、构建步骤
1. 运行 configure
bash
cd /Users/zsh/Projects/JavaProjects/jdk
bash configure \
--with-debug-level=fastdebug \
--with-extra-cxxflags=-Wno-error=nontrivial-memcall
configure 会自动探测 boot JDK、Xcode 工具链等。输出目录为 build/macosx-aarch64-server-fastdebug。配置摘要确认:
- Debug level: fastdebug
- JVM variants: server
- OpenJDK target: macosx / aarch64 / 64-bit
- macOS code signing: debug (自动,用于支持 core dump)
2. 清理旧产物
因警告标志变更,configure 提示需要 clean。本仓库存在多个配置 (fastdebug 与 release),必须用 CONF= 指定:
bash
make CONF=fastdebug clean
3. 编译
构建 exploded image(开发用最小集,产物在 build/.../jdk):
bash
make CONF=fastdebug jdk
如需完整发布镜像(产物在 build/.../images/jdk),改用:
bash
make CONF=fastdebug images
18 核并行,构建顺利完成(exit code 0)。
五、验证
bash
./build/macosx-aarch64-server-fastdebug/jdk/bin/java -version
输出:
csharp
openjdk version "25-internal" 2025-09-16
OpenJDK Runtime Environment (fastdebug build 25-internal-adhoc.zsh.jdk)
OpenJDK 64-Bit Server VM (fastdebug build 25-internal-adhoc.zsh.jdk, mixed mode)
fastdebug build 字样确认 debug 版构建成功。
六、常用命令速查
| 操作 | 命令 |
|---|---|
| 配置 fastdebug | bash configure --with-debug-level=fastdebug --with-extra-cxxflags=-Wno-error=nontrivial-memcall |
| 编译 exploded image | make CONF=fastdebug jdk |
| 编译完整镜像 | make CONF=fastdebug images |
| 只编译 hotspot | make CONF=fastdebug hotspot |
| 清理产物(保留配置) | make CONF=fastdebug clean |
| 清理全部(含配置) | make CONF=fastdebug dist-clean |
| 运行新 JDK | ./build/macosx-aarch64-server-fastdebug/jdk/bin/java -version |
| 跑 tier1 测试 | make CONF=fastdebug test-tier1 |
在 VSCode 中调试 JVM 源码 (macOS / Apple Silicon)
本文记录如何在 VSCode 里调试自编译的 fastdebug JDK 源码,支持 C++ (HotSpot/libjvm) 与 Java (java.base 类库) 的混合调试。
一、两个层次的区别
JVM 源码分两层,调试器与插件完全不同,务必先分清:
| 调试目标 | 属于 | 调试器 | VSCode 插件 |
|---|---|---|---|
LauncherHelper.java、String.java 等类库 |
Java 层 | JDWP | Extension Pack for Java (redhat.java + vscjava.*) |
libjvm.dylib、解释器/GC/JIT、main.c、java_md_macosx.m |
C++ 层 | LLDB | lldb-dap (llvm-vs-code-extensions.lldb-dap) |
注意:本机装的是 lldb-dap (LLVM 官方),不是 CodeLLDB。 两者 launch.json 的 type 不同:lldb-dap 用 "type": "lldb-dap",CodeLLDB 用 "type": "lldb"。
二、前置条件(均已满足)
- fastdebug 构建,带调试符号:
COMPILE_WITH_DEBUG_SYMBOLS := true。 - 存在独立调试符号包
libjvm.dylib.dSYM,UUID 与 libjvm 匹配 → C++ 断点可命中源码行。 java二进制带com.apple.security.get-task-allowentitlement(构建时 codesign=debug 自动添加) → LLDB 可以启动/附加该进程。- 已生成
build/macosx-aarch64-server-fastdebug/compile_commands.json→ C++ 跳转/补全可用。
三、配置文件
均位于 jdk/.vscode/:
launch.json------ 三个调试配置 + 一个混合复合配置。tasks.json------ 生成 compile_commands.json、增量重编 hotspot/jdk。c_cpp_properties.json------ C/C++ 插件指向 compile_commands.json。settings.json------ 排除子 Maven 项目 + 钉死默认 JDK 运行时。
launch.json (完整内容)
jsonc
{
// JVM 源码混合调试配置
// - C++ 层 (HotSpot/libjvm):lldb-dap 启动 java 进程,可在 .cpp 下断点
// - Java 层 (java.base):Java 调试器通过 JDWP attach,可在 .java 下断点
// 重要:调 Java 类库必须用 attach 接「命令行启动的自编译 java」,不能用 launch,
// 否则 VSCode 会自挑 JDK(实测用成 JDK 17),导致类库源码行号对不上。
"version": "0.2.0",
"configurations": [
{
// 【C++ 调试】用 lldb-dap 直接启动自编译的 java,断点打在 libjvm 的 .cpp/.m 上
// 同时开启 JDWP(address=5005,suspend=y),供 Java 调试器后续 attach
"name": "LLDB: 启动 java (C++ + 开 JDWP)",
"type": "lldb-dap",
"request": "launch",
"program": "${workspaceFolder}/build/macosx-aarch64-server-fastdebug/jdk/bin/java",
"args": [
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005",
"${workspaceFolder}/../hello/app/src/main/java/HelloWorld.java"
],
"cwd": "${workspaceFolder}",
"stopOnEntry": false,
"initCommands": [
// 加载 libjvm 的独立调试符号(dSYM),确保断点能映射到源码
"settings set target.load-script-from-symbol-file true"
],
"stopCommands": [
// 关键:HotSpot 在 aarch64 上把多个信号当正常机制使用,lldb 默认会拦截误报。
// - SIGILL:运行期生成的 stub 里埋了调试陷阱指令 `dcps1 #0xdead`,
// 仅当有调试器附加时才触发,交还控制权用,无害(本例实际撞的就是它)。
// - SIGSEGV/SIGBUS:空指针检查(implicit null check)、safepoint 轮询、
// 栈溢出 guard page 等常态机制。
// 全部放行(pass=true、不停、不提示),交还给 JVM 自己的信号处理器。
"process handle SIGILL --pass true --stop false --notify false",
"process handle SIGSEGV --pass true --stop false --notify false",
"process handle SIGBUS --pass true --stop false --notify false"
]
},
{
// 【Java 调试】attach 到上面 LLDB 启动并开了 JDWP 的 java 进程
// 用于在 LauncherHelper.checkAndLoadMain、String 等 java.base 源码下断点
"name": "Java: Attach 到 5005 (Java 层)",
"type": "java",
"request": "attach",
"hostName": "localhost",
"port": 5005,
"projectName": "jdk",
// 让 Java 调试器用 JDK 源码目录解析类库源码
"sourcePaths": [
"${workspaceFolder}/src/java.base/share/classes",
"${workspaceFolder}/src/java.base/macosx/classes"
]
}
// 注意:不要用 "request": "launch" 的 Java 配置来调试 JDK 类库!
// 那样 VSCode Java 扩展会自挑默认 JDK(实测会用 Gradle 缓存里的 JDK 17),
// 跑的不是自编译的 JDK 25,导致 LauncherHelper 等类库行号完全对不上。
// 调 Java 类库只走上面的 attach:先用命令行的自编译 java 起进程(suspend=y),
// 再 attach。被调试的 JVM 由命令行决定,VSCode 无法选错版本。
// build/macosx-aarch64-server-fastdebug/jdk/bin/java \
// "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005" \
// -cp <hello>/app/build/classes/java/main HelloWorld
],
"compounds": [
{
// 【混合调试,推荐】一键先起 LLDB(suspend=y 会停在 JVM 启动处等待),
// 再 attach Java 调试器。两个调试会话同时存在,C++ 与 Java 断点都生效。
"name": "混合调试: C++ + Java",
"configurations": [
"LLDB: 启动 java (C++ + 开 JDWP)",
"Java: Attach 到 5005 (Java 层)"
],
"stopAll": true
}
]
}
launch.json 关键点
- LLDB: 启动 java (C++ + 开 JDWP) :用 lldb-dap 启动自编译的
java, 参数里带-agentlib:jdwp=...,suspend=y,address=*:5005, 进程会停在 JVM 启动早期、监听 5005 等待 Java 调试器接入。stopCommands放行 SIGILL/SIGSEGV/SIGBUS,避免 lldb 把 HotSpot 的正常控制流信号误报为崩溃。 - Java: Attach 到 5005 (Java 层) :Java 调试器 attach 到 5005, 并通过
sourcePaths指向src/java.base/share/classes等解析类库源码。 - 混合调试: C++ + Java(compound,推荐):一键先起 LLDB 再 attach Java, 两个会话并存,C++ 与 Java 断点同时生效。
tasks.json (完整内容)
jsonc
{
"version": "2.0.0",
"tasks": [
{
// 生成 compile_commands.json,供 C/C++ 插件做 libjvm 源码的跳转与补全
// 产物在 build/macosx-aarch64-server-fastdebug/compile_commands.json
"label": "生成 compile_commands.json",
"type": "shell",
"command": "make CONF=fastdebug compile-commands",
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"group": "build"
},
{
// 增量重新编译 hotspot(改了 C++ 源码后用)
"label": "重新编译 hotspot (fastdebug)",
"type": "shell",
"command": "make CONF=fastdebug hotspot",
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"group": "build"
},
{
// 增量重新编译整个 exploded image(改了 Java 类库后用)
"label": "重新编译 jdk (fastdebug)",
"type": "shell",
"command": "make CONF=fastdebug jdk",
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [],
"group": "build"
}
]
}
c_cpp_properties.json (完整内容)
jsonc
{
"configurations": [
{
"name": "Mac",
// 指向 JDK 构建生成的 compile_commands.json,实现 libjvm 源码的精确跳转/补全
"compileCommands": "${workspaceFolder}/build/macosx-aarch64-server-fastdebug/compile_commands.json",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "macos-clang-arm64"
}
],
"version": 4
}
settings.json (完整内容)
jsonc
{
// 排除 jdk 仓库里的子 Maven 项目(IGV、LogCompilation),避免 redhat.java 自动导入它们。
// 这些项目的 pom 要求 Java 17(IGV enforcer 限定 [17,22)),被导入后会让 Java 扩展
// 注册 JDK 17 运行时,进而导致调试时误用 JDK 17、类库源码行号对不上。
"java.import.exclusions": [
"**/node_modules/**",
"**/.metadata/**",
"**/archetype-resources/**",
"**/META-INF/maven/**",
"**/src/utils/**"
],
// 显式登记运行时,把自编译 JDK 25 设为默认,杜绝回退到 JDK 17。
// 路径指向用作 boot JDK 的 JDK 25(与构建所用一致)。
"java.configuration.runtimes": [
{
"name": "JavaSE-25",
"path": "/Users/zsh/.gradle/jdks/oracle_corporation-25-aarch64-os_x.2/jdk-25.0.2.jdk/Contents/Home",
"default": true
}
]
}
四、使用步骤
混合调试(C++ + Java)
- 在
.cpp/.m(如main.c、resolvedFieldEntry.cpp)按需打断点。 - 在
.java(如LauncherHelper.checkAndLoadMain)按需打断点。 - 运行与调试面板选择 「混合调试: C++ + Java」,F5 启动。
- 先命中 C++ 断点;继续运行后,Java 调试器会 attach,命中 Java 断点。
只调 C++
选 「LLDB: 启动 java (C++ + 开 JDWP)」 单独运行即可(可忽略 JDWP 部分)。
只调 Java
选 「Java: 直接调试 HelloWorld (仅 Java 层)」 。 注意此配置用项目默认 java;若要用自编译 JDK 跑,改用混合配置或在 settings.json 里把 java.configuration.runtimes 指向 build 产物。
五、调试入口
入口采用单文件源码模式(对应 LauncherHelper 的 LM_SOURCE 分支):
bash
java /Users/zsh/Projects/JavaProjects/hello/app/src/main/java/HelloWorld.java
六、改了源码后如何重编
| 改动 | 重编命令 / 任务 |
|---|---|
| 改了 HotSpot C++ 源码 | make CONF=fastdebug hotspot(任务:重新编译 hotspot) |
| 改了 java.base 等类库 | make CONF=fastdebug jdk(任务:重新编译 jdk) |
| 新增/删除源文件后刷新索引 | make CONF=fastdebug compile-commands |
七、命令行验证(不依赖 VSCode GUI)
VSCode 里按 F5 走「混合调试」的等价底层操作,可用 lldb / jdb 在命令行复刻, 用于在配置 GUI 之前快速确认两层断点都能命中。以下命令均已实测通过。
C++ 层:lldb 在 main.c 打断点
bash
cd /Users/zsh/Projects/JavaProjects/jdk
cat > /tmp/lldb_cpp_test.txt <<'EOF'
breakpoint set --file main.c --line 55
run
thread backtrace
frame variable argc
expression -- (char*)argv[3]
continue
quit
EOF
lldb -b -s /tmp/lldb_cpp_test.txt -- \
./build/macosx-aarch64-server-fastdebug/jdk/bin/java \
/Users/zsh/Projects/JavaProjects/hello/app/src/main/java/HelloWorld.java
实测要点:
- 断点命中
main(argc=2, argv=...) at main.c,frame variable argc可读 → dSYM 符号生效。 - 因 fastdebug 仍带
[opt]优化,断点会落到最近的有效语句行(如 55 → 67),属正常现象。 - 断点命中两次 :第一次在
thread #1 (main-thread),continue后第二次落在thread #2, 实证 macOS 启动器在新线程重跑 main(参见 runMain.md)。
Java 层:jdb attach 到 checkAndLoadMain
bash
cd /Users/zsh/Projects/JavaProjects/jdk
# 1. 后台启动带 JDWP 的 java(suspend=y 等待 attach)
./build/macosx-aarch64-server-fastdebug/jdk/bin/java \
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005" \
/Users/zsh/Projects/JavaProjects/hello/app/src/main/java/HelloWorld.java &
# 2. attach,设断点后保持输入流开启,留时间让断点命中
{
echo "stop in sun.launcher.LauncherHelper.checkAndLoadMain"; sleep 2
echo "run"; sleep 6
echo "where"; sleep 2
echo "locals"; sleep 2
echo "cont"; sleep 2
echo "quit"
} | jdb -attach localhost:5005 -sourcepath src/java.base/share/classes
实测输出(命中并打印参数):
ini
断点命中: "线程=main", sun.launcher.LauncherHelper.checkAndLoadMain(), 行=737
方法参数:
printToStderr = true
mode = 4
what = "jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher"
注:mode = 4(LM_SOURCE)+ what = ...SourceLauncher 揭示了单文件源码模式的机制------ java HelloWorld.java 先加载 SourceLauncher,由它在内存中编译并运行源码。
常见坑
- jdb 用管道喂命令时,命令喂完输入流一关 jdb 就 detach。需在
run后加sleep留出时间让断点命中,否则会出现「应用程序已退出 / 输入流已关闭」而看不到命中。
八、C++ 调试时遇到"段错误 / SIGILL"------不是真崩溃
现象
在 libjvm 的 C++ 代码(典型如 javaCalls.cpp 的 StubRoutines::call_stub(), 即 JVM 从 C++ 跳进 Java 字节码的入口)附近调试时,lldb 会停下并报段错误 / 信号, 但同一条命令不带调试器裸跑却一切正常。
根因
HotSpot 在 aarch64 上把多个信号当作正常控制流机制使用,lldb 默认会拦截这些信号、 误报为崩溃:
- SIGILL :运行期生成的 stub 里埋了 aarch64 调试陷阱指令
dcps1 #0xdead(0xdead是 HotSpot 约定的魔数)。这条指令仅当有调试器附加时才被执行到, 用于把控制权交给调试器。这正是为什么裸跑没事、一 attach 就"崩"。它不是 assert 失败 (assert/__ stop()失败会带源码位置和错误消息)。 - SIGSEGV / SIGBUS:空指针检查(implicit null check,不判空而是靠捕获信号抛 NPE)、 safepoint 轮询(GC 停线程靠把特殊页设为不可读)、栈溢出 guard page 等常态机制。
证据:signals_posix.cpp 启动时 set_signal_handler(SIGSEGV/SIGBUS/SIGILL), JVM 有自己的处理器消化这些信号。
排查过程(实测)
- lldb 启动 java,
continue后停在thread #3, stop reason = signal SIGILL。 disassemble --pc看信号点指令 →dcps1 #0xdead,确认是调试陷阱而非真异常。- 放行该信号后 java 完整跑完输出
[zsh, Tom]、Process exited with status = 0。
手动复现与确认指令:
bash
cat > /tmp/lldb_diag.txt <<'EOF'
process launch --stop-at-entry
process handle SIGILL --pass true --stop false --notify false
process handle SIGSEGV --pass true --stop false --notify false
process handle SIGBUS --pass true --stop false --notify false
continue
quit
EOF
lldb -b -s /tmp/lldb_diag.txt -- \
./build/macosx-aarch64-server-fastdebug/jdk/bin/java \
/Users/zsh/Projects/JavaProjects/hello/app/src/main/java/HelloWorld.java
# 若想看 SIGILL 现场,把 continue 换成 continue 后接 disassemble --pc
解决
launch.json 的 C++ 配置已加入 stopCommands,让 lldb 放行这三个信号 (pass=true、不停、不提示),交还给 JVM 自己的处理器:
jsonc
"stopCommands": [
"process handle SIGILL --pass true --stop false --notify false",
"process handle SIGSEGV --pass true --stop false --notify false",
"process handle SIGBUS --pass true --stop false --notify false"
]
配好后 VSCode 里 F5 调 C++ 不会再被这些假信号打断。
九、Java 类库断点"行号对不上"------其实是调成了别的 JDK
现象
在 LauncherHelper.java 等 java.base 类库源码打断点,命中位置却跟源码对不上 (例如在自编译源码第 736 行打点,实际却停在另一处、甚至跳进 JDK 17 的同名文件)。
根因
调试器 attach/launch 到的 JVM 不是自编译的 JDK 25,而是别的版本的 JDK (实测是 Gradle 缓存里的 JDK 17.0.2)。JDK 17 与 25 的 LauncherHelper.java 相差多个大版本,同一行号是完全不同的代码,于是"对不上"。
来源是一个 "request": "launch" 的 Java 调试配置 :它不指定 runtime, VSCode 的 Java 扩展会自挑它认定的默认 JDK(挑中了 JDK 17), 启动的进程根本不经过 build/.../jdk/bin/java。
排查时已确认:自编译源码、编译出的 class、class 的 LineNumberTable 三者行号完全一致, 所以问题不在编译产物,而在"跑错了 JVM"。
解决:调 Java 类库只用 attach,绝不用 launch
被调试的 JVM 必须由命令行决定,而不是让 VSCode 去挑:
-
命令行用自编译 java 起进程(suspend=y 挂起等待):
bashbuild/macosx-aarch64-server-fastdebug/jdk/bin/java \ "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005" \ -cp <hello>/app/build/classes/java/main HelloWorld -
VSCode 选 「Java: Attach 到 5005 (Java 层)」,F5 接上去。
-
命中断点后,在 Debug Console 验证确实是 JDK 25:
arduinoSystem.getProperty("java.version") // 应返回 25-internal,而非 17.0.2
launch.json 中已删除 request: launch 的 Java 配置,只保留 attach,从根上杜绝选错版本。
附带检查:源码是否与 class 同步
若确认 JVM 版本无误但行号仍偏移,再排查源码/class 是否同步:
bash
# class 比源码旧 = 改了源码没重编,需 make CONF=fastdebug jdk
find build/macosx-aarch64-server-fastdebug -name LauncherHelper.class -exec stat -f "%Sm %N" {} \;
# 看 class 真实行号表,与源码逐行核对
build/macosx-aarch64-server-fastdebug/jdk/bin/javap -p -l \
-classpath build/macosx-aarch64-server-fastdebug/jdk/modules/java.base \
sun.launcher.LauncherHelper
为什么 VSCode 会冒出一个 JDK 17
根源在 jdk 仓库内部带有要求 Java 17 的子项目:
src/utils/IdealGraphVisualizer/(IGV,HotSpot 编译图可视化工具)是 Maven 项目, 下有 18 个 pom.xml,全部声明<release>17,父 pom 还用 enforcer 限定[17,22)("IGV requires a JDK version between 17 and 21")。src/utils/LogCompilation/也是 Maven 项目。
在 jdk 根目录打开 VSCode 时,redhat.java 扩展会自动扫描并导入这些子项目, 为满足 IGV 的 Java 17 要求,jdt.ls 会注册 Gradle 缓存里的 JDK 17.0.2 作为运行时 (可在工作区缓存 .../redhat.java/jdt_ws/.metadata/.plugins/org.eclipse.core.runtime/ .settings/org.eclipse.jdt.launching.prefs 看到登记了 jdk-17/21/25 三个运行时)。 这就是 launch 式调试会跑成 JDK 17 的源头。
解决:排除子项目 + 钉死默认运行时
已在 .vscode/settings.json 配置(完整内容见三、配置文件),关键项:
java.import.exclusions排除**/src/utils/**,阻止 redhat.java 导入 IGV / LogCompilation 子项目。java.configuration.runtimes把 JDK 25 设为"default": true,杜绝回退到 JDK 17。
改后需重启 VSCode 或执行命令面板的「Java: Clean Java Language Server Workspace」 让 jdt.ls 重新索引。
十、注意事项
- 命令行手动起 java 时,
address=*:5005的*要加引号 (zsh 会把*当通配符):java "-agentlib:jdwp=...,address=*:5005" ...。 VSCode launch.json 的 args 数组逐元素直传、不经过 shell,无需加引号。 - 验证 JDWP 是否就绪:启动后应打印
Listening for transport dt_socket at address: 5005, 且lsof -nP -iTCP:5005 -sTCP:LISTEN能看到 java 在监听。 - 若 C++ 断点变灰打不上,确认
libjvm.dylib.dSYM存在且与当前 libjvm 同一次构建产物。
java Hello.java 从 JVM 源码看完整执行流程
整个流程分为三个阶段:启动器(C 代码)→ JVM 初始化(HotSpot C++)→ 源码编译执行(Java 层)。
第一阶段:启动器(Launcher)
1. main() 入口
src/java.base/share/native/launcher/main.c:103
c
int main(int argc, char **argv) {
// 处理 @参数文件、JDK_JAVA_OPTIONS 环境变量
JLI_InitArgProcessing(...);
// 最终调用
JLI_Launch(args);
}
2. JLI_Launch() --- 总调度
src/java.base/share/native/libjli/java.c:226
按顺序做四件事:
CreateExecutionEnvironment()--- 找到 JDK 根目录、libjvm.so路径LoadJavaVM()---dlopen加载libjvm.so,用dlsym解析出三个函数指针:JNI_CreateJavaVM、JNI_GetDefaultJavaVMInitArgs、JNI_GetCreatedJavaVMsParseArguments()--- 解析命令行参数JVMInit()→ 最终调JavaMain()
3. ParseArguments() --- 识别 .java 文件
src/java.base/share/native/libjli/java.c:1163
关键逻辑:
c
// 循环消费所有以 "-" 开头的参数后:
if (mode == LM_UNKNOWN) {
// 没指定过 -jar、-m、--source,判断参数是不是 .java 文件
mode = IsSourceFile(arg) ? LM_SOURCE : LM_CLASS;
}
IsSourceFile() (java.c:766)做两件事:
c
static jboolean IsSourceFile(const char *arg) {
struct stat st;
return (JLI_HasSuffix(arg, ".java") && stat(arg, &st) == 0);
}
即:参数以 .java 结尾 且 文件确实存在于磁盘上。
确认为 LM_SOURCE 模式后,启动器把 "主类" 替换为:
c
#define SOURCE_LAUNCHER_MAIN_ENTRY \
"jdk.compiler/com.sun.tools.javac.launcher.SourceLauncher"
同时注入:
-Dsun.java.launcher.mode=source--add-modules=ALL-DEFAULT
4. JavaMain() --- 启动 JVM 并调用主类
src/java.base/share/native/libjli/java.c:468
c
int JavaMain(void *_args) {
InitializeJVM(&vm, &env, &ifn); // ① 启动 JVM
LoadMainClass(env, mode, what); // ② 加载主类
// ③ 调用 main 方法
invokeStaticMainWithArgs(env, mainClass, mainArgs);
}
InitializeJVM()(java.c:1471)调用:
c
ifn->CreateJavaVM(pvm, (void **)penv, &args);
这就是跨越进入 HotSpot 的边界。
第二阶段:JVM 初始化(HotSpot C++)
5. JNI_CreateJavaVM() → Threads::create_vm()
src/hotspot/share/prims/jni.cpp:3583 → src/hotspot/share/runtime/threads.cpp:447
Threads::create_vm() 是 JVM 初始化的核心,按顺序执行:
| 步骤 | 函数 | 作用 |
|---|---|---|
| 1 | os::init() |
OS 层初始化 |
| 2 | Arguments::parse() |
解析 VM 参数 |
| 3 | vm_init_globals() |
早期全局初始化(互斥锁等) |
| 4 | init_globals() → universe_init() |
创建堆、元空间、加载 Object/Class/String 等核心类 |
| 5 | initialize_java_lang_classes() |
初始化 String、System、Class、Thread、ThreadGroup 等 |
| 6 | call_initPhase1() |
调用 java.lang.System.initPhase1() |
| 7 | call_initPhase2() |
调用 System.initPhase2(),初始化模块系统 |
| 8 | call_initPhase3() |
调用 System.initPhase3(),设置系统类加载器 |
| 9 | 创建 VMThread、Compiler、Signal Dispatcher 等 |
6. LoadMainClass() --- 加载主类
回到 JavaMain() 后,LoadMainClass()(java.c:1574)调用 Java 层:
c
LauncherHelper.checkAndLoadMain(useStderr, LM_SOURCE, what);
对于 LM_SOURCE 模式,LauncherHelper(sun/launcher/LauncherHelper.java)走模块加载路径,从 boot layer 中解析出 jdk.compiler 模块的 com.sun.tools.javac.launcher.SourceLauncher。
然后 JavaMain 调用 SourceLauncher.main(args)。
第三阶段:编译 + 执行(Java 层)
7. SourceLauncher.run() --- 编译
src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/SourceLauncher.java:130
java
public Result run(String[] runtimeArgs, String[] args) {
Path file = getFile(args); // Hello.java 的 Path
ProgramDescriptor program = ProgramDescriptor.of(
ProgramFileObject.of(file)); // 解析源码
RelevantJavacOptions options = RelevantJavacOptions.of(
program, runtimeArgs); // 提取 javac 选项
MemoryContext context = new MemoryContext(out, program, options);
context.compileProgram(); // ★ 编译
context.execute(...); // ★ 执行
}
编译过程 (MemoryContext.compileProgram()):
MemoryFileManager拦截编译输出------.class字节码不写磁盘,而是存入Map<String, byte[]> inMemoryClassesJavacTool.getTask(...)创建编译任务task.call()执行编译- 注入的隐式选项:
-proc:none、-implicit:none、-Xprefer:source等
关键文件:
ProgramFileObject.java--- 读源码,处理 shebang(#!行)ProgramDescriptor.java--- 用 javac 解析出包名、类型名MemoryFileManager.java--- 拦截CLASS_OUTPUT,字节码写进Map<String, byte[]>MemoryContext.java--- 组装编译器并执行
8. SourceLauncher.execute() --- 执行
SourceLauncher.java:178
-
创建类加载器 :
MemoryClassLoader以系统类加载器为父加载器,持有inMemoryClasses,优先从内存中加载类 -
找主类 :先找文件中第一个类,如果没有
main则找与文件名同名的类(Hello.java→Hello) -
找 main 方法:支持 static 和 instance 两种形式
-
反射调用 :
javamainMethod.invoke(receiver, (Object) mainArgs); -
如果
main抛异常,截断调用栈,去掉 SourceLauncher 的栈帧
完整调用链总结
css
shell: java Hello.java
│
├─ main.c:103 main()
└─ java.c:226 JLI_Launch()
├─ java_md.c CreateExecutionEnvironment() 找 JDK 根目录
├─ java_md.c LoadJavaVM() dlopen libjvm.so
├─ java.c:1163 ParseArguments()
│ └─ IsSourceFile("Hello.java") → LM_SOURCE
│ └─ *pwhat = "jdk.compiler/...SourceLauncher"
│
├─ java.c:1471 InitializeJVM()
│ └─ jni.cpp:3583 JNI_CreateJavaVM_inner()
│ └─ threads.cpp:447 Threads::create_vm()
│ ├─ init.cpp:120 init_globals()
│ │ └─ universe.cpp:868 universe_init() 创建堆、加载核心类
│ ├─ threads.cpp:342 initialize_java_lang_classes()
│ │ └─ String → System → Class → ThreadGroup → Thread
│ ├─ threads.cpp:293 call_initPhase1()
│ ├─ threads.cpp:311 call_initPhase2() 模块系统
│ └─ threads.cpp:335 call_initPhase3() 系统类加载器
│
└─ java.c:468 JavaMain()
├─ java.c:1574 LoadMainClass()
│ └─ LauncherHelper.checkAndLoadMain(LM_SOURCE, ...)
│ └─ 加载 com.sun.tools.javac.launcher.SourceLauncher
│
└─ 调用 SourceLauncher.main(args)
└─ SourceLauncher.java:130 run()
├─ ProgramDescriptor 解析源码(包名、类名)
├─ MemoryContext 组装编译器(MemoryFileManager 拦截输出)
├─ compileProgram() javac 编译 → 字节码存入内存 Map
└─ execute()
├─ MemoryClassLoader 自定义类加载器
├─ 找 main 方法(static/instance)
└─ mainMethod.invoke() ← Hello.main() 开始执行!
一句话概括
启动器检测到 .java 后缀后,不走普通类加载路径,而是先启动 JVM,然后委托 SourceLauncher 在内存中编译源文件,最后用自定义类加载器加载字节码并反射调用 main 方法。
关键源文件索引
| 文件 | 作用 |
|---|---|
src/java.base/share/native/launcher/main.c |
main() 入口 |
src/java.base/share/native/libjli/java.c |
JLI_Launch()、ParseArguments()、JavaMain()、IsSourceFile() |
src/java.base/unix/native/libjli/java_md.c |
Unix 平台:LoadJavaVM()、CreateExecutionEnvironment() |
src/hotspot/share/prims/jni.cpp |
JNI_CreateJavaVM()、JNI_CreateJavaVM_inner() |
src/hotspot/share/runtime/threads.cpp |
Threads::create_vm()、initialize_java_lang_classes() |
src/hotspot/share/runtime/init.cpp |
init_globals() |
src/hotspot/share/memory/universe.cpp |
universe_init() |
src/hotspot/share/classfile/systemDictionary.cpp |
SystemDictionary::initialize() 引导核心类 |
src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/SourceLauncher.java |
源码模式入口,编译+执行 |
src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryContext.java |
组装编译器,compileProgram() |
src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryFileManager.java |
拦截编译输出到内存 |
src/jdk.compiler/share/classes/com/sun/tools/javac/launcher/MemoryClassLoader.java |
从内存字节码加载类 |
invokestatic 指令实现全览
指令操作码:0xB8 (184) ,定义在 src/hotspot/share/interpreter/bytecodes.hpp:227
一、模板解释器(Template Interpreter)------ 实际执行逻辑
这是解释器中 invokestatic 真正"跑"的地方。
x86_64 --- src/hotspot/cpu/x86/templateTable_x86.cpp:3314-3328:
cpp
void TemplateTable::invokestatic(int byte_no) {
transition(vtos, vtos);
load_resolved_method_entry_special_or_static(rcx, rbx, rdx); // ①
prepare_invoke(rcx, rcx, rdx); // ②
__ profile_call(rax);
__ profile_arguments_type(rax, rbx, rbcp, false);
__ jump_from_interpreted(rbx, rax); // ③
}
三步走:
load_resolved_method_entry_special_or_static(templateTable_x86.cpp:2381)--- 从常量池缓存中解析/加载ResolvedMethodEntry(包含目标Method*和 flags),若未解析则调用InterpreterRuntime::resolve_from_cacheprepare_invoke(templateTable_x86.cpp:3193)--- 关键:跳过 receiver 加载 (invokestatic无 receiver),只保存返回地址、设置 TOS 状态jump_from_interpreted--- 跳转到目标方法的解释入口开始执行
AArch64 同理,在 src/hotspot/cpu/aarch64/templateTable_aarch64.cpp:3413-3427。
补充:模板解释器的工作原理
以上 invokestatic 的 TemplateTable::invokestatic() 是一个生成器函数 ------它在 JVM 启动时被调用,向 InterpreterMacroAssembler 写入 x86 机器码。生成完成后,CPU 在运行时直接执行这些机器码,没有 C++ 代码介入。下面以 iload 为例详细拆解整个过程。
1. 注册阶段:字节码 → 生成器函数的绑定
JVM 启动时,TemplateTable::initialize()(templateTable.cpp:218)通过 def() 为每条字节码注册一个 Template 对象:
cpp
// templateTable.cpp:255
def(Bytecodes::_iload, ubcp|____|clvm|____, vtos, itos, iload, _);
def() 函数(templateTable.cpp:182-195)将信息存入 _template_table 数组:
| 字段 | 含义 | 本例值 |
|---|---|---|
_gen |
生成器函数指针 | TemplateTable::iload |
_tos_in |
执行前栈顶状态 | vtos(不关心栈顶) |
_tos_out |
执行后栈顶状态 | itos(int 在 rax 寄存器中) |
_flags |
模板属性 | `uses_bcp |
2. 生成阶段:C++ 函数调用 → x86 机器码写入
TemplateInterpreterGenerator::generate_all() 遍历所有 Template,逐个调用 Template::generate():
cpp
// templateTable.cpp:56
void Template::generate(InterpreterMacroAssembler* masm) {
TemplateTable::_desc = this; // 设置当前模板上下文
TemplateTable::_masm = masm; // 设置当前汇编器
_gen(_arg); // 调用生成器函数,写入机器码
masm->flush(); // 将机器码提交到 CodeBuffer
}
_gen(_arg) 展开为 TemplateTable::iload(),它转发到 iload_internal(may_rewrite):
cpp
// templateTable_x86.cpp:576
void TemplateTable::iload() {
iload_internal(); // 默认 rc = may_rewrite
}
iload_internal 中的 __ 宏是核心:
cpp
// templateTable_x86.cpp:51
#define __ Disassembler::hook<InterpreterMacroAssembler>(__FILE__, __LINE__, _masm)->
当代码写 __ movl(rax, iaddress(rbx)) 时,预处理器展开为:
ruby
Disassembler::hook<InterpreterMacroAssembler>(__FILE__, __LINE__, _masm)->movl(rax, iaddress(rbx))
注意宏末尾的 ->,它让整个宏如同一个"前缀",右边的方法调用通过指针成员访问符链上去。
Disassembler::hook 的实现 (disassembler.hpp:114):
cpp
template<class T> inline static T* hook(const char* file, int line, T* masm) {
if (PrintInterpreter) {
_hook(file, line, masm); // 在当前机器码写入位置关联源码行号
}
return masm; // 原样返回 masm 指针
}
_hook 内部(disassembler.cpp:946)调用 decode_env::hook(),在 CodeBuffer 当前游标位置打一个标记:"此后生成的机器码来自源文件 file 的第 line 行"。
__ 宏等价于两步合成一步:
ini
① Disassembler::hook("templateTable_x86.cpp", 620, _masm)
│ PrintInterpreter=true → 在机器码位置打上行号标记(调试用)
│ PrintInterpreter=false → if 分支被优化掉,无开销
│ 返回 _masm
└→ ② _masm->movl(rax, iaddress(rbx)) ← 正常生成机器码
PrintInterpreter=true(调试构建):之后用-XX:+PrintInterpreter反汇编解释器时,每条汇编指令旁会标注对应的 C++ 源码行号,方便对照。PrintInterpreter=false(正常 fastdebug/release 构建):if分支被编译器优化掉,只剩return masm,__就是透明的_masm->简写,零开销。
最终调用链为 InterpreterMacroAssembler → MacroAssembler → Assembler,由 Assembler::emit_* 方法将 x86 指令编码为原始字节写入 CodeBuffer。
生成阶段产出的是一段连续的 x86 机器码 ,存储在解释器的 StubQueue 中(每条字节码对应一段 codelet)。
3. iload_internal 逐块详解
cpp
void TemplateTable::iload_internal(RewriteControl rc) {
transition(vtos, itos); // ① 编译期断言:栈状态转化
transition(vtos, itos) 是编译期/调试期检查,声明此模板将栈顶状态从"无有效值"变为"int 在 rax 中"。
cpp
if (RewriteFrequentPairs && rc == may_rewrite) // ② 条件:允许字节码重写
字节码 quickening (重写优化):当 RewriteFrequentPairs 标志开启且允许重写时,当前 iload 会根据下一条字节码的操作码,将自身替换为更快的变体。
less
__ load_unsigned_byte(rbx, at_bcp(Bytecodes::length_for(Bytecodes::_iload)));
// → movzbl rbx, [r13+1] 读取下一条字节码的操作码
__ cmpl(rbx, Bytecodes::_iload);
__ jcc(Assembler::equal, done);
// → cmp rbx, 0x15 _iload 的操作码值
// je done 如果是 _iload,不重写(等待形成 pair)
关键设计 :如果下一条也是 iload,跳过重写 。只有当第二次执行时(下一条已变为 _fast_iload),才触发 _iload → _fast_iload2 的合并。这样保证只有连续的 两个 iload 才会合并为 pair。
cpp
__ cmpl(rbx, Bytecodes::_fast_iload);
__ movl(bc, Bytecodes::_fast_iload2);
__ jccb(Assembler::equal, rewrite);
// → cmp rbx, _fast_iload
// mov bc, _fast_iload2 准备重写为一次加载两个 int 的超级指令
__ cmpl(rbx, Bytecodes::_caload);
__ movl(bc, Bytecodes::_fast_icaload);
__ jccb(Assembler::equal, rewrite);
// → 检测 iload + caload 组合,重写为 fast_icaload
__ movl(bc, Bytecodes::_fast_iload);
// → 兜底:至少升级为 _fast_iload,下次不再执行检测逻辑
重写执行流:
scss
__ bind(rewrite);
patch_bytecode(Bytecodes::_iload, bc, rbx, false);
// → 在运行时将当前 _iload 替换为 bc(如 _fast_iload2)
patch_bytecode(templateTable_x86.cpp:163)生成的机器码直接在字节码流中修改操作码,这是 JVM 的字节码自修改(bytecode rewriting)机制。
cpp
// ③ 实际功能:从局部变量加载 int
locals_index(rbx);
// → movzbl rbx, [r13+0] 读取操作数(局部变量索引)
// neg rbx 取反(局部变量向低地址增长)
__ movl(rax, iaddress(rbx));
// → mov eax, [r14 + rbx*8] 从局部变量区加载 int 到 rax
}
r13 是字节码指针(rbcp),r14 是局部变量基址(rlocals)。iaddress(rbx) 计算地址 [r14 + rbx * 8]。
4. 执行阶段:机器码如何被 CPU 执行
生成的机器码存储在 StubQueue 中。运行时通过分发表 (DispatchTable,templateInterpreter.hpp:131)跳转:
ini
字节码执行循环(每次循环执行一条字节码):
① dispatch_next:
movzbl rbx, [r13] ← 取当前字节码操作码
add r13, 1 ← bcp 前进 1
② dispatch_base:
testb [r15 + polling_offset], mask ← safepoint 轮询
lea rscratch1, [dispatch_table] ← 加载分发表基址
jmp [rscratch1 + rbx * 8] ← 以操作码为索引,间接跳转到对应 codelet
③ codelet 执行:
<iload_internal 生成的机器码>
movzbl rbx, [r13] ← 读取局部变量索引
neg rbx
mov eax, [r14 + rbx*8] ← 从局部变量加载 int
④ 回到 ①,取下一条字节码执行
分发表是一个长度为 256 的指针数组 (templateInterpreter.hpp:164-168),以字节码操作码为索引,每个槽指向对应 codelet 的入口地址。jmp [rscratch1 + rbx*8] 是一次间接跳转 ------CPU 从表中取出地址,直接跳到那里,不再经过任何 C++ 代码。
safepoint 轮询(interp_masm_x86.cpp:699)在每次分发时检查 GC 是否需要暂停线程。如果当前分发表与 safepoint 表不同且需要轮询,先检查再跳转。
5. 完整流程总结
css
编译期(make hotspot)
C++ 编译器将 templateTable_x86.cpp 编译为 .o
↓
JVM 启动时
TemplateTable::initialize() → 注册 256 个生成器函数
TemplateInterpreterGenerator::generate_all()
→ 逐个调用 Template::generate()
→ 生成器函数通过 __ 宏向 InterpreterMacroAssembler 写入机器码
→ flush() 提交到 StubQueue
↓
Java 方法解释执行
call_stub(汇编入口)→ dispatch_next 循环
↓
每条字节码
取操作码 → 查分发表 → jmp → 执行对应机器码 → 取下一条 → 查表 → jmp → ...
iload_internal 的 quickening 机制展示了模板解释器的精髓:生成的机器码在执行时能修改字节码自身,将慢速通用路径替换为快速专用路径,后续执行绕过检测逻辑,直接走快路径。
二、零级解释器(C++ 字节码解释器)
src/hotspot/share/interpreter/zero/bytecodeInterpreter.cpp:2364:
cpp
CASE(_invokestatic): {
u2 index = Bytes::get_native_u2(pc+1);
ResolvedMethodEntry* entry = cp->resolved_method_entry_at(index);
if (!entry->is_resolved((Bytecodes::Code)opcode)) {
CALL_VM(InterpreterRuntime::resolve_from_cache(THREAD, ...), handle_exception);
entry = cp->resolved_method_entry_at(index);
}
istate->set_msg(call_method); // 下沉到通用方法调用分发
}
纯 C++ 实现,逻辑相同。
三、方法解析(LinkResolver)------ 决定"到底调哪个方法"
src/hotspot/share/interpreter/linkResolver.cpp
调用链:
scss
resolve_invoke() (line 1713)
└─ resolve_invokestatic() (line 1754)
└─ resolve_static_call() (line 1109) ★ 核心
├─ linktime_resolve_static_method() (line 1142)
│ ├─ resolve_method() 类层次查找
│ └─ 检查 is_static() 否则 ICCE
├─ resolved_klass->initialize() 触发类初始化 ★
└─ result.set_static()
invokestatic 独有的行为 :必须触发目标类的 <clinit> 初始化。如果类未初始化,先初始化再重新解析。
四、运行时解析(InterpreterRuntime)
src/hotspot/share/interpreter/interpreterRuntime.cpp
scss
resolve_from_cache() → resolve_invoke() (line 778)
├─ LinkResolver::resolve_invoke() 解析出 Method*
└─ update_invoke_cp_cache_entry() 写入常量池缓存
└─ cache->set_direct_call() cpCache.cpp:235
└─ index = Method::nonvirtual_vtable_index (负数,标记直接调用)
五、C1 编译器实现
分发 --- src/hotspot/share/c1/c1_GraphBuilder.cpp:2934:
cpp
case Bytecodes::_invokestatic:
case Bytecodes::_invokeinterface: invoke(code); break;
LIR 生成 --- src/hotspot/share/c1/c1_LIRGenerator.cpp:2717:
cpp
case Bytecodes::_invokestatic:
__ call_static(target, result_register,
SharedRuntime::get_resolve_static_call_stub(),
arg_list, info);
break;
内联检查 --- c1_GraphBuilder.cpp:3865:
cpp
if (bc == Bytecodes::_invokestatic && !callee->holder()->is_initialized())
INLINE_BAILOUT("callee's klass not initialized yet");
类未初始化完成就不能内联。
六、C2 编译器实现
分发 --- src/hotspot/share/opto/parse2.cpp:2820:
cpp
case Bytecodes::_invokestatic:
case Bytecodes::_invokespecial:
case Bytecodes::_invokevirtual:
case Bytecodes::_invokeinterface:
do_call();
do_call() --- src/hotspot/share/opto/doCall.cpp:546:
is_virtual = false、has_receiver = false- 生成
CallGenerator::for_direct_call(callee)→DirectCallGenerator
DirectCallGenerator::generate() --- src/hotspot/share/opto/callGenerator.cpp:143:
cpp
address target = is_static ? SharedRuntime::get_resolve_static_call_stub()
: SharedRuntime::get_resolve_opt_virtual_call_stub();
CallStaticJavaNode* call = new CallStaticJavaNode(kit.C, tf(), target, method());
生成 CallStaticJavaNode 理想图节点,无 null check、无 receiver。
七、编译代码的解析桩(SharedRuntime)
src/hotspot/share/runtime/sharedRuntime.cpp
编译代码首次遇到未解析的 invokestatic 时,调用:
cpp
resolve_static_call_C() (line 1547)
└─ resolve_helper(false /*is_virtual*/, false /*is_optimized*/) (line 1332)
└─ 检查类初始化状态:
如果 !VM_Version::supports_fast_class_init_checks()
&& callee_method->needs_clinit_barrier()
→ 不修补调用点,每次调用都重新解析以强制类初始化检查
八、类初始化屏障
src/hotspot/share/oops/cpCache.cpp:176-189:
cpp
if (invoke_code == Bytecodes::_invokestatic) {
// 如果目标类尚未完成初始化,不标记为 resolved
// 强制下次调用重新解析,确保 <clinit> 已执行
}
这保证了 invokestatic 在类初始化完成前永不永久缓存。
总结文件索引
| 文件 | 角色 |
|---|---|
cpu/x86/templateTable_x86.cpp |
x86 模板解释器实现(3314、2381、3193) |
cpu/aarch64/templateTable_aarch64.cpp |
AArch64 模板解释器实现(3413) |
share/interpreter/zero/bytecodeInterpreter.cpp |
C++ 解释器(2364) |
share/interpreter/linkResolver.cpp |
方法解析(1754、1109、1142) |
share/interpreter/interpreterRuntime.cpp |
运行时缓存解析(778、985) |
share/oops/cpCache.cpp |
常量池缓存写入(235)、类初始化屏障(176) |
share/c1/c1_GraphBuilder.cpp |
C1 字节码处理(2934、1915) |
share/c1/c1_LIRGenerator.cpp |
C1 LIR 生成(2717) |
share/opto/parse2.cpp |
C2 字节码分发(2820) |
share/opto/doCall.cpp |
C2 call 生成(546) |
share/opto/callGenerator.cpp |
C2 DirectCallGenerator(143) |
share/runtime/sharedRuntime.cpp |
编译代码解析桩(143、1332、1547) |
附录:<clinit> 失败抛出 NoClassDefFoundError 的代码位置
核心文件:src/hotspot/share/oops/instanceKlass.cpp
类的状态机
src/hotspot/share/oops/instanceKlass.hpp:150:
lua
allocated → loaded → linked → being_initialized → fully_initialized
→ initialization_error ← 失败
关键判断(instanceKlass.hpp:531-539):
cpp
bool is_initialized() const { return init_state() == fully_initialized; }
bool is_in_error_state() const { return init_state() == initialization_error; }
核心入口:initialize_impl()
instanceKlass.cpp:1258,按 JVMS §2.16.5 实现。有三个关键分支:
❶ 类已在 initialization_error 状态(Step 5)--- 抛出 NoClassDefFoundError
instanceKlass.cpp:1317-1335:
cpp
// Step 5
if (is_in_error_state()) {
Handle cause(THREAD, get_initialization_error(THREAD)); // 取出之前保存的失败原因
stringStream ss;
ss.print("Could not initialize class %s", external_name());
if (cause.is_null()) {
THROW_MSG(vmSymbols::java_lang_NoClassDefFoundError(), // ★ 抛 NoClassDefFoundError
ss.as_string());
} else {
THROW_MSG_CAUSE(vmSymbols::java_lang_NoClassDefFoundError(), // ★ 带 cause
ss.as_string(), cause);
}
}
这是抛出 NoClassDefFoundError 的核心位置 。消息是 "Could not initialize class <类名>",原始异常作为 cause 链入。
❷ <clinit> 执行失败(Step 8-11)--- 第一个线程走到这里
instanceKlass.cpp:1386-1439:
cpp
// Step 8
call_class_initializer(THREAD); // 执行 <clinit>
// Step 9
if (!HAS_PENDING_EXCEPTION) {
set_initialization_state_and_notify(fully_initialized, CHECK); // 成功
} else {
// Step 10,11 --- 失败处理
Handle e(THREAD, PENDING_EXCEPTION);
CLEAR_PENDING_EXCEPTION;
add_initialization_error(THREAD, e); // ★ 保存异常
set_initialization_state_and_notify(initialization_error, THREAD); // ★ 标记错误状态
if (e->is_a(vmClasses::Error_klass())) {
THROW_OOP(e()); // Error 直接抛出
} else {
THROW_ARG(vmSymbols::java_lang_ExceptionInInitializerError(), // ★ 非 Error 包装为
...); // ExceptionInInitializerError
}
}
第一个线程看到的异常:
- 如果
<clinit>抛的是Error→ 直接抛出原Error - 如果
<clinit>抛的是非Error(如RuntimeException)→ 包装为ExceptionInInitializerError
注意:第一个线程不会 直接看到 NoClassDefFoundError。
❸ 父类初始化失败(Step 7)--- 同样标记错误状态
instanceKlass.cpp:1356-1382:
cpp
super_klass->initialize(THREAD);
if (HAS_PENDING_EXCEPTION) {
Handle e(THREAD, PENDING_EXCEPTION);
add_initialization_error(THREAD, e); // 保存异常
set_initialization_state_and_notify(initialization_error, THREAD);
THROW_OOP(e()); // 直接往上抛
}
保存异常:add_initialization_error()
instanceKlass.cpp:1185-1216 :将原始异常包装为 ExceptionInInitializerError 并存入全局哈希表 _initialization_error_table。
读取异常:get_initialization_error()
instanceKlass.cpp:1218-1225 :从哈希表中取出之前保存的 ExceptionInInitializerError。
状态通知:set_initialization_state_and_notify()
instanceKlass.cpp:1444-1457:原子写入状态 + 唤醒所有等待线程。
完整流程总结
scss
Thread A (第一个触发初始化的线程)
initialize_impl()
├─ call_class_initializer() 执行 <clinit>
├─ <clinit> 异常!
├─ add_initialization_error() 保存异常到全局表
├─ set_init_state(initialization_error) 标记为错误状态
└─ 抛给 Thread A: Error 或 ExceptionInInitializerError
Thread B (后续访问该类的线程)
initialize_impl()
├─ is_in_error_state() → true
├─ get_initialization_error() 取出保存的异常作为 cause
└─ ★ 抛出 NoClassDefFoundError("Could not initialize class XXX")
一句话:第一个线程遇到 <clinit> 异常时得到 ExceptionInInitializerError(或 Error);之后所有用到该类的线程,在 instanceKlass.cpp:1331 处得到 NoClassDefFoundError。
附录二:实战验证 ------ ClassNotFoundException → NoClassDefFoundError
错误现场
csharp
Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory
at HelloWorld.<clinit>(HelloWorld.java:6)
Caused by: java.lang.ClassNotFoundException: org.slf4j.LoggerFactory
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
第一段:ClassNotFoundException → NoClassDefFoundError
src/hotspot/share/classfile/systemDictionary.cpp:310-332
arduino
HelloWorld.<clinit> 中引用 org.slf4j.LoggerFactory
│
├─ SystemDictionary::resolve_or_fail(class_name, ..., throw_error=true, ...) (line 336)
│ └─ resolve_or_null()
│ └─ resolve_instance_class_or_null() (line 569)
│ └─ load_instance_class() (line 691)
│ └─ BuiltinClassLoader.loadClass("org.slf4j.LoggerFactory") (Java 层)
│ │
│ └─ 抛出 ClassNotFoundException ← 栈帧中可见
│
└─ handle_resolution_exception(class_name, throw_error=true, THREAD) (line 310)
│
├─ throw_error == true? YES
├─ PENDING_EXCEPTION is ClassNotFoundException? YES
├─ CLEAR_PENDING_EXCEPTION; (line 318)
└─ THROW_MSG_CAUSE(NoClassDefFoundError, (line 319)
"org/slf4j/LoggerFactory", original_cnfe);
│
└─ initCause(cnfe) ← 形成 Caused by 链
关键代码(handle_resolution_exception,systemDictionary.cpp:314-319):
cpp
if (throw_error && PENDING_EXCEPTION->is_a(vmClasses::ClassNotFoundException_klass())) {
ResourceMark rm(THREAD);
Handle e(THREAD, PENDING_EXCEPTION); // 保存 CNFE
CLEAR_PENDING_EXCEPTION; // 清除
THROW_MSG_CAUSE(vmSymbols::java_lang_NoClassDefFoundError(),
class_name->as_C_string(), e); // ★ 抛 NoClassDefFoundError,CNFE 作为 cause
}
其中 CHECK_NULL 宏(exceptions.hpp:237)使得类加载器抛出 ClassNotFoundException 后函数返回 nullptr,异常作为 pending exception 传播。
第二段:<clinit> 中的 Error 被直接重新抛出
回到 <clinit> 失败处理 --- instanceKlass.cpp:1431-1432:
cpp
if (e->is_a(vmClasses::Error_klass())) {
THROW_OOP(e()); // ★ Error 直接抛出,不包装为 ExceptionInInitializerError
}
NoClassDefFoundError 继承链:NoClassDefFoundError → LinkageError → Error。
所以 is_a(Error_klass()) 返回 true,直接重新抛出,不包装。
栈帧对照
| 栈帧 | 对应源码 | 说明 |
|---|---|---|
HelloWorld.<clinit>:6 |
instanceKlass.cpp:1432 |
<clinit> 中 Error 直接被 THROW_OOP |
NoClassDefFoundError: org/slf4j/LoggerFactory |
systemDictionary.cpp:319 |
handle_resolution_exception 将 CNFE 转为 NoClassDefFoundError |
Caused by: ClassNotFoundException |
systemDictionary.cpp:318 之前 |
类加载器抛出的原始异常,被 CLEAR + chain 为 cause |
总结
NoClassDefFoundError 是 Error 的子类,在 <clinit> 失败处理中走 THROW_OOP 路径直接抛出(不包装为 ExceptionInInitializerError);其根因 ClassNotFoundException 在 handle_resolution_exception 中被转为 cause 链入。