JITWatch需配合-XX:+LogCompilation、-XX:+PrintAssembly(配hsdis)及-XX:+UnlockDiagnosticVMOptions才能可视化字节码到汇编的完整链条,单独LogCompilation仅含编译决策XML,无汇编指令。

直接用 -XX:+LogCompilation 生成的 XML 日志本身不包含汇编指令,它只记录 JIT 编译器的决策过程(如方法是否被编译、编译层级、内联路径、逃逸分析结果、去虚拟化等)。要看到“字节码 → 汇编”的完整链条,必须配合 -XX:+PrintAssembly(需 hsdis 插件)和 JITWatch 的交叉比对能力——JITWatch 不是汇编查看器,而是把编译日志、热点方法、内联树、IR 节点、甚至反汇编片段组织成可导航的可视化视图。
关键前提:环境必须齐备
缺一不可:
- JDK 必须是带调试符号的版本(如 OpenJDK build with debug info),或使用 JDK 17+ 的
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly配合正确安装的hsdis-amd64.so(Linux)或hsdis-amd64.dll(Windows); -
-XX:+LogCompilation要搭配-XX:LogFile=hotspot.log显式指定输出路径,避免被重定向或丢弃; - JITWatch 需加载同一运行周期产生的
hotspot.log,且 JVM 启动时最好加上-XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading辅助定位类加载与编译时机; - 避免容器或沙箱环境干扰信号处理——
PrintAssembly依赖 SIGQUIT(Ctrl+\)触发或由 JVM 自动在编译后输出,某些 runtime 会屏蔽该信号。
定位“断层”的真实含义
所谓“字节码向汇编转译的断层”,通常不是编译失败,而是以下几种可观测偏差:
-
字节码被跳过编译:LogCompilation 中显示
not compilable (disabled)或not osr compilable,常见于构造器、异常处理器、或被 C1 编译器拒绝的复杂控制流; -
内联断裂:父方法被 C2 编译,但某个被调用方法未被内联(日志中出现
inline_fail: reason=too_big或reason=hot_method_not_compilable),导致最终汇编里仍保留 call 指令而非展开代码; -
去优化回退(deoptimization):LogCompilation 中出现
made not entrant+made zombie,说明该编译版本因类型假设失败被废弃,后续执行回落到解释器或重新编译,此时你看到的汇编可能根本没被执行; -
汇编缺失对应段:LogCompilation 显示某 method 已编译(
compile_id='123'),但PrintAssembly输出里找不到 ID 123 的汇编块——大概率是编译线程尚未完成输出就退出,或日志被截断,需加-XX:+UsePerfData并用jstat确认编译队列状态。
在 JITWatch 中高效抓断层
不要从头扫 XML。聚焦三个视图联动:
- 左侧 Hot Methods 找执行次数 TopN 的方法,右键 → “Show Compilation” 查看是否被编译、用哪个层级(C1/C2)、有无失败标记;
- 点击编译条目进入 Compilation Log,展开
<task></task>下的<inlining></inlining>节点,逐层点开子调用,红色叉号即内联失败点,鼠标悬停显示具体 reason; - 切换到 Assembly 标签页(前提是
PrintAssembly日志已导入),搜索方法签名,观察是否有明显“call”指令指向未内联方法,或是否存在大量mov rax, [rip + ...]类型的常量池访问——这往往意味着字符串拼接、lambda 重写等未被充分优化的模式; - 启用 Code Tree 视图,勾选 “Show IR Nodes”,对比
Phi、CallStaticJava、LoadB等节点数量变化:若某次编译后CallStaticJava暴增,基本可判定逃逸分析失效或同步消除未生效。
一个典型断层案例:StringBuilder.append 被拒绝内联
现象:业务循环中频繁调用 sb.append("x"),LogCompilation 显示该 append 方法反复触发 C1 编译但始终未被 C2 内联进外层方法;JITWatch Assembly 中看到大量 call 0x... <string.valueof></string.valueof> 指令。
原因链:
- C2 默认对
StringBuilder.append(String)设定高内联阈值(因涉及字符数组复制逻辑); - 若该 String 是编译期不可知的(如来自 Map.get()),则逃逸分析无法证明其局部性,导致后续标量替换失败;
- 结果:JIT 放弃内联,每次调用都走完整方法入口,且无法消除中间对象创建。
验证方式:在 JVM 参数中加入
-XX:MaxInlineSize=50 -XX:FreqInlineSize=300并观察 JITWatch 中内联树是否闭合;或改用sb.append('x')(char 版本),通常能被完全内联并展开为寄存器操作。











