Skip to content

07.5 - JITWatch 使用手册

定位: JIT 编译日志的可视化分析工具——将晦涩的 -XX:+PrintCompilation 输出转换为可交互的编译决策看板 面试高频度: ⭐⭐⭐ 考查方式: 如何生成 JIT 日志(了解)、JITWatch 主要视图功能(应用中)、如何分析内联失败原因(进阶)

一、这是什么?为什么需要它?

JIT 编译器编译了哪些方法?为什么某些方法没有被内联?逃逸分析失败了还是成功了?循环展开做了吗?

这些问题的答案藏在 JIT 编译日志中。但原始日志是纯文本格式,信息密集难以阅读:

bash
# -XX:+PrintCompilation 输出示例 (纯文本, 一行一个方法):
 52    1       3       java.lang.String::hashCode (55 bytes)
 52    2       3       java.lang.String::equals (50 bytes)
 55    4       1       java.lang.Object::<init> (1 bytes)
 57    5       3       java.lang.String::indexOf (70 bytes)
 73   12       4       java.lang.String::hashCode (55 bytes)

一屏几百行——很难看出哪些是自己的方法、哪些优化没生效、内联链是怎样的。

JITWatch(Chris Newland 开发的开源工具)解决了这个问题:它解析 JIT 编译日志,提供可视化界面,让你像 IDE 调试代码一样"看"JIT 编译决策。

原始日志 (Text) ─→ JITWatch 解析 ─→ 可视化界面
                                       ├─ TriView: 源码 ↔ 字节码 ↔ 汇编
                                       ├─ 内联报告: 哪些方法内联了/没内联
                                       ├─ 编译链: 方法编译时间线
                                       └─ 热点排名: 哪些方法编译耗时最长

二、原理拆解

2.1 JIT 日志生成

要让 JITWatch 工作,首先需要生成完整的 JIT 编译日志:

bash
# 标准 JIT 日志参数集
-XX:+UnlockDiagnosticVMOptions   # 解锁诊断选项 (部分日志功能需要)
-XX:+TraceClassLoading            # 记录类加载事件 (JITWatch 需要)
-XX:+LogCompilation               # 启用编译日志记录
-XX:LogFile=jit.log               # 日志输出路径 (默认 hotspot.log)
# 可选增强
-XX:+PrintAssembly                # 包含反汇编输出 (需要 hsdis 库)
-XX:+PrintInlining                # 详细的内联决策信息
bash
# 生产环境请注意: 这些参数有性能开销!
# - 日志 I/O: 频繁写入日志文件 (特别是 PrintAssembly)
# - 内存开销: 日志缓冲区
# 建议: 仅测试/预发环境收集, 使用短暂的采样窗口

# 带时间限制的 JIT 日志收集 (可用 -XX:+UnlockDiagnosticVMOptions)
java -XX:+UnlockDiagnosticVMOptions \
     -XX:+LogCompilation \
     -XX:LogFile=jit.log \
     -XX:+PrintInlining \
     -jar myapp.jar

# 应用运行结束后, 在当前目录得到 jit.log
# jit.log 是 XML 格式, 直接打开可读性较差 → 需用 JITWatch

日志内容示例(简化的 XML 结构):

xml
<hotspot_log version='...'>
  <thread ...>
    <!-- 方法被编译 -->
    <task compile_id='1' method='java/lang/String hashCode ()I' 
          bytes='55' level='3' stamp='0.052'>
      <phase name='parse'/>
      <phase name='optimize'/>
      <phase name='matcher'/>
      <!-- 内联决策 -->
      <inline_fail reason='too big'/>
    </task>
  </thread>
</hotspot_log>

2.2 JITWatch 的安装与启动

bash
# 方式 1: 从 GitHub 下载发行版
# 访问 https://github.com/AdoptOpenJDK/jitwatch/releases
# 下载 jitwatch-<version>.jar

# 方式 2: 从源码构建
git clone https://github.com/AdoptOpenJDK/jitwatch.git
cd jitwatch
mvn clean install -DskipTests
# 构建后在 ui/app/target/ 下得到可执行 jar

# 启动 JITWatch
java -jar jitwatch-<version>.jar
# 或
java -cp jitwatch-<version>.jar com.chrisnewland.jitwatch.launch.JITWatchUI

2.3 JITWatch 核心视图

TriView — 三栏对照视图

这是 JITWatch 最有价值的功能——同一方法同时展示源码、字节码、机器码

┌─────────────────────────────────────────────────────────────────┐
│  TriView (三栏对照视图)                                          │
├──────────────────┬──────────────────┬──────────────────────────┤
│  源代码 (Java)    │  字节码           │  汇编 (机器码)            │
│                  │                  │                          │
│ int sum(int n) { │   0: iconst_0    │ mov     $0x0,%eax        │
│   int r = 0;     │   1: istore_2    │                          │
│   for(int i=0;   │   2: iconst_0    │                          │
│       i<n; i++){ │   3: istore_3    │ xor     %r10d,%r10d      │
│     r += i;      │   4: iload_3     │                          │
│   }              │   5: iload_1     │                          │
│   return r;      │   6: if_icmpge 15│ cmp    %rsi,%rdi         │
│ }                │   9: iload_2     │ jge    0x...             │
│                  │  10: iload_3     │                          │
│                  │  11: iadd        │ add     %eax,%r10d       │
│                  │  12: istore_2    │                          │
│                  │  13: iinc 3, 1   │ inc     %r10d            │
│                  │  16: goto 4      │ jmp     0x...            │
│                  │  19: iload_2     │                          │
│                  │  20: ireturn     │ ret                       │
├──────────────────┴──────────────────┴──────────────────────────┤
│  每行对应关系清晰可见:                                            │
│  Java 源码 for(i=0; i<n; i++) → 字节码 4-16 → 机器码循环主体     │
│  点击任意栏 → 其他两栏自动高亮对应行                               │
└─────────────────────────────────────────────────────────────────┘

使用场景

  • 确认热点方法的编译质量:机器码是否符合预期?
  • 检查边界检查是否被消除:if (i < arr.length) 是否已消除?
  • 观察循环展开效果:循环体是否被复制展开?

内联报告 (Inline Report)

展示每个方法的内联树——哪些方法被内联了,哪些没被内联以及原因:

┌─────────────────────────────────────────────────────────────────┐
│  内联报告 — com.example.MyService::processOrder (125 字节)     │
│                                                                 │
│  com.example.MyService::processOrder (125 bytes)                │
│  ├── inline (hot) → com.example.Order::getPrice (15 bytes)     │
│  │   └── inline (hot) → java.math.BigDecimal::multiply ...     │
│  │       └── inline (intrinsic) → java.math.BigInteger::...     │
│  ├── inline → com.example.Discount::calculate (45 bytes)       │
│  │   └── not inline → ...::applyCoupon (180 bytes)             │
│  │                           ^^^^^^^^^^^^^^^^^^^^^^^^          │
│  │                           callee 太大 → 超出 FreqInlineSize │
│  ├── not inline → com.example.Audit::log (55 bytes)            │
│  │                   ^^^^^^^^^^^^^^^^^^^^^^^^^^                │
│  │                   callee 不是热点 (cold)                     │
│  └── not inline → com.example.dao::save (80 bytes)             │
│                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^             │
│                      虚调用 — 接口有多种实现                     │
│                                                                 │
│  ㅤ                                                             │
│  红色/带❌标记 = 未内联 (鼠标悬停显示具体原因)                    │
│  绿色/带✓标记 = 已内联                                          │
│  内联预算剩余: 25%                                              │
└─────────────────────────────────────────────────────────────────┘

使用场景

  • 排查为什么热点方法没达到 C2 编译——是不是内联预算被耗尽?
  • 发现未被内联的虚方法调用——考虑是否可以将类/方法声明为 final
  • 发现过大的方法阻碍内联——考虑是否要拆分为更小的子方法

编译链 (Compilation Chain)

展示一个方法从 L0 到 L4 的完整编译演化历史:

┌─────────────────────────────────────────────────────────────────┐
│  Compilation Chain — com.example.MyService::handleRequest       │
│                                                                 │
│  Timeline:                                                      │
│  ───────────────────────────────────────────────────────────    │
│  T+0.000s  L0: 解释执行                                        │
│  T+0.500s  L3: C1 完整 profiling 编译 (因为方法调用达 10000 次)  │
│  T+0.510s  L3 编译完成, 替换解释版本                              │
│  T+1.200s  L4: C2 开始编译 (使用 L3 收集的 profile)              │
│  T+2.350s  L4 编译完成 → 替换 L3 版本                            │
│  T+3.000s  L4 → L0: 去优化! (类加载触发: 新子类被加载)            │
│  T+3.001s  L0: 回退到解释执行                                    │
│  T+3.500s  L3: 重新 C1 编译 (新 profile)                        │
│  T+4.200s  L4: 重新 C2 编译 (使用包含新类型的 profile)            │
│  T+5.500s  L4 编译完成, 稳定执行至今                              │
│                                                                 │
│  总编译耗时: 5.5s                                               │
│  编译次数: 4 次 (L3→L4→L3→L4)                                   │
│  去优化次数: 1 次                                               │
└─────────────────────────────────────────────────────────────────┘

使用场景

  • 查看方法是否经历了异常的去优化
  • 确认方法的最终编译层级(目标是 L4)
  • 分析去优化原因(类加载、类型 profile 变化等)

OSR 编译视图

专门展示 On-Stack Replacement 相关的编译:

┌─────────────────────────────────────────────────────────────────┐
│  OSR 编译视图                                                    │
│                                                                 │
│  com.example.DataProcessor::processLargeSet                      │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │ OSR 编译请求: 回边计数器达阈值 (14000)                     │    │
│  │ OSR 编译 ID: 42                                           │    │
│  │ OSR 编译层级: L4                                          │    │
│  │ OSR 入口: 循环开始处 (bci=15)                              │    │
│  │ OSR 触发时: 方法已执行到循环第 5000 次                      │    │
│  │ OSR 编译耗时: 850ms                                       │    │
│  └─────────────────────────────────────────────────────────┘    │
│  正常入口编译: ID 45, 稍后完成                                   │
└─────────────────────────────────────────────────────────────────┘

2.4 实际分析案例

案例 1:发现方法未达到 C2

问题: 一个热点 REST 接口方法始终在 L3 运行, 从未达到 L4

JITWatch 分析:
1. 打开 TriView → 方法字节码大小: 560 字节 (C2 编译预算可能不够)
2. 内联报告 → 该方法的内联链中有一个 400 字节的工具方法
   未被内联 → 因为超出 FreqInlineSize (325)
3. 因此 C2 编译时需要处理的代码总量太大 → 编译时间太长 → 
   编译器线程放弃 → 停在 L3

解决:
1. 手动内联那个大工具方法 (重构)
2. 或增加 FreqInlineSize: -XX:FreqInlineSize=500 (视情况)
3. 或增加 CICompilerCount 允许更多编译线程

案例 2:逃逸分析失败排查

问题: 分配大量临时对象的代码没有通过标量替换优化

JITWatch 分析:
1. 在内联报告中, 检查对象创建所在方法的内联链
2. 方法 A 调用了方法 B, 方法 B 调用了方法 C
3. 方法 C 中创建了临时对象 — 但方法 C 没有被内联到方法 A
4. 逃逸分析无法跨方法判断 → 对象被标记为 ArgEscape (参数逃逸)

解决:
1. 方法 C 为什么不内联? → 检查内联报告中的原因
2. 如果是因为方法太大 → 考虑拆小
3. 如果可以确保调用链简单 → 考虑使用 final 方法促进内联

三、图解全景

┌──────────────────────────────────────────────────────────────────────────┐
│                     JITWatch 分析工作流                                      │
│                                                                          │
│  ① 收集阶段                                                               │
│  ┌──────────────────────────────────────────────────────────────────┐    │
│  │ java -XX:+UnlockDiagnosticVMOptions                             │    │
│  │      -XX:+LogCompilation -XX:LogFile=jit.log                    │    │
│  │      -XX:+PrintInlining -jar myapp.jar                          │    │
│  └──────────────────────────────────────────────────────────────────┘    │
│           │                                                               │
│           ▼                                                               │
│  ② 加载阶段                                                               │
│  ┌──────────────────────────────────────────────────────────────────┐    │
│  │ 启动 JITWatch → 打开 jit.log                                    │    │
│  │ 配置源码路径: 指向项目源代码                                     │    │
│  │ (JITWatch 需要源码路径来显示 TriView 的源代码列)                  │    │
│  └──────────────────────────────────────────────────────────────────┘    │
│           │                                                               │
│           ▼                                                               │
│  ③ 探索阶段                                                               │
│  ┌──────────────────────────────────────────────────────────────────┐    │
│  │ 选择分析方法:                                                     │    │
│  │                                                                  │    │
│  │  热点方法排名 → 查看哪些方法编译耗时最长 (C2 在它们身上花时间最多)    │    │
│  │       ↓                                                          │    │
│  │  选择感兴趣的方法 → TriView 查看编译质量                           │    │
│  │       ↓                                                          │    │
│  │  内联报告 → 分析内联失败原因                                       │    │
│  │       ↓                                                          │    │
│  │  编译链 → 查看方法从 L0→L4 的完整演进                             │    │
│  └──────────────────────────────────────────────────────────────────┘    │
│           │                                                               │
│           ▼                                                               │
│  ④ 优化阶段                                                               │
│  ┌──────────────────────────────────────────────────────────────────┐    │
│  │ 基于分析结果做针对性优化:                                         │    │
│  │ - 过大方法 → 重构拆小                                             │    │
│  │ - 虚调用未内联 → 加 final                                         │    │
│  │ - 逃逸分析失败 → 减少参数传递链                                    │    │
│  │ - 编译线程不足 → 增加 CICompilerCount                             │    │
│  │ - 去优化频繁 → 检查类加载触发条件                                 │    │
│  └──────────────────────────────────────────────────────────────────┘    │
│           │                                                               │
│           ▼                                                               │
│  ⑤ 验证阶段                                                               │
│  ┌──────────────────────────────────────────────────────────────────┐    │
│  │ 再次运行 + 收集 JIT 日志 → 对比优化前后的编译决策                   │    │
│  │ 确认: 内联是否生效了? 方法是否达到 L4? 逃逸分析成功了?              │    │
│  └──────────────────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────────────────┘

四、实战验证

bash
# 完整 JIT 日志收集命令
java -XX:+UnlockDiagnosticVMOptions \
     -XX:+LogCompilation \
     -XX:+TraceClassLoading \
     -XX:+PrintInlining \
     -XX:LogFile=myapp-jit.log \
     -XX:+PrintCodeCache \
     -jar myapp.jar &
APP_PID=$!

# 等待应用预热 (如压测完成)
sleep 120

# 生成热点方法采样 (可选)
jcmd $APP_PID Compiler.print_statistics

# 停止应用
kill $APP_PID

# 调试用: 只收集特定方法的 JIT 日志
# -XX:CompileOnly=com.example.MyClass::myMethod
# 慎用: 只编译指定方法, 严重影响性能
bash
# 查看已编译方法的统计信息 (运行时)
jcmd <PID> Compiler.print_statistics

# 输出示例:
# Compiler statistics:
#   Number of compiled methods: 3842
#   Number of C1 compilations: 3100
#   Number of C2 compilations: 742
#   Average compilation time C1: 12ms
#   Average compilation time C2: 520ms
#   Total compilation time: 47.8s
bash
# 运行 JITWatch
java -jar jitwatch-1.4.5.jar

# 在 UI 中:
# 1. File → Open Log → 选择 myapp-jit.log
# 2. 如果看到源码, 需配置 Source Path (指向项目 src 目录)
# 3. 加载完成后 → 进入主界面

JITWatch 控制台模式(无 UI 时使用):

bash
# JITWatch 也支持命令行分析 (从 1.4.0+)
# 可直接生成分析报告
java -cp jitwatch-1.4.5.jar com.chrisnewland.jitwatch.launch.Headless \
     --log myapp-jit.log \
     --report output.html
# 生成 HTML 报告, 包含关键编译统计

五、面试视角

追问答案要点
JITWatch 能做什么?可视化分析 JIT 编译日志。主要功能:TriView(源码↔字节码↔汇编对照)、内联报告(内联决策树与失败原因)、编译链(方法编译历史)、OSR 视图(长循环编译分析)、热点排名(编译耗时排序)。
如何生成 JITWatch 需要的日志?添加 JVM 参数:-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:+TraceClassLoading -XX:LogFile=jit.log。生产环境慎用(有 I/O 开销)。
如何通过 JITWatch 发现内联问题?打开内联报告,检查热点方法的内联树。红色标记的方法就是未被内联的方法——鼠标悬停查看原因(方法太大、虚调用、不是热点等)。针对性优化:拆小方法、加 final、加参数。
什么是 TriView?解决了什么问题?三栏对照视图——同一方法同时展示源码、字节码、机器码。解决"Java 代码到底被编译成了什么"的问题。可以看到边界检查是否被消除、循环是否被展开、锁是否被消除。
JITWatch 的"编译链"视图有什么用?展示一个方法从 L0(解释执行)到 L4(C2 完整优化)的完整编译过程。特别有用的是排查"为什么方法没有达到 L4"——如果看到去优化事件,可以分析触发原因。
JITWatch 是生产环境工具吗?主要用于测试/预发环境。生产环境记录完整 JIT 日志有性能开销。生产环境推荐使用 jcmd 的 Compiler.print_statistics 做轻量级查看。

📚 相关链接

  • **解释执行与编译执行** — JIT 编译日志中的解释 vs 编译状态对比
  • **C1与C2编译器与分层编译** — JITWatch 中 L0-L4 层级对应的编译阶段
  • **方法内联和逃逸分析** — JITWatch 内联报告中展示的内联决策细节
  • ← 返回 **执行引擎索引**

Knowledge4J — Java 知识库