JVM常见核心知识梳理02 类加载 字节码技术 双亲委派 Java内存模型 JMM 并发底层 volatile synchronized 常见排障与调优工具

四 类加载与字节码技术

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/istoreaload/astorebipush/sipushldc(常量池取值)、iconst_i
运算 iadd/isub/imul/idiviinc直接改局部变量表,不经过操作数栈
类型转换 i2li2c...
对象 new(分配空间并入栈引用)、dup(复制栈顶引用)、getfield/putfieldgetstatic/putstatic
方法调用 invokestaticinvokespecialinvokevirtualinvokeinterfaceinvokedynamic
控制转移 if_icmpeqgototableswitch/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 的代码会被复制多份插入到所有可能的执行路径(正常路径 + 各异常路径),因此:

    1. finally 中有 return以 finally 的返回值为准 ,且会吞掉异常

    2. tryreturn 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/librt.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;
    }
}

为什么要双亲委派?

  1. 保证类的唯一性与安全性 (防止核心 API 被同名类替换,如自定义 java.lang.String 无效);

  2. 避免重复加载(父加载器已加载的类,子加载器直接复用)。

破坏双亲委派的经典场景(高频)

场景 原因 解决
JDBC / SPI(JDK 6 之前) 接口 java.sql.Driver 在 Bootstrap 加载,实现类在 classpath,父加载器无法访问子加载器的类 线程上下文类加载器 TCCLThread.currentThread().setContextClassLoader(),由 ServiceLoader 用它去加载实现(本质是"父委托子")
Tomcat 多个 WebApp 需要隔离同名不同版本的类 自定义 WebAppClassLoader先自己找再委派(与双亲委派相反);另有 Common / Catalina / Shared 分层
OSGi / 热部署 需要模块级类隔离与动态替换 每个 Bundle 一个 ClassLoader,网状委派
历史兼容 JDK 1.2 之前没有双亲委派 findClass 的兼容设计

自定义类加载器的步骤

  1. 继承 ClassLoader

  2. 重写 findClass()(而不是 loadClass(),这样才不会破坏双亲委派;

  3. 读取类文件字节码;

  4. 调用 defineClass() 生成 Class 对象;

  5. 使用时调用 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
可见性 一个线程对共享变量的修改,其他线程能立刻看到 volatilesynchronized、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 的两层语义

  1. 可见性 :对 volatile 变量的写立即刷新到主存,读必须从主存取(底层通过 lock 前缀指令 + 缓存一致性协议 + 内存屏障);

  2. 有序性:禁止指令重排(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 条 + 传递性)

  1. 程序次序规则:一个线程内,前面的操作 hb 后面的操作;

  2. 管程锁定规则:解锁 m 之前的写,对随后加锁 m 的线程可见;

  3. volatile 规则:volatile 变量的写,对后续的读可见;

  4. 线程启动规则Thread.start() 前的写,对线程内可见;

  5. 线程终止规则 :线程结束前的写,对其他线程(通过 join()/isAlive() 得知结束)可见;

  6. 中断规则interrupt() 前的写,对被中断线程感知后可见;

  7. 对象终结 / 默认值规则:变量默认值(0/false/null)的写,对所有线程可见;

  8. 传递性: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.UnsafecompareAndSwapInt/Long/Object,最终映射到 CPU 的 cmpxchg 指令(多核下加锁总线/缓存行)。

  • 三大问题

    1. ABA :值从 A→B→A,CAS 认为没变 → 用 AtomicStampedReference(版本戳)或 AtomicMarkableReference 解决;

    2. 自旋开销:竞争激烈时大量重试空耗 CPU;

    3. 只能保证一个变量的原子操作 → 多变量用 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 的其他优化

  1. 减少上锁时间:同步块尽量短;

  2. 减小锁粒度:一把锁拆多把(ConcurrentHashMap 分段/桶锁、LongAdder 的 Cell 数组、LinkedBlockingQueue 入队出队两把锁);

  3. 锁粗化 :多次连续加锁/解锁合并为一次(new StringBuffer().append("a").append("b") 会被粗化);

  4. 锁消除:逃逸分析发现加锁对象不会逃逸 → 直接去掉同步;

  5. 读写分离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 感知容器限制。

参考资料

黑马程序员JVM完整教程,Java虚拟机快速入门,全程干货不拖沓

Java Guide JVM 面试题总结:内存区域、垃圾回收、类加载与问题排查

相关推荐
Shan12051 小时前
经典算法题示例与详解:飞地的数量(二)
java·数据结构·算法
一嘴一个橘子1 小时前
java - redis 缓存雪崩
java
liliangcsdn1 小时前
IC计算-前向/远期收益计算和代码示例
开发语言·python·pandas
小溪学编程1 小时前
Java ListIterator 接口详解:双向遍历与列表修改的利器
java·开发语言·windows
影视飓风TIM1 小时前
C++11 新特性:Lambda表达式、std::function与std::bind
开发语言·c++
得物技术1 小时前
企业级 MultiAgent 落地:Plan 模式与主子 Agent 协作|得物技术
java·程序员·架构
程序员清风1 小时前
为什么不推荐走Agent开发?
java·后端·面试
我命由我123451 小时前
Compose Codelab 学习 - Jetpack Compose 中的状态
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
土司大王1 小时前
LeetCode hot100——全排列
java·算法·leetcode