四 类加载与字节码技术
Class文件结构
java
ClassFile {
u4 magic; // 0xCAFEBABE,魔数
u2 minor_version;
u2 major_version; // 52=Java8, 55=Java11, 61=Java17, 65=Java21, 69=Java25
u2 constant_pool_count;
cp_info constant_pool[...]; // 常量池:类名、方法名、字段名、字面量
u2 access_flags; // ACC_PUBLIC / ACC_FINAL / ACC_INTERFACE ...
u2 this_class;
u2 super_class;
u2 interfaces_count; u2 interfaces[];
u2 fields_count; field_info fields[];
u2 methods_count; method_info methods[];
u2 attributes_count; attribute_info attributes[]; // Code、LineNumberTable、LocalVariableTable...
}
-
魔数
cafebabe:4 字节,标识这是合法的 class 文件。 -
版本号 :
major_version决定运行所需的最低 JDK(低版本 JVM 无法运行高版本 class →UnsupportedClassVersionError)。 -
常量池 :从
#1开始,#0不使用;存放字面量与符号引用(类、字段、方法、MethodHandle、InvokeDynamic 等)。 -
反编译看结构:
javap -v -p Xxx.class(加上-c看字节码,-p看私有成员)。
字节码指令与执行引擎
栈帧里的两个核心结构:局部变量表(slot 数组,索引访问)与操作数栈(临时计算区)。
常见指令速记:
| 类别 | 指令 |
|---|---|
| 加载/存储 | iload/istore、aload/astore、bipush/sipush、ldc(常量池取值)、iconst_i |
| 运算 | iadd/isub/imul/idiv、iinc(直接改局部变量表,不经过操作数栈) |
| 类型转换 | i2l、i2c... |
| 对象 | new(分配空间并入栈引用)、dup(复制栈顶引用)、getfield/putfield、getstatic/putstatic |
| 方法调用 | invokestatic、invokespecial、invokevirtual、invokeinterface、invokedynamic |
| 控制转移 | if_icmpeq、goto、tableswitch/lookupswitch |
| 返回 | ireturn/areturn/return |
经典题:a++ 与 ++a 的字节码
java
int a = 10;
int b = a++ + ++a + a--;
// 结果:a = 11, b = 34
字节码视角:
java
0: bipush 10 → 栈:[10]
2: istore_1 → a=10
3: iload_1 → 压入 a 的当前值 10 栈:[10]
4: iinc 1, 1 → a 直接 +1 = 11(不经过栈) 栈:[10]
7: iinc 1, 1 → a = 12(++a 先自增)
10: iload_1 → 压入 12 栈:[10,12]
11: iadd → 10+12=22 栈:[22]
12: iload_1 → 压入 12(a-- 先取值) 栈:[22,12]
13: iinc 1, -1 → a = 11
16: iadd → 22+12=34
17: istore_2 → b=34
关键点:
iinc直接修改局部变量表;后置a++是先压旧值再自增,前置++a是先自增再压新值。
另一个循环陷阱(结果永远为 0):
java
for (int i = 0; i < 10; i++) { x = x++; } // x 保持 0
// iload x(旧值入栈)→ iinc x(x+1)→ istore x(把栈里的旧值写回)→ 自增被覆盖
方法调用与多态(超高频)
| 调用语句 | 字节码 | 说明 |
|---|---|---|
私有方法 test1() |
invokespecial |
编译期确定,无多态 |
private final test2() |
invokespecial |
本质是 private |
普通 public test3() |
invokevirtual |
运行时按对象实际类型分派 → 多态 |
静态方法 d.test4() |
invokestatic |
与对象无关;调用前会先 pop 掉栈顶的对象引用 |
| 接口方法 | invokeinterface |
运行时查 itable |
| Lambda | invokedynamic |
JDK 8 起,Lambda 的"翻译策略" |
多态的实现原理 :执行 invokevirtual 时 → 通过对象引用找到对象 → 解析对象头中的类型指针找到 Class → 查虚方法表 vtable(在类加载的链接阶段按重写规则生成好)→ 得到方法入口地址 → 执行。
new + dup 的配合 :new 分配空间并把引用压栈,dup 复制一份:一份给 invokespecial <init> 调用构造(消耗掉),一份给 astore 存到局部变量。
异常处理与 finally
-
try-catch用**异常表(Exception table)**实现:记录 起始 PC, 结束 PC, 处理 PC, 异常类型; -
finally的代码会被复制多份插入到所有可能的执行路径(正常路径 + 各异常路径),因此:-
finally中有return→ 以 finally 的返回值为准 ,且会吞掉异常; -
try中return i(i=10),finally里改i=20→ 返回 10(返回值已被暂存到另一个 slot,finally 修改无效)。
-
编译期处理(语法糖)
| 语法糖 | 编译后的真相 |
|---|---|
| 默认构造器 | 无构造器的类自动补 <init>() |
| 自动拆装箱 | Integer.valueOf() / intValue()(注意 Integer 缓存 -128~127) |
| 泛型 | 类型擦除 :编译后变 Object + 强制转型;必要时生成桥接方法(子类返回值是父类返回值的子类型时) |
| 变长参数 | String... → String[];foo() ≡ foo(new String[]{})(不是 null) |
| foreach | 数组 → 下标循环;Iterable → 迭代器 |
| switch-String | 两遍 switch:先按 hashCode + equals 映射为 byte,再按 byte 比较(不能为 null);hashCode 用于提速,equals 防冲突(如 "BM" 与 "C." 同 hash) |
| switch-Enum | 生成合成类,用 ordinal() 映射到数组下标 |
| 枚举类 | 编译为 final class Xxx extends Enum,每个枚举值是 static final 实例 |
| try-with-resources | 自动生成 finally { close() },且用 addSuppressed() 保留被压制的异常(资源需实现 AutoCloseable) |
| 匿名内部类 | 生成 Xxx$1 类;引用的局部变量必须 final/effectively final (值被拷贝成 val$x 字段,不允许变化) |
字符串 + |
JDK 8:StringBuilder;JDK 9+:invokedynamic + StringConcatFactory |
| record / sealed(JDK 16+) | 自动生成 equals/hashCode/toString/accessor;sealed 用 PermittedSubclasses 属性限制子类 |
类加载过程
加载 Loading → 链接 Linking(验证 → 准备 → 解析)→ 初始化 Initialization → 使用 → 卸载
1. 加载
-
通过类的全限定名获取二进制字节流(本地 jar、网络、动态生成、加密文件...);
-
把字节流的静态结构转为方法区的运行时数据结构 (HotSpot 中是 C++ 的
instanceKlass); -
在堆中生成
java.lang.Class对象(_java_mirror),作为访问方法区元数据的入口; -
父类未加载则先加载父类;加载与链接可能交替进行。
2. 链接
-
验证:文件格式、元数据、字节码(语义)、符号引用 4 项校验,保证安全;
-
准备 :为 static 变量分配内存并设零值;
-
static int a = 10;→ 准备阶段 a = 0,初始化阶段才赋 10; -
static final int a = 10;(编译期常量)→ 准备阶段就赋值 10; -
static final Object o = new Object();→ 引用类型,初始化阶段才赋值; -
JDK 7 起静态变量存在
_java_mirror(Class 对象,堆中)末尾,此前存在instanceKlass(方法区);
-
-
解析 :把常量池中的符号引用 替换为直接引用(类/字段/方法/接口方法的解析)。
3. 初始化 :执行 <clinit>(),即编译器按源码顺序合并所有 static 变量赋值与 static 代码块得到的方法;JVM 保证其线程安全(多线程下只有一个线程执行,其他阻塞)。
<clinit> vs <init>
<clinit>() |
<init>() |
|
|---|---|---|
| 内容 | 静态变量赋值 + static 代码块 | 实例变量赋值 + 实例代码块 + 构造方法体 |
| 时机 | 类加载的初始化阶段,只执行一次 | 每次 new 都执行 |
| 顺序 | 源码顺序 | 先父类 <init>(super),再按源码顺序执行实例赋值/代码块,最后执行构造方法体 |
| 线程安全 | JVM 保证 | 不保证 |
初始化触发 vs 不触发(必背)
| 会触发初始化 | 不会触发初始化 |
|---|---|
new、首次访问类的静态变量/静态方法 |
访问 static final 编译期常量(基本类型/字符串) |
| 子类初始化前先初始化父类 | Xxx.class(只是拿到 Class 对象) |
Class.forName("...")(默认 initialize=true) |
创建数组 new Xxx[0] |
反射 Class.forName、main 方法所在类 |
ClassLoader.loadClass()(只加载不初始化) |
| MethodHandle 解析出的静态方法调用等 | Class.forName(name, false, loader) |
易错点:
static final int N = 10是编译期常量,会被内联到调用方;改成static final int N = new Random().nextInt()就不再是常量,访问它会触发初始化。
加载器与双亲委派
JDK 8 的四层
| 类加载器 | 加载路径 | 说明 |
|---|---|---|
| Bootstrap(启动类) | JAVA_HOME/jre/lib(rt.jar 等) |
C++ 实现,getClassLoader() 返回 null |
| Extension(扩展类) | JAVA_HOME/jre/lib/ext |
上级为 Bootstrap |
| Application(应用/系统类) | classpath | 上级为 Extension;默认的线程上下文类加载器 |
| 自定义 | 自定义路径 | 上级为 Application |
JDK 9 起:Extension 被 Platform ClassLoader 取代,三层结构变为 Bootstrap / Platform / Application,且后两者不再继承
URLClassLoader;模块化后类的查找会先按模块归属委派。
双亲委派流程 :loadClass() 收到请求 → 先委派给父加载器 → 父加载器反馈无法完成(搜索范围内找不到)→ 才自己尝试 findClass()。
java
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name); // 1. 查缓存
if (c == null) {
try {
if (parent != null) c = parent.loadClass(name, false); // 2. 委派父类
else c = findBootstrapClassOrNull(name);
} catch (ClassNotFoundException e) { /* 父加载不了 */ }
if (c == null) c = findClass(name); // 3. 自己加载
}
return c;
}
}
为什么要双亲委派?
-
保证类的唯一性与安全性 (防止核心 API 被同名类替换,如自定义
java.lang.String无效); -
避免重复加载(父加载器已加载的类,子加载器直接复用)。
破坏双亲委派的经典场景(高频)
| 场景 | 原因 | 解决 |
|---|---|---|
| JDBC / SPI(JDK 6 之前) | 接口 java.sql.Driver 在 Bootstrap 加载,实现类在 classpath,父加载器无法访问子加载器的类 |
线程上下文类加载器 TCCL :Thread.currentThread().setContextClassLoader(),由 ServiceLoader 用它去加载实现(本质是"父委托子") |
| Tomcat | 多个 WebApp 需要隔离同名不同版本的类 | 自定义 WebAppClassLoader,先自己找再委派(与双亲委派相反);另有 Common / Catalina / Shared 分层 |
| OSGi / 热部署 | 需要模块级类隔离与动态替换 | 每个 Bundle 一个 ClassLoader,网状委派 |
| 历史兼容 | JDK 1.2 之前没有双亲委派 | findClass 的兼容设计 |
自定义类加载器的步骤
-
继承
ClassLoader; -
重写
findClass()(而不是loadClass()),这样才不会破坏双亲委派; -
读取类文件字节码;
-
调用
defineClass()生成 Class 对象; -
使用时调用
loadClass()。
判定"同一个类"的三要素:包名 + 类名 + 类加载器实例 都相同。所以两个不同 ClassLoader 加载的同名类
equals为 false,相互转换会ClassCastException。
运行期优化(JIT)
解释器 vs JIT
| 解释器 | JIT 编译器 | |
|---|---|---|
| 方式 | 逐条解释字节码 | 把热点代码编译为本地机器码,存入 Code Cache |
| 启动 | 快(无需编译) | 需要warm-up |
| 长期性能 | 低 | 高(可根据平台生成特定机器码) |
| 依据 | --- | 热点探测 + 运行时 profiling |
分层编译(Tiered Compilation,JDK 7+ 默认开启,5 层)
| 层级 | 内容 |
|---|---|
| 0 | 解释执行(Interpreter) |
| 1 | C1 编译,无 profiling |
| 2 | C1 + 基本 profiling(调用次数、回边次数) |
| 3 | C1 + 完全 profiling |
| 4 | C2(C2 编译优化程度最高:内联、循环展开、向量化...) |
执行效率大致:Interpreter < C1 < C2 。-XX:TieredStopAtLevel=1 可只用 C1(换取更快启动,常用于 Serverless)。
关键优化手段
-
方法内联(Inlining) :热点且不太长的方法,把代码"拷贝粘贴"到调用处,还能进一步触发常量折叠 。调试:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining、-XX:+PrintCompilation。 -
逃逸分析(Escape Analysis) (
-XX:+DoEscapeAnalysis,默认开启):-
对象不逃逸 (只在方法内使用)→ 可做标量替换 (
-XX:+EliminateAllocations):把对象拆成若干局部变量,不真正分配对象 ------ 注意 HotSpot 实际并没有做"栈上分配",而是更激进的标量替换,这一点常答错; -
对象不逃逸 → 锁消除 (
-XX:+EliminateLocks):synchronized(局部对象)直接被去掉。
-
-
字段优化 :方法内联后,成员变量的读取(如
array.length)可被优化为寄存器访问甚至常量。 -
反射膨胀(Inflation) :
Method.invoke前约 15 次用NativeMethodAccessorImpl(慢),之后 JIT 生成GeneratedMethodAccessor1字节码版(快)。可用sun.reflect.noInflation=true直接生成,sun.reflect.inflationThreshold改阈值。 -
Code Cache :JIT 产物存放区,太小会导致 "CodeCache is full" 并退化为解释执行(
-XX:ReservedCodeCacheSize)。 -
AOT(JDK 24/25):JEP 483(AOT 类加载与链接)、JEP 514/515(AOT 命令行便利 + 方法 profiling 缓存),把 warm-up 数据固化到缓存,显著缩短冷启动(Leyden 项目)。
类加载与字节码技术 面试回答
Q:双亲委派是什么?为什么要?怎么破坏? 三步走:讲清"自下而上委派、自上而下加载"的流程 → 讲两个好处(安全、去重)→ 举三个破坏例子(SPI/TCCL、Tomcat、OSGi/热部署)。
Q:类初始化顺序题 父类静态 → 子类静态 → 父类实例(实例变量/代码块 → 构造方法体) → 子类实例。静态只跑一次,实例每次 new 都跑
五 Java 内存模型与并发底层
注意:JMM(Java Memory Model)≠ JVM 内存结构。前者是并发语义规范(JSR-133),后者是运行时数据区划分。
JMM 的三大特性
线程 A ── 工作内存 ──┐
├── 主内存(共享变量)
线程 B ── 工作内存 ──┘
| 特性 | 含义 | 保障手段 |
|---|---|---|
| 原子性 | 一组操作要么全做要么全不做,中间不被打断 | synchronized(monitorenter/monitorexit)、CAS、原子类、Lock |
| 可见性 | 一个线程对共享变量的修改,其他线程能立刻看到 | volatile、synchronized、final、CAS |
| 有序性 | 程序执行顺序符合预期(禁止特定重排序) | volatile(内存屏障)、synchronized、happens-before |
为什么 i++ 不是原子操作?(字节码证据)
java
getstatic i // 读
iconst_1 // 准备 1
iadd // 加
putstatic i // 写回
4 条指令在多线程下交错 → 结果可能为正、负、零。
可见性反例(退不出的循环)
java
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> { while (run) { /* ... */ } }).start();
Thread.sleep(1000);
run = false; // 线程可能永远停不下来
}
原因:JIT 把 run 缓存到工作内存/寄存器(循环回边被优化为"只读一次")。解决:加 volatile。
冷知识:如果在循环体里加一句
System.out.println(),即使不加 volatile 也能停下来 ------ 因为println内部有synchronized,同步块会刷新工作内存。
volatile 的两层语义
-
可见性 :对 volatile 变量的写立即刷新到主存,读必须从主存取(底层通过
lock前缀指令 + 缓存一致性协议 + 内存屏障); -
有序性:禁止指令重排(JSR-133 的内存屏障规则):
-
写 volatile 前:store-store 屏障(前面的普通写不许重排到 volatile 写之后)
-
写 volatile 后:store-load 屏障
-
读 volatile 后:load-load / load-store 屏障
-
volatile 不保证原子性 :适合 一个写线程 + 多个读线程 (状态标志位),不适合 count++ 这类复合操作。
DCL 单例为什么必须加 volatile?
java
INSTANCE = new Singleton();
// 字节码:
// 0: new → 分配空间,拿到引用
// 3: invokespecial → 执行 <init>
// 7: putstatic → 引用赋给 INSTANCE
步骤 3 和 7 可能被重排:t1 先赋值(INSTANCE ≠ null)但对象还没初始化完 → t2 拿到半初始化对象 。加 volatile 禁止重排即可(JDK 5 之后 volatile 语义才真正完整生效)。
更好的写法:静态内部类(
Holder)或枚举单例,天然线程安全且无 DCL 的坑。
happens-before 规则(7 条 + 传递性)
-
程序次序规则:一个线程内,前面的操作 hb 后面的操作;
-
管程锁定规则:解锁 m 之前的写,对随后加锁 m 的线程可见;
-
volatile 规则:volatile 变量的写,对后续的读可见;
-
线程启动规则 :
Thread.start()前的写,对线程内可见; -
线程终止规则 :线程结束前的写,对其他线程(通过
join()/isAlive()得知结束)可见; -
中断规则 :
interrupt()前的写,对被中断线程感知后可见; -
对象终结 / 默认值规则:变量默认值(0/false/null)的写,对所有线程可见;
-
传递性:x hb y 且 y hb z ⇒ x hb z。
注意:这里的变量指成员变量或静态成员变量(共享变量),不含局部变量。
CAS 与原子类
java
// CAS 语义:认为值仍是 old 时,才更新为 new
while (true) {
int old = value;
int newValue = old + 1;
if (compareAndSwap(old, newValue)) break; // 失败则重试(自旋)
}
-
底层:
sun.misc.Unsafe的compareAndSwapInt/Long/Object,最终映射到 CPU 的cmpxchg指令(多核下加锁总线/缓存行)。 -
三大问题
-
ABA :值从 A→B→A,CAS 认为没变 → 用
AtomicStampedReference(版本戳)或AtomicMarkableReference解决; -
自旋开销:竞争激烈时大量重试空耗 CPU;
-
只能保证一个变量的原子操作 → 多变量用
AtomicReference或锁。
-
-
LongAdder优于AtomicLong的地方 :把热点计数拆成base+Cell[],不同线程打散到不同 Cell,最后求和 ------ 高并发计数场景吞吐高得多(代价是sum()不保证强一致)。 -
JDK 9+ 推荐用
VarHandle替代部分Unsafe用法(更安全,且支持 acquire/release/opaque 等内存序)。
synchronized 与锁优化
字节码层面 :同步代码块 → monitorenter / monitorexit(编译器会补一个异常表项确保异常时也释放);同步方法 → 方法 access_flags 上的 ACC_SYNCHRONIZED 标识。
对象头 Mark Word(64 位)与锁状态
| 锁状态 | 标志位 | 存储内容 |
|---|---|---|
| 无锁 | 01 | 哈希码、分代年龄 |
| 偏向锁 | 01(可偏向位=1) | 偏向线程 ID、epoch、分代年龄 |
| 轻量级锁 | 00 | 指向栈中锁记录(Lock Record)的指针 |
| 重量级锁 | 10 | 指向 ObjectMonitor 的指针 |
| GC 标记 | 11 | 转发指针等 |
锁升级路径(重要:版本差异)
无锁 ──(有线程进入同步块)──▶ 轻量级锁(CAS 抢锁) ──(CAS 失败/有竞争)──▶ 重量级锁(monitor, 阻塞)
▲
└── 旧版本(JDK 6~14):无锁 → 偏向锁 → 轻量级锁 → 重量级锁
-
轻量级锁:无竞争(线程错开访问)时,用 CAS 把 Mark Word 替换为指向栈帧锁记录的指针;重入时 CAS 失败但发现是自己 → 记一条"空锁记录"即可;
-
锁膨胀 :CAS 抢锁失败(别人已持有)→ 升级为重量级锁,竞争失败的线程进入
_EntryList阻塞; -
自旋优化 :重量级锁竞争时先自旋几次(JDK 6 后自适应自旋,根据历史成功率调整),成功则避免阻塞;单核无意义,多核才有价值(JDK 7 后不可手动关闭);
-
偏向锁(历史知识):JDK 6 引入,第一次用 CAS 把线程 ID 写入 Mark Word,之后同一线程重入不再 CAS。缺点:撤销偏向需要 STW、访问 hashCode 会撤销、与现代高并发应用形态不符。
-
JDK 15(JEP 374)默认关闭并标记废弃 ;JDK 18 起相关参数作废、功能不可再用;
-
所以 JDK 17/21/25 上,锁升级路径已变成"无锁 → 轻量级锁 → 重量级锁"。答"偏向锁→轻量级→重量级"而不说明版本,是近年面试最容易翻车的点。
-
synchronized 的其他优化
-
减少上锁时间:同步块尽量短;
-
减小锁粒度:一把锁拆多把(ConcurrentHashMap 分段/桶锁、LongAdder 的 Cell 数组、LinkedBlockingQueue 入队出队两把锁);
-
锁粗化 :多次连续加锁/解锁合并为一次(
new StringBuffer().append("a").append("b")会被粗化); -
锁消除:逃逸分析发现加锁对象不会逃逸 → 直接去掉同步;
-
读写分离 :
CopyOnWriteArrayList/CopyOnWriteArraySet(读无锁,写时复制,适合读多写少)。
synchronized vs ReentrantLock
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 层次 | JVM 关键字(对象头 + monitor) | JDK 层(AQS:volatile state + CAS + CLH 队列 + LockSupport) |
| 释放 | 自动 | 必须 finally { unlock(); } |
| 特性 | 非公平、可重入、不可中断 | 可公平/非公平、可 tryLock 超时、可响应中断、多个 Condition |
| 性能 | JDK 6 后与 Lock 基本持平 | 高竞争下更可控 |
虚拟线程(JDK 21+)对并发的影响
-
虚拟线程由 JVM 调度,挂载在少量平台线程(载体线程)上,阻塞操作(LockSupport.park)会卸载,不占用 OS 线程;
-
不要池化虚拟线程 (
Executors.newVirtualThreadPerTaskExecutor()); -
Pinning(固定)问题 :在
synchronized块内阻塞会固定住载体线程,削弱吞吐。-
JDK 24(JEP 491) 修复了 synchronized 导致的 pinning;
-
诊断:
-Djdk.tracePinnedThreads=full; -
实践:虚拟线程场景优先用
ReentrantLock,并避免ThreadLocal海量使用(可用 JDK 25 的 Scoped Values,JEP 506)。
-
Java内存模型和并发底层 面试回答
Q:volatile 和 synchronized 的区别?
volatile 只保证可见性和有序性,不保证原子性,不会阻塞,只能修饰变量;
synchronized 保证原子性、可见性、有序性,会发生阻塞,可修饰方法和代码块。
DCL 单例必须用 volatile 是因为要禁止对象引用赋值与构造初始化的重排序。
六 常用的排障与调优工具箱
命令行工具(JDK 自带)
| 工具 | 用途 | 常用写法 |
|---|---|---|
jps |
列出 Java 进程 | jps -lv |
jstat |
监控类加载/GC/编译统计 | jstat -gcutil <pid> 1000 10 |
jmap |
堆快照、堆内存概况 | jmap -heap <pid>;jmap -dump:format=b,file=heap.hprof <pid> |
jstack |
线程栈、死锁检测 | jstack -l <pid> > thread.txt |
jinfo |
查看/修改 JVM 参数 | jinfo -flags <pid> |
jcmd |
万能工具(推荐替代上述多数) | jcmd <pid> VM.flags / GC.heap_dump / Thread.print / VM.native_memory |
jconsole / jvisualvm |
图形化监控(VisualVM 需单独下载) | --- |
| JFR + JMC | 生产级低开销事件录制 | -XX:StartFlightRecording=...,JDK 25 增强(JEP 518/520) |
JDK 8 之后推荐统一用
jcmd,功能更全、更安全。
内存分析
-
MAT / JProfiler / JOverflow:分析 hprof,看 Dominator Tree、Leak Suspects;
-
jhat (已废弃)→ 用
jvisualvm或 MAT; -
jmap -histo :快速看对象数量排行(
jmap -histo:live <pid> | head -20); -
NMT(Native Memory Tracking) :
-XX:NativeMemoryTracking=detail+jcmd <pid> VM.native_memory summary,排查堆外/元空间/线程栈的真实占用。
线上问题排查标准流程
Case 1:CPU 占用飙高
java
top # 找到高 CPU 的进程 pid
top -Hp <pid> # 找到高 CPU 的线程 tid
printf '%x\n' <tid> # tid 转 16 进制
jstack <pid> | grep -A 30 <nid_hex> # 定位到具体线程栈和代码行号
# 常见原因:死循环、正则回溯、大量 GC、频繁线程上下文切换
Case 2:程序不响应 / 疑似死锁
java
jstack <pid> # 搜索 "deadlock"、"BLOCKED"、"waiting to lock"
jcmd <pid> Thread.print -l # 等价方式,带锁信息
Case 3:内存占用高 / OOM
java
jmap -heap <pid> # 各区占用概况
jstat -gcutil <pid> 1000 # 观察 GC 频率与回收效果
jmap -dump:format=b,file=heap.hprof <pid> # 导出快照(注意会 STW,生产需摘流量)
# MAT 打开 → Leak Suspects → Dominator Tree → 找到持有链(GC Root 路径)
Case 4:接口偶发慢 / GC 抖动
java
# 开 GC 日志(JDK9+)
-Xlog:gc*:file=/logs/gc.log:time,uptime:filecount=10,filesize=50m
# 用 GCeasy / GCViewer 分析:看吞吐量、平均/最大暂停、分配速率、晋升速率
Case 5:容器内被 OOM-Killer 杀掉,但堆监控不高
典型是堆外内存:
元空间、直接内存、线程栈、Code Cache、JNI、gzip/zip native 内存。 排查:
-XX:NativeMemoryTracking=detail+jcmd VM.native_memory;设置-XX:MaxMetaspaceSize、-XX:MaxDirectMemorySize、-Xss,并用-XX:MaxRAMPercentage让 JVM 感知容器限制。
参考资料