mac(m5)平台编译openjdk

在 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-allow entitlement(构建时 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)

  1. 在 .cpp/.m(如 main.c、resolvedFieldEntry.cpp)按需打断点。
  2. 在 .java(如 LauncherHelper.checkAndLoadMain)按需打断点。
  3. 运行与调试面板选择 「混合调试: C++ + Java」,F5 启动。
  4. 先命中 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 有自己的处理器消化这些信号。

排查过程(实测)

  1. lldb 启动 java,continue 后停在 thread #3, stop reason = signal SIGILL。
  2. disassemble --pc 看信号点指令 → dcps1 #0xdead,确认是调试陷阱而非真异常。
  3. 放行该信号后 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 去挑:

  1. 命令行用自编译 java 起进程(suspend=y 挂起等待):

    bash 复制代码
    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
  2. VSCode 选 「Java: Attach 到 5005 (Java 层)」,F5 接上去。

  3. 命中断点后,在 Debug Console 验证确实是 JDK 25:

    arduino 复制代码
    System.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_GetCreatedJavaVMs
  • ParseArguments() --- 解析命令行参数
  • 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()):

  1. MemoryFileManager 拦截编译输出------.class 字节码不写磁盘,而是存入 Map<String, byte[]> inMemoryClasses
  2. JavacTool.getTask(...) 创建编译任务
  3. task.call() 执行编译
  4. 注入的隐式选项:-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

  1. 创建类加载器 :MemoryClassLoader 以系统类加载器为父加载器,持有 inMemoryClasses,优先从内存中加载类

  2. 找主类 :先找文件中第一个类,如果没有 main 则找与文件名同名的类(Hello.java → Hello)

  3. 找 main 方法:支持 static 和 instance 两种形式

  4. 反射调用 :

    java 复制代码
    mainMethod.invoke(receiver, (Object) mainArgs);
  5. 如果 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);                            // ③
}

三步走:

  1. load_resolved_method_entry_special_or_static (templateTable_x86.cpp:2381)--- 从常量池缓存中解析/加载 ResolvedMethodEntry(包含目标 Method* 和 flags),若未解析则调用 InterpreterRuntime::resolve_from_cache
  2. prepare_invoke (templateTable_x86.cpp:3193)--- 关键:跳过 receiver 加载 (invokestatic 无 receiver),只保存返回地址、设置 TOS 状态
  3. 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 链入。

相关推荐
霸道流氓气质5 分钟前
Saga模式入门与实战:编排与协同模式、补偿设计原则与Java生产级实现
java·开发语言
江湖有缘10 分钟前
Docker实战 | 使用Docker部署Readeck书签管理工具
java·docker·eureka
小毛驴85014 分钟前
mvn help:effective-settings 与 mvn help:effective-pom
java·linux·前端
silvamux_27 分钟前
MiniMax API 调用教程:兼容 OpenAI 写法
java·数据库·elasticsearch
染指111031 分钟前
145.Agent-LangGraph基础概念和安装和简单代码示例
java·服务器·数据库·langchain·agents
Doker 多克34 分钟前
ForkJoinPool 详解
java·多线程
Wang's Blog1 小时前
Java 项目实战: 交友项目 - Dubbo 启动检查
java·dubbo·交友
零基础1231 小时前
权限管理怎么做?从“资格”与“空间”双维度设计一套可落地的权限体系
java·服务器·数据库
starzy19901 小时前
Flink Task 调度任务深度剖析:从调度策略到故障恢复的完整链路
java·大数据·flink
开开心心_Every1 小时前
文件查重软件批量删除重复文件释放空间
java·开发语言·c#·计算机外设·ocr·excel·perl