Skip to content

JIT 编译与性能调优深度解析

面试官:说说 JVM 的 JIT 编译吧,为什么 Java 要用 JIT?

:JIT(Just-In-Time)编译器将热点代码编译为本地机器码,运行速度接近 C/C++。Java 不直接编译成机器码是因为支持运行时多态,且 JIT 可以根据运行时数据做更激进的优化,如内联优化、逃逸分析。

面试官:那线上 CPU 飙高怎么排查?

这个问题考察的是实战排查能力。能说清 top -Hpjstack、MAT 等工具链式排查的候选人,才是真正有经验的 JVM 实战者。


Q1:JIT 编译和解释执行有什么区别?为什么 Java 用 JIT 而不是提前编译?必考

Section titled “Q1:JIT 编译和解释执行有什么区别?为什么 Java 用 JIT 而不是提前编译?”

解释执行 vs JIT 编译:

Java 源码 → 字节码(.class)
运行时
┌──────────────────────┐
│ 执行引擎 │
│ │
│ 解释器(Interpreter) │← 逐条翻译字节码执行,启动快,运行慢
│ JIT 编译器 │← 将热点代码编译为本地机器码,运行极快
└──────────────────────┘

对比表格:

执行方式原理启动速度运行速度内存占用
解释执行逐条翻译字节码快(无编译开销)慢(约慢 10~100 倍)
JIT 编译热点代码编译为机器码慢(需要预热)快(接近 C/C++)高(机器码占用内存)
AOT 编译提前编译为机器码极快(无预热)中等(无运行时优化)

为什么用 JIT 而非 AOT(Ahead-Of-Time):

1. 运行时多态
├── 接口调用的具体实现在运行时才确定
└── 提前编译无法做激进的内联优化
2. 运行时特化优化
├── JIT 可以利用运行时 Profiling 数据
│ └── 实际调用最多的分支、实际使用的类型
├── 根据实际数据做更激进的优化
│ └── 如去除未使用的分支、内联虚方法
└── 效果比静态分析更好
3. 平台无关性
├── "一次编译,到处运行"需要字节码作为中间格式
└── AOT 为特定平台编译,失去跨平台能力

性能数据:

解释执行:基准(1x)
C1 编译(Client Compiler):约快 5~10 倍
C2 编译(Server Compiler):约快 10~50 倍,接近 C/C++
启动时间:
解释执行:约 100ms
JIT 预热:约 1~5 秒(取决于热点代码量)
AOT(GraalVM Native Image):约 10~50ms

本质一句话:JIT 在运行时编译热点代码,利用 Profiling 数据做特化优化,性能超越 AOT,但有预热开销。


Q2:什么是分层编译?C1 和 C2 有什么区别?高频

Section titled “Q2:什么是分层编译?C1 和 C2 有什么区别?”

分层编译(Tiered Compilation,Java 8+ 默认):

Level 0:解释执行(启动阶段,立即可用)
Level 1~3:C1 编译器(Client Compiler)
├── Level 1:简单编译(无 Profiling),快速生成机器码
├── Level 2:有限 Profiling(方法和循环计数)
└── Level 3:完整 Profiling(收集类型信息等,为 C2 做准备)
Level 4:C2 编译器(Server Compiler)
└── 使用 Profiling 数据做深度优化,生成最优机器码

编译路径:

典型路径:0 → 3 → 4
先用 C1 快速编译上线(Level 3)
收集 Profiling 数据后,C2 深度优化(Level 4)
极端路径:0 → 4(跳过 C1)
方法调用极频繁,JVM 直接用 C2 编译
降级路径:4 → 0(C2 失败)
C2 编译失败或代码被去优化(deoptimize),回退到解释执行

C1 vs C2 对比:

编译器优化程度编译速度生成的机器码质量适用场景
C1浅层优化快(约 1~2 秒)中等启动阶段、客户端应用
C2深度优化慢(约 5~10 秒)最优长期运行的服务端应用

关键参数:

Terminal window
# 分层编译(默认开启)
-XX:+TieredCompilation
# 只用 C1(客户端模式)
-client
# 只用 C2(服务端模式,不推荐,启动慢)
-server
# 设置编译阈值(调低可加快预热,但可能降低优化质量)
-XX:CompileThreshold=10000 # 方法调用次数阈值

本质一句话:分层编译用 C1 快速生成机器码,用 C2 深度优化,兼顾启动速度和运行性能。


Q3:热点代码探测是如何工作的?什么是 OSR?高频

Section titled “Q3:热点代码探测是如何工作的?什么是 OSR?”

热点代码探测机制:

JIT 只编译”值得编译”的热点代码,通过计数器判断:

方法调用计数器(Invocation Counter):
├── 统计方法被调用的次数
├── Server 模式默认阈值:10000 次
└── 超过阈值 → 触发方法编译
回边计数器(Back Edge Counter):
├── 统计循环体被执行的次数(代码的"向后跳转")
└── 超过阈值 → 触发 OSR 编译

OSR(On-Stack Replacement,栈上替换):

循环体在执行中途被 JIT 编译,编译完成后直接在当前栈帧中”偷偷替换”执行引擎:

场景:循环执行 10000 次
Step 1:循环开始,解释执行
执行 10000 次,回边计数器超阈值
Step 2:JIT 编译循环体为机器码
Step 3:OSR 替换
在当前栈帧中替换执行引擎
继续执行剩余的 99990000 次循环(机器码,极快)
用户无感知,继续循环但速度大幅提升

代码示例:

// OSR 示例
void process() {
long start = System.nanoTime();
int sum = 0;
// 循环执行 1 亿次
for (int i = 0; i < 100_000_000; i++) {
sum += i; // 解释执行约 10 秒
}
// OSR 后,循环体编译为机器码,剩余循环极快
long elapsed = System.nanoTime() - start;
System.out.println("耗时: " + elapsed / 1_000_000 + "ms");
}

性能数据:

循环 1 亿次:
解释执行:约 10 秒
OSR 后:约 100ms(快 100 倍)
OSR 触发条件:
回边计数器 > 阈值(默认 10000)

本质一句话:热点探测用计数器判断,方法编译和 OSR 分别针对方法和循环,OSR 让循环在中途被优化。


Q1:什么是方法内联?它对性能有什么影响?必考

Section titled “Q1:什么是方法内联?它对性能有什么影响?”

方法内联(Method Inlining):

将被调用方法的代码直接复制到调用处,消除方法调用开销(入栈/出栈):

// 内联前:
int result = Math.abs(x); // 一次方法调用
// 内联后(等效):
int result = x < 0 ? -x : x; // 直接展开,无调用开销

内联的优势:

1. 消除方法调用开销
├── 无需创建栈帧
├── 无需参数传递
└── 无需跳转指令
2. 为后续优化创造条件
├── 内联后代码连成一片
└── 可以做更激进的优化(如常量折叠、死代码消除)
3. 支持多态内联
└── JIT 根据运行时类型信息,内联实际调用的方法

内联的限制:

限制因素说明默认值
方法体大小方法体过大不会内联≤ 35 字节(普通),≤ 325 字节(热点)
调用深度内联嵌套层数过多会停止≤ 9 层
虚方法需要运行时类型信息有 Profiling 数据后可内联

代码示例:

// 未内联:每次调用都走方法调用流程
int calculate(int x) {
return Math.abs(x) * 2;
}
// 内联后:直接展开
int calculate(int x) {
return (x < 0 ? -x : x) * 2;
}
// 进一步优化:常量折叠
int result = calculate(5);
// 内联后: result = (5 < 0 ? -5 : 5) * 2
// 编译器推断: result = 5 * 2 = 10
// 最终:result = 10

性能数据:

方法调用开销:约 5~10 纳秒(入栈/出栈)
内联后:约 0.5~1 纳秒(直接计算)
性能提升:约 5~10 倍
大量小方法内联后:
吞吐量提升 10~30%
延迟降低 10~20%

本质一句话:方法内联消除调用开销,为后续优化创造条件,是 JIT 最重要的优化之一。


Q2:逃逸分析是什么?对 GC 性能有什么帮助?高频

Section titled “Q2:逃逸分析是什么?对 GC 性能有什么帮助?”

逃逸分析(Escape Analysis):

JIT 编译器分析对象作用域,判断对象是否”逃逸”出方法或线程:

对象逃逸类型:
├── 不逃逸(方法逃逸不发生)
│ └── 对象只在方法内使用,方法返回后无人引用
│ → 可在栈上分配,方法返回时随栈帧销毁,无 GC 压力
├── 方法逃逸
│ └── 对象被作为返回值、赋值给类字段、传入其他方法
│ → 必须在堆上分配
└── 线程逃逸
└── 对象被共享给多个线程(如赋值给静态字段)
→ 必须在堆上分配,且需考虑线程安全

基于逃逸分析的三种优化:

1. 栈上分配(Stack Allocation):

// 对象不逃逸
void process() {
Point p = new Point(1, 2); // p 不逃逸
int r = p.x + p.y; // 只在方法内使用
return r;
// JIT 优化:栈上分配,方法结束时随栈帧销毁,无 GC
}

2. 标量替换(Scalar Replacement):

// 更激进的优化:对象被拆散为基本类型变量
void scalarReplace() {
Point p = new Point(1, 2); // p 不逃逸
int area = p.x * p.y;
// JIT 标量替换后:
// int p_x = 1, p_y = 2; // Point 对象不分配,直接用标量
// int area = p_x * p_y;
// 甚至可能存入寄存器,完全不走内存
}

3. 同步消除(Lock Elimination):

void process() {
Vector<String> v = new Vector<>(); // Vector 所有方法都 synchronized
v.add("a");
v.add("b");
// v 不逃逸,JIT 消除所有 synchronized,等同于使用 ArrayList
}

性能对比:

优化方式GC 压力内存分配性能提升
堆上分配高(需 GC)在堆基准
栈上分配无(随栈帧销毁)在栈约 10~20%
标量替换无(无对象分配)寄存器/栈约 20~30%

验证逃逸分析效果:

Terminal window
# 开启逃逸分析(Java 8+ 默认)
-XX:+DoEscapeAnalysis
# 关闭逃逸分析,用于对比测试
-XX:-DoEscapeAnalysis
# 打印逃逸分析结果(需要 debug JVM)
-XX:+PrintEscapeAnalysis

本质一句话:逃逸分析让”短命对象”在栈上消亡,绕过 GC,极大减少 Minor GC 频率,是 JIT 的关键优化。


Q3:JIT 还有哪些常见优化?列举几个重要的。中频

Section titled “Q3:JIT 还有哪些常见优化?列举几个重要的。”

常见 JIT 优化:

1. 常量折叠(Constant Folding):

int a = 1 + 2; // 编译期优化为:a = 3
int b = a * 3; // 编译期优化为:b = 9
final int MAX = 100;
int c = MAX + 1; // 编译期优化为:c = 101

2. 死代码消除(Dead Code Elimination):

void process() {
int a = 1;
int b = 2;
int c = a + b; // c 未使用,消除
if (false) {
System.out.println("never executed"); // 消除
}
}
// 优化后:
void process() {
// 方法体为空
}

3. 循环优化:

// 循环展开(Loop Unrolling)
for (int i = 0; i < 100; i++) {
sum += i;
}
// 展开后:
for (int i = 0; i < 100; i += 4) {
sum += i;
sum += i + 1;
sum += i + 2;
sum += i + 3;
}
// 减少循环次数,减少分支预测失败
// 循环不变量外提(Loop Invariant Code Motion)
for (int i = 0; i < n; i++) {
int x = a * b; // a、b 在循环内不变
arr[i] = x + i;
}
// 优化后:
int x = a * b; // 外提
for (int i = 0; i < n; i++) {
arr[i] = x + i;
}

4. 分支预测优化:

// 根据运行时 Profiling 数据,JIT 知道哪个分支更可能执行
if (condition) { // JIT 发现 condition 95% 为 true
// 热路径:JIT 优化这部分代码
hotPath();
} else {
// 冷路径:不优化
coldPath();
}
// JIT 甚至会生成"去优化"指令:
// 如果 condition 竟然为 false,回退到解释执行

5. 虚方法内联(Virtual Method Inlining):

interface Animal {
void speak();
}
class Dog implements Animal {
void speak() { System.out.println("Woof"); }
}
void process(Animal a) {
a.speak(); // 虚方法调用
}
// JIT 根据 Profiling 数据发现 90% 是 Dog
// 生成类型检查 + 内联:
void process(Animal a) {
if (a instanceof Dog) {
System.out.println("Woof"); // 内联 Dog.speak()
} else {
a.speak(); // 回退到虚方法调用
}
}

本质一句话:JIT 利用运行时 Profiling 数据做常量折叠、死代码消除、循环优化、分支预测、虚方法内联等优化,超越静态编译。


Q1:如何系统性地进行 JVM 调优?有什么方法论?实战

Section titled “Q1:如何系统性地进行 JVM 调优?有什么方法论?”

调优优先级:

代码优化 > 架构优化 > JVM 调优
JVM 调优是最后手段,不要期望调 JVM 参数解决所有问题

方法论:目标驱动:

1. 明确目标
├── 是要低延迟(GC 停顿 < 100ms)?
├── 还是要高吞吐量(提高 QPS)?
└── 两者有一定矛盾,需权衡
2. 建立基准
├── 用 JMeter/wrk 建立性能基线
├── 收集 GC 日志、JVM 指标(Prometheus + JMX Exporter)
└── 记录当前 QPS、延迟、GC 频率等
3. 识别瓶颈
├── 吞吐量低 → 检查 GC 时间占比(jstat -gcutil,GCT/运行时间)
├── 延迟高 → 检查 GC 停顿时间(GC 日志中的 Pause)
└── OOM → 排查内存泄漏
4. 调优顺序
第一优先:堆大小(-Xms, -Xmx)
第二优先:收集器选择(G1/ZGC)
第三优先:各分代大小(-Xmn, -XX:MaxMetaspaceSize)
第四优先:GC 线程数等细节参数
5. 验证效果
└── 每次只改一个参数,对比前后指标,量化改善效果

调优决策树:

问题:吞吐量不足?
├── 是 → GC 时间占比 > 10%?
│ ├── 是 → 增大堆或换 Parallel GC
│ └── 否 → 优化业务代码,减少对象分配
└── 否 → 问题:延迟过高?
├── 是 → Full GC 频繁?
│ ├── 是 → 排查内存泄漏或换 G1/ZGC
│ └── 否 → Minor GC 频繁?增大新生代
└── 否 → 问题:OOM?
├── 堆 OOM → 增大堆或排查泄漏
├── 元空间 OOM → 增大元空间或排查类加载
└── 直接内存 OOM → 增大 MaxDirectMemorySize

关键参数清单:

Terminal window
# 堆内存
-Xms4g # 堆初始大小(建议与 Xmx 一致)
-Xmx4g # 堆最大大小
-Xmn1g # 新生代大小(或用 -XX:NewRatio=3)
# GC 收集器
-XX:+UseG1GC # G1(推荐,Java 9+ 默认)
-XX:MaxGCPauseMillis=200 # 目标停顿时间
-XX:+UseZGC # ZGC(Java 17+ 推荐)
# 元空间
-XX:MaxMetaspaceSize=256m # 元空间上限
# GC 日志
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=10,filesize=20m
# 故障排查
-XX:+HeapDumpOnOutOfMemoryError # OOM 时自动 dump
-XX:HeapDumpPath=/tmp/ # dump 文件路径

本质一句话:JVM 调优遵循”目标驱动 → 建立基准 → 识别瓶颈 → 逐步调优 → 验证效果”的方法论,优先调堆大小和收集器。


Q2:堆大小如何设置?有什么最佳实践?必考

Section titled “Q2:堆大小如何设置?有什么最佳实践?”

堆大小设置原则:

1. 初始大小 = 最大大小(-Xms = -Xmx)
└── 避免 JVM 运行时动态扩堆引发 Full GC 和停顿
2. 新生代不要太小
└── 新生代太小导致对象过早晋升老年代,频繁 Full GC
3. 新生代不要超过堆的 1/2
└── 老年代需要足够空间存放长期对象
4. 元空间建议设置上限
└── -XX:MaxMetaspaceSize=256m,避免无限增长耗尽本地内存
5. 堆不要超过物理内存的 70%
└── 为 OS 页缓存、本地内存等留余量

内存计算示例:

服务器:16GB 内存
JVM 堆:8GB(-Xms8g -Xmx8g) ← 约占物理内存 50%
新生代:2GB(-Xmn2g) ← 占堆 25%
老年代:6GB(堆 - 新生代) ← 占堆 75%
元空间:256MB(-XX:MaxMetaspaceSize=256m)
直接内存:1GB(-XX:MaxDirectMemorySize=1g)
剩余:约 4GB ← OS 页缓存、线程栈等

对比表格:

场景堆大小新生代大小收集器建议
微服务(轻量)2~4GB512MB~1GBG1-Xms2g -Xmx2g -Xmn512m -XX:+UseG1GC
Web 服务(中量)4~8GB1~2GBG1-Xms4g -Xmx4g -Xmn1g -XX:+UseG1GC
大数据处理8~16GB2~4GBParallel-Xms8g -Xmx8g -Xmn2g -XX:+UseParallelGC
实时交易(低延迟)16~32GB动态ZGC-Xms16g -Xmx16g -XX:+UseZGC

本质一句话:堆大小设置遵循”-Xms = -Xmx,新生代适中,总堆不超过物理内存 70%“的原则,根据场景动态调整。


Q3:GC 日志如何分析?有什么工具?高频

Section titled “Q3:GC 日志如何分析?有什么工具?”

GC 日志参数(Java 11+):

Terminal window
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=10,filesize=20m
# 参数解释:
# gc* → 记录所有 GC 相关日志
# file → 输出到文件
# time,uptime → 时间戳
# level,tags → 日志级别和标签
# filecount=10 → 保留最近 10 个日志文件
# filesize=20m → 每个文件最大 20MB

GC 日志示例:

[2025-01-15T10:30:15.123+0800][0.123s][info][gc] GC(0) Pause Young (Allocation Failure)
[2025-01-15T10:30:15.125+0800][0.125s][info][gc] GC(0) 24M->8M(256M) 2.345ms
# 解释:
# GC(0) → 第 0 次 GC
# Pause Young → 年轻代 GC(Minor GC)
# Allocation Failure → Eden 区满,触发 GC
# 24M->8M(256M) → GC 前 24MB,GC 后 8MB,堆总大小 256MB
# 2.345ms → GC 停顿时间
[2025-01-15T10:35:20.456+0800][5.456s][info][gc] GC(10) Pause Full (System.gc())
[2025-01-15T10:35:20.856+0800][5.856s][info][gc] GC(10) 200M->150M(256M) 400.123ms
# 解释:
# Pause Full → Full GC
# System.gc() → 显式调用 System.gc()
# 400.123ms → 停顿 400ms(较长)

分析工具:

1. GCeasy(在线工具):

上传 GC 日志 → 自动生成分析报告
├── GC 停顿时间分布
├── GC 原因统计
├── 内存使用趋势
└── 优化建议
地址:https://gceasy.io/

2. GCViewer(离线工具):

Terminal window
# 安装
brew install gcviewer
# 使用
gcviewer gc.log

3. 手动分析(jstat):

Terminal window
# 实时监控 GC
jstat -gcutil <pid> 1000 10
# 输出:
# S0 S1 E O M YGC YGCT FGC FGCT GCT
# 0.00 12.5 85.3 92.1 45.2 1234 12.345 5 3.456 15.801
# 分析:
# O(老年代) > 90% → 可能即将 Full GC
# FGC 快速增加 → 需立即排查
# FGCT / FGC = 单次 Full GC 平均耗时
# GCT = 总 GC 耗时,占比 = GCT / 运行时间

关键指标:

指标计算方式健康值不健康值
GC 吞吐量(运行时间 - GC 时间) / 运行时间> 95%< 90%
平均 GC 停顿总 GC 时间 / GC 次数< 100ms> 500ms
Full GC 频率Full GC 次数 / 运行时间< 1 次/天> 1 次/小时
老年代增长速度老年代占用增长趋势平稳快速增长

本质一句话:GC 日志分析关注停顿时间、频率、内存增长趋势,用 GCeasy 或 jstat 辅助,量化 GC 性能。


Q1:线上服务 CPU 突然飙到 100%,如何快速定位问题?实战

Section titled “Q1:线上服务 CPU 突然飙到 100%,如何快速定位问题?”

排查步骤(5 分钟定位法):

Terminal window
# Step 1: 确认 Java 进程(假设 PID=1234)
top -c # 找到 CPU 最高的 java 进程 PID
# Step 2: 找到该进程内 CPU 最高的线程
top -Hp 1234 # 找到 TID,如 1289
# Step 3: 将十进制 TID 转为十六进制
printf '%x\n' 1289 # 输出 509
# Step 4: 抓取线程快照,找到问题线程
jstack 1234 > /tmp/thread.dump
grep -A 30 "nid=0x509" /tmp/thread.dump

常见原因与解法:

现象原因解法
while(true) 死循环条件判断逻辑 bug修复代码,加退出条件
GC 线程占用高堆太小或内存泄漏,GC 频繁扩堆或排查泄漏
正则匹配 CPU 高灾难性回溯(catastrophic backtracking)优化正则,避免嵌套量词
线程大量竞争锁热点锁竞争减小锁粒度,用并发容器

实战案例:

场景:某 API 服务 CPU 持续 100%
排查:
1. top -c 找到进程 PID=1234
2. top -Hp 1234 找到线程 TID=1289
3. printf '%x\n' 1289 → 509
4. jstack 1234 | grep -A 30 "nid=0x509"
线程栈:
"http-nio-8080-exec-10" #10 prio=5 os_prio=0 tid=0x... nid=0x509 runnable
at com.example.service.UserService.process(UserService.java:123)
at com.example.controller.UserController.getUser(UserController.java:45)
定位代码:
// UserService.java:123
while (user.isActive()) { // 死循环,user.isActive() 一直为 true
process(user);
}
修复:
while (user.isActive() && !Thread.currentThread().isInterrupted()) {
process(user);
Thread.sleep(100); // 避免 CPU 100%
}

本质一句话:CPU 飙高排查是”找进程 → 找线程 → 抓栈 → 定位代码”,核心是 top -Hp + jstack


Q2:频繁 Full GC 如何排查?如何定位内存泄漏?实战

Section titled “Q2:频繁 Full GC 如何排查?如何定位内存泄漏?”

排查步骤:

Terminal window
# Step 1: 实时监控 GC
jstat -gcutil <pid> 1000 20
# 重点关注:
# O(老年代)持续增长 → 内存泄漏
# FGC(Full GC 次数)快速增加 → 需立即排查
# Step 2: 查看存活对象分布
jmap -histo:live <pid> | head -20
# 输出:实例数量 × 字节大小 × 类名
# 找到实例数最多的类
# Step 3: dump 堆快照(生产环境慎用,会 STW)
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
# Step 4: 用 Eclipse MAT 分析
# - 查看 Dominator Tree(哪些对象占用最多内存)
# - 找到 GC Root 到泄漏对象的引用链
# - 定位代码位置

MAT 分析流程:

1. 打开堆快照(heap.hprof)
2. 查看 Dominator Tree
└── 找到占用内存最大的对象
3. 右键对象 → Path to GC Roots → exclude weak/soft references
└── 找到谁在持有这个对象
4. 查看引用链
└── 定位代码位置
5. 分析泄漏原因
├── 静态集合无限增长
├── ThreadLocal 未 remove
├── 监听器未注销
└── 未关闭的资源

常见内存泄漏场景:

// 场景1:静态集合无限增长
static List<Object> cache = new ArrayList<>();
void addToCache(Object obj) {
cache.add(obj); // 只加不删
}
// 修复:定期清理或用 WeakHashMap
// 场景2:ThreadLocal 未 remove
ThreadLocal<BigObject> local = new ThreadLocal<>();
void process() {
local.set(new BigObject());
// ... 业务逻辑
// 忘记 local.remove()
}
// 修复:try-finally 确保 remove
// 场景3:监听器未注销
void registerListener() {
eventBus.register(this);
// 对象销毁时未 unregister
}
// 修复:在 @PreDestroy 或 finally 中注销

本质一句话:内存泄漏排查是”jstat 监控 → jmap 查看对象 → dump 堆快照 → MAT 分析 GC Root 路径”,核心是找到谁在持有泄漏对象。


Q3:使用 Arthas 如何在线诊断问题?实战

Section titled “Q3:使用 Arthas 如何在线诊断问题?”

Arthas 简介:

阿里开源的 Java 诊断工具,无需重启应用,在线排查问题:

Terminal window
# 启动 Arthas
java -jar arthas-boot.jar <pid>

常用命令:

1. 查看高 CPU 线程:

Terminal window
# 查看最忙的 3 个线程
thread -n 3
# 输出:
# "http-nio-8080-exec-10" cpu=85% ...
# at com.example.service.UserService.process(UserService.java:123)

2. 查看方法执行耗时:

Terminal window
# 追踪方法调用链和耗时
trace com.example.UserService getUserById
# 输出:
# `---[12.345ms] com.example.UserService:getUserById()
# +---[5.123ms] com.example.dao.UserDao:selectById()
# `---[3.456ms] com.example.cache.Cache:get()

3. 查看方法的参数和返回值:

Terminal window
# 监控方法调用
watch com.example.UserService getUserById "{params,returnObj}" -x 2
# 输出:
# method=com.example.UserService.getUserById
# params=[123]
# returnObj=User{id=123, name="Alice"}

4. 反编译线上代码:

Terminal window
# 反编译类,确认是否部署了正确的版本
jad com.example.UserService
# 输出反编译后的源码

5. 查看类加载信息:

Terminal window
# 查找加载某个类的 ClassLoader
classloader -c com.example.UserService
# 查看 JVM 已加载的类
sc -d com.example.*

6. 监控 JVM 状态:

Terminal window
# 查看 JVM 信息
jvm
# 查看 GC 统计
memory
# 查看线程死锁
thread -b

实战案例:

场景:API 响应慢,怀疑某个方法耗时过长
排查:
1. 启动 Arthas
java -jar arthas-boot.jar 1234
2. 追踪方法耗时
trace com.example.controller.UserController getUser
3. 发现瓶颈
`---[234.567ms] com.example.controller.UserController:getUser()
+---[200.123ms] com.example.service.UserService:getUserById()
| `---[180.456ms] com.example.dao.UserDao:selectById() ← 瓶颈!
`---[10.234ms] com.example.cache.Cache:get()
4. 定位问题
数据库查询慢 → 添加索引或优化 SQL
效果:API 响应时间从 234ms 降至 50ms

本质一句话:Arthas 是线上诊断神器,无需重启应用即可追踪方法耗时、监控参数、反编译代码,快速定位性能瓶颈。


JIT 编译核心知识点:

  1. JIT vs 解释执行:JIT 编译热点代码为机器码,运行速度接近 C/C++
  2. 分层编译:C1 快速编译,C2 深度优化,兼顾启动速度和运行性能
  3. 热点探测:方法调用计数器 + 回边计数器,OSR 优化循环
  4. 方法内联:消除调用开销,为后续优化创造条件
  5. 逃逸分析:栈上分配、标量替换、同步消除,减少 GC 压力
  6. 其他优化:常量折叠、死代码消除、循环优化、分支预测
  7. JVM 调优:目标驱动,优先调堆大小和收集器
  8. 堆大小设置:-Xms = -Xmx,新生代适中,总堆不超过物理内存 70%
  9. GC 日志分析:关注停顿时间、频率、内存增长趋势
  10. CPU 飙高排查:top -Hp + jstack,定位高 CPU 线程
  11. 内存泄漏排查:jstat 监控 → jmap dump → MAT 分析 GC Root
  12. Arthas 诊断:trace、watch、thread 等命令在线排查问题

面试必答点:

  • JIT 编译 vs 解释执行的区别与优势
  • 分层编译的 C1/C2 与编译路径
  • 热点探测机制与 OSR
  • 方法内联的原理与限制
  • 逃逸分析的三种优化(栈上分配、标量替换、同步消除)
  • JVM 调优方法论与参数配置
  • 堆大小设置的最佳实践
  • GC 日志分析的关键指标与工具
  • CPU 飙高排查的完整流程(top -Hp + jstack)
  • 内存泄漏排查的步骤与 MAT 使用
  • Arthas 常用命令与实战案例