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.javaString.java 等类库 Java 层 JDWP Extension Pack for Java (redhat.java + vscjava.*)
libjvm.dylib、解释器/GC/JIT、main.cjava_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.cresolvedFieldEntry.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.cppStubRoutines::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_CreateJavaVMJNI_GetDefaultJavaVMInitArgsJNI_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:3583src/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() 初始化 StringSystemClassThreadThreadGroup
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 模式,LauncherHelpersun/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.javaHello

  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_statictemplateTable_x86.cpp:2381)--- 从常量池缓存中解析/加载 ResolvedMethodEntry(包含目标 Method* 和 flags),若未解析则调用 InterpreterRuntime::resolve_from_cache
  2. prepare_invoketemplateTable_x86.cpp:3193)--- 关键:跳过 receiver 加载invokestatic 无 receiver),只保存返回地址、设置 TOS 状态
  3. jump_from_interpreted --- 跳转到目标方法的解释入口开始执行

AArch64 同理,在 src/hotspot/cpu/aarch64/templateTable_aarch64.cpp:3413-3427

补充:模板解释器的工作原理

以上 invokestaticTemplateTable::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-> 简写,零开销。

最终调用链为 InterpreterMacroAssemblerMacroAssemblerAssembler,由 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_bytecodetemplateTable_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 中。运行时通过分发表DispatchTabletemplateInterpreter.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 = falsehas_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 模板解释器实现(331423813193
cpu/aarch64/templateTable_aarch64.cpp AArch64 模板解释器实现(3413
share/interpreter/zero/bytecodeInterpreter.cpp C++ 解释器(2364
share/interpreter/linkResolver.cpp 方法解析(175411091142
share/interpreter/interpreterRuntime.cpp 运行时缓存解析(778985
share/oops/cpCache.cpp 常量池缓存写入(235)、类初始化屏障(176
share/c1/c1_GraphBuilder.cpp C1 字节码处理(29341915
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 编译代码解析桩(14313321547

附录:<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


附录二:实战验证 ------ ClassNotFoundExceptionNoClassDefFoundError

错误现场

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)

第一段:ClassNotFoundExceptionNoClassDefFoundError

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_exceptionsystemDictionary.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

总结

NoClassDefFoundErrorError 的子类,在 <clinit> 失败处理中走 THROW_OOP 路径直接抛出(不包装为 ExceptionInInitializerError);其根因 ClassNotFoundExceptionhandle_resolution_exception 中被转为 cause 链入。

相关推荐
唐青枫1 天前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
一个做软件开发的牛马1 天前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端
用户3721574261351 天前
Java 处理 PDF 图片:提取 PDF 中的图片,并压缩 PDF 图片体积
java
用户3721574261351 天前
Java 打印 Word 文档:从基础打印到高级设置
java
用户3521802454752 天前
当 Prompt 学会"热更新":Spring Boot × Nacos3 AI 实战
java·spring boot·ai编程
东坡白菜2 天前
破局全栈:一个前端开发的Java入门实战记录(1)
java·全栈
唐青枫2 天前
Java Tomcat 实战指南:从 Servlet 容器到 Spring Boot 部署
java
wsaaaqqq2 天前
roudan:自由选择实体、灵活操作数据、快速写入数据库的 Java 框架
java
plainGeekDev2 天前
null 判断 → Kotlin 可空类型
android·java·kotlin