JIT 编译日志解读

开启 JIT 日志

基本配置

# 开启编译日志
java -XX:+PrintCompilation \
     -jar application.jar

# 开启详细日志
java -XX:+PrintCompilation \
     -XX:+UnlockDiagnosticVMOptions \
     -jar application.jar

日志输出

# 输出到文件
java -XX:+PrintCompilation \
     -XX:LogFile=/tmp/jit.log \
     -jar application.jar

# 实时查看
java -XX:+PrintCompilation \
     -XX:+UnlockDiagnosticVMOptions \
     -jar application.jar 2>&1 | head -100

日志格式

基本格式

[timestamp] [method_id] [flags] [type] [method_name] [@ bc_pos] [compile_info]

示例解析

10    234 %  !   com.example.MyClass::hotMethod @ 5 <compiled>
|     |     |    |                              |    |     |
|     |     |    |                              |    |     编译信息
|     |     |    |                              |    字节码位置
|     |     |    |                              方法名
|     |     |    标记(详见下表)
|     方法ID
时间戳(启动后的毫秒数)

标记符号

符号说明
%OSR(栈上替换)编译
!编译方法包含异常处理
nnative 方法
@指定热点位置
*尝试编译但失败
s同步方法

编译类型

类型说明
compiled方法已编译
made not entrant编译代码已废弃
made zombie编译代码可回收

日志字段详解

编译信息

<compiled>
<made not entrant>
<inline (hot)>
<too big>
<exception handler>

编译阶段

信息说明
inline (hot)方法已内联
inline (hot)方法已内联(热点)
inline (cold)方法已内联(冷)
too big方法太大,无法内联

日志分析方法

找出热点方法

# 统计编译次数
grep -o 'com\.example\.' /tmp/jit.log | sort | uniq -c | sort -rn | head -20

# 找出编译最多的方法
grep '<compiled>' /tmp/jit.log | awk '{print $5}' | sort | uniq -c | sort -rn | head -20

找出未编译的方法

# 找出频繁调用但未编译的方法
grep -v '<compiled>' /tmp/jit.log | head -100

找出 OSR 编译

# 找出栈上替换编译
grep '%' /tmp/jit.log

常见问题分析

问题一:方法未编译

// 日志
123  456        com.example.MyClass::rareMethod

// 分析:方法被编译但不是热点
// 如果方法应该被编译但没有,考虑:
// 1. 编译阈值是否太低
// 2. 代码缓存是否满了

问题二:去优化

// 日志
456  789 %      com.example.MyClass::method @ 5 <made not entrant>

// 分析:编译代码被废弃
// 可能原因:
// 1. 类加载改变了类型假设
// 2. 逆优化次数过多
// 3. OSR 后代码不再有效

问题三:方法太大

// 日志
789  123        com.example.MyClass::hugeMethod @ 0 <compiled too big>

// 分析:方法太大,编译受限
// 考虑拆分方法

问题四:内联失败

// 日志
... com.example.MyClass::innerMethod <inline (hot)>
... com.example.MyClass::bigMethod <callee is too large>

// 分析:大方法无法内联
// 考虑拆分大方法

使用 JITWatch 分析

JITWatch 介绍

JITWatch 是一个可视化的 JIT 日志分析工具。

使用方法

# 1. 获取 JIT 日志
java -XX:+UnlockDiagnosticVMOptions \
     -XX:+LogCompilation \
     -XX:LogFile=/tmp/jit.log \
     -jar application.jar

# 2. 下载 JITWatch
git clone https://github.com/AdoptOpenJDK/jitwatch.git

# 3. 启动 JITWatch
./gradlew run

# 4. 加载日志和源代码

JITWatch 功能

功能说明
内联树显示方法的内联关系
编译列表列出所有编译的方法
未编译方法显示未编译的热点方法
源码对照将优化与源码对应

PrintInlining

开启内联日志

# 打印内联决策
java -XX:+PrintCompilation \
     -XX:+PrintInlining \
     -XX:+UnlockDiagnosticVMOptions \
     -jar application.jar

日志格式

@ 5   com.example.MyClass::innerMethod   inline (hot)
@ 10  com.example.MyClass::privateMethod inline (hot)
@ 15  com.example.MyClass::bigMethod    too big (325 bytes)

字段说明

字段说明
@ bc_pos字节码位置
method_name方法名
inline (hot)已内联(热点)
too big方法太大
callee is too large被调用者太大

LogCompilation XML 格式

开启 XML 日志

# 输出为 XML 格式
java -XX:+UnlockDiagnosticVMOptions \
     -XX:+LogCompilation \
     -XX:LogFile=/tmp/jit.xml \
     -jar application.jar

XML 结构

<?xml version="1.0" encoding="UTF-8"?>
<CompilationLog>
  <task compile_id="1" method="java/lang/String.hashCode" ...>
    <phase name="C2" />
    <directive ... />
  </task>
</CompilationLog>

编译日志统计

常用统计命令

# 统计编译次数
grep -c '<task_queued' /tmp/jit.log

# 统计 C1/C2 编译比例
grep "compiler='C1'" /tmp/jit.log | wc -l
grep "compiler='C2'" /tmp/jit.log | wc -l

# 统计内联次数
grep -c 'inline (hot)' /tmp/jit.log

# 统计去优化次数
grep -c 'made not entrant' /tmp/jit.log

Python 脚本分析

#!/usr/bin/env python3
import re
from collections import Counter

with open('/tmp/jit.log') as f:
    content = f.read()

# 提取编译方法
methods = re.findall(r'(\S+)::(\S+)', content)
method_counts = Counter(methods)

# 打印 Top 10
for method, count in method_counts.most_common(10):
    print(f"{method}::{count}")

常见性能问题

1. 代码缓存满

// 日志
Java HotSpot(TM) 64-Bit Server VM warning: 
    code cache is full. Compiler has been disabled.
    Try increasing the code cache size.

// 解决
java -XX:ReservedCodeCacheSize=100m

2. 编译线程饥饿

// 日志
// 编译队列持续增长

// 解决
java -XX:CICompilerCount=8

3. 频繁去优化

// 日志
... <made not entrant> ...
... <made not entrant> ...

// 解决
// 1. 减少类加载动态性
// 2. 使用 final 类/方法