🔬 字节码 & 调优
class 文件结构 · 字节码指令与 javap · jps/jstat/jmap/jstack/Arthas · 线上问题排查实战
1. class 文件的结构?
class 文件是一组以 8 字节为单位的二进制流,无分隔符。主要结构:
- 魔数(Magic Number):前 4 字节
CAFEBABE,识别文件类型 - 版本号:minor_version + major_version(如 52 = Java 8,61 = Java 17)
- 常量池(Constant Pool):最大的部分,存所有字面量和符号引用(类名、方法名、字段名等)
- 访问标志:public/final/abstract/interface 等修饰符
- 类索引、父类索引、接口索引:指向常量池中的类描述
- 字段表、方法表:各成员的描述,方法表里就是字节码指令
- 属性表:附加信息(Code 属性存方法体、Exceptions、LineNumberTable 等)
用 javap 查看字节码
# javap 是 JDK 自带的反汇编工具(输出字节码指令,不是 Java 源码)
javap -v Hello.class # 详细:常量池、方法字节码、行号表
javap -c Hello.class # 只反汇编字节码指令
javap -p Hello.class # 包含 private 成员
// 源代码
public static int add(int a, int b) {
return a + b;
}
// 字节码(javap -c 输出)
0: iload_0 // 把局部变量表第 0 个 int 压栈(a)
1: iload_1 // 压入 b
2: iadd // 弹出两数相加,结果压栈
3: ireturn // 返回栈顶 int
🎯 面试要点
- 符号引用 → 直接引用 的解析就发生在类加载"解析"阶段
- class 版本号不兼容报 UnsupportedClassVersionError(编译版本高于运行版本)
- 字节码是 JVM 的指令集(类似汇编),JIT 会把热点字节码编译成机器码
2. 常用 JVM 诊断工具怎么用?
JDK 自带工具全家桶
jps -l # 列出 Java 进程(拿 pid)
jps -v # 附带 JVM 参数
jstat -gc <pid> 1000 # 每秒输出 GC 统计(各区容量、GC 次数、耗时)
jstat -gcutil <pid> 1000 # 输出各区使用率百分比,看瓶颈更快
jmap -heap <pid> # 堆配置与当前使用概况
jmap -histo <pid> # 堆内对象直方图(找大对象/泄漏源头)
jmap -dump:format=b,file=x.hprof <pid> # 堆转储,配合 MAT 分析
jstack <pid> # 线程快照(找死锁、卡住的线程、CPU 高的线程)
jstack -l <pid> # 附带锁信息(找到 BLOCKED 的锁持有链)
jcmd <pid> GC.heap_info # 新一代全能工具,JDK8+ 推荐
jinfo <pid> # 查看 JVM 参数
Arthas(阿里开源,生产排查神器):在线诊断不用重启——dashboard(实时面板)、thread -n 3(最忙的线程)、trace(方法耗时链路)、watch(观测方法入参返回值)、jad(反编译线上代码)、sc/sm(查类/方法)。
🎯 面试要点
- jstat -gcutil 三个关键指标:E(Eden)、O(Old)、FGC(Full GC 次数)
- jmap dump 大堆会卡住应用(STW 级别),生产慎用;优先用 jcmd GC.heap_dump 或 Arthas heapdump
- jstack 线程状态关键字:RUNNABLE/BLOCKED/WAITING/TIMED_WAITING + "waiting for monitor"(等锁)
3. 线上 CPU 飙高 100%,怎么排查?(高频实战题)
top找到 CPU 最高的进程 PIDtop -Hp <pid>找到进程内 CPU 最高的线程号 TIDprintf "%x\n" <TID>把十进制线程号转十六进制 nidjstack <pid> | grep -A 20 "nid=0x..."定位到具体线程和代码行- 看到代码后分析:死循环?正则回溯?GC 线程异常?锁竞争自旋?
完整命令链(Linux)
top # ① 找高 CPU 进程
top -Hp 12345 # ② 进程内找高 CPU 线程 TID(如 15250)
printf "%x\n" 15250 # ③ 得到 3b92
jstack 12345 | grep -A 30 "0x3b92" # ④ 线程栈,定位代码
# 更省事:Arthas
./as.sh <pid>
dashboard # 直接看各线程 CPU,不必转进制
thread -n 3 # 最忙的 3 个线程及栈
🎯 面试要点
- 全程只读排查,无侵入,生产可用
- 死循环常见原因:while(true) 无退出条件、HashMap 并发 put(JDK7 死链)、正则灾难性回溯
- CPU 高不一定是业务线程:GC 线程(-XX:+UseG1GC 的 G1 Concurrent Marking)也会占 CPU
4. 内存泄漏如何排查?
先判断是泄漏还是容量不足:
- Full GC 后老年代占用不下降,且持续增长 → 泄漏
- Full GC 后能下降但频率高 → 堆太小或对象过大
jstat -gcutil <pid>观察 O 区(老年代)趋势jmap -histo:live <pid>多次采样对比:某类对象数量只增不减 → 泄漏源头jmap -dump:format=b,file=heap.hprof <pid>导出堆- MAT / jvisualvm 分析:Dominator Tree(支配树)找大对象;Leak Suspects 直接给嫌疑报告;GC Roots 路径看谁还引用着它
常见泄漏场景:静态集合只增不减(缓存忘清)、ThreadLocal 用完不 remove(线程池线程长活)、监听器/回调未注销、连接/IO 流未关闭(间接泄漏)、类加载器泄漏(热部署)
🎯 面试要点
- MAT 三大武器:Leak Suspects / Dominator Tree / Path to GC Roots(排除软弱虚引用)
- 生产 dump 前评估:大堆 dump 可能 OOM 加剧,优先 Arthas heapdump 或夜间操作
- 排查泄漏的代码侧技巧:Java Flight Recorder(JFR)持续采样,不用停机
5. 线程死锁如何发现和定位?
死锁四条件:互斥、持有并等待、不可剥夺、循环等待。定位方法:
jstack <pid>输出末尾会有Found one Java-level deadlock的完整报告,直接告诉你两个线程互相等待的锁- 或者观察:多个线程都处于
BLOCKED且都在 "waiting for monitor lock" - Arthas:
thread -b直接找阻塞其他线程的线程(blocking thread)
死锁代码示例
Object lockA = new Object();
Object lockB = new Object();
// 线程 1:先 A 后 B;线程 2:先 B 后 A → 循环等待,死锁
new Thread(() -> { synchronized (lockA) {
synchronized (lockB) { ... } } }).start();
new Thread(() -> { synchronized (lockB) {
synchronized (lockA) { ... } } }).start();
🎯 面试要点
- 预防:按固定顺序加锁、锁超时(tryLock)、尽量缩小锁粒度
- jstack 的 deadlock 报告是检测死锁最快的官方手段