#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(栈上替换)编译 |
! | 编译方法包含异常处理 |
n | native 方法 |
@ | 指定热点位置 |
* | 尝试编译但失败 |
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 类/方法