> 技术文档 > JVM 核心内容

JVM 核心内容


JVM 类加载机制详解

1. 什么是类加载

类加载(Class Loading)是指JVM将类的字节码(.class 文件)加载到内存,并为之创建Class对象的过程。Java 程序运行时,只有被加载到内存中的类才能被使用。

2. 类加载的生命周期

JVM对类的处理分为以下几个阶段:

  1. 加载
    • 通过类的全限定名(包名+类名)查找并加载 class 文件的字节流到内存。
    • 生成对应的 java.lang.Class 对象。     
  2. 验证: 校验字节码文件的正确性、安全性。
  3. 准备:为类的静态变量分配内存,并设置默认初始值(不会执行静态代码块)
  4. 解析:将常量池中的符号引用替换为直接引用(如方法、字段等的内存地址)
  5. 初始化:执行类的静态初始化块和静态变量的初始化赋值
  6. 使用: 类被真正使用(如实例化、调用静态方法等)
  7. 卸载:类被垃圾回收,Class对象被回收(很少发生,通常是自定义ClassLoader加载的类才会被卸载)

3. 类加载器(ClassLoader)

JVM 通过类加载器来实现类的加载。类加载器有分层结构,主要有三种:

1. 启动类加载器(Bootstrap ClassLoader)

  • 加载 JDK 的核心类库($JAVA_HOME/lib 下的类,如 rt.jar)。
  • 由 C++ 实现,JVM 自己的一部分。

2. 扩展类加载器(Extension ClassLoader)

  • 加载 JDK 扩展目录($JAVA_HOME/lib/ext)下的类。

3. 应用类加载器(Application ClassLoader)

  • 加载用户 classpath 下的类(开发者写的代码)。

4. 自定义类加载器

  • 用户可以继承 java.lang.ClassLoader 实现自己的类加载逻辑。
  • 自定义类加载器默认还是会先走双亲委派模型(即先让父加载器尝试加载)。
  • 只有父加载器找不到时,才会调用你重写的 findClass。

4. 双亲委派模型(Parent Delegation Model)

核心思想:

类加载请求会先委托给父加载器,只有父加载器找不到,才由当前加载器尝试加载。

流程:

  1. 当前类加载器收到加载请求。
  2. 先让父加载器尝试加载。
  3. 父加载器再往上递归,直到 Bootstrap ClassLoader。
  4. 如果父加载器都找不到,才由当前加载器加载。

优点:

  • 避免重复加载。
  • 保证核心类库的安全性(比如你不能伪造 java.lang.String)。

代码:

protected Class loadClass(String name, boolean resolve) { synchronized (getClassLoadingLock(name)) { // 1. 检查类是否已加载 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) {  // 2. 委派给父加载器  c = parent.loadClass(name, false); } else {  // 3. 没有父加载器则使用启动类加载器  c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {} if (c == null) { // 4. 父加载器无法加载时自己尝试加载 c = findClass(name); } } return c; }}

5. 代码示例

public class Test { static { System.out.println(\"Test类被初始化\"); } public static void main(String[] args) throws Exception { Class clazz = Class.forName(\"Test\"); // 触发类加载和初始化 }}

6. 常见面试/考点

  1. 什么时候会触发类初始化?:
  • new 对象、调用静态方法/字段、反射、子类初始化会先初始化父类等。
  1. 如何自定义类加载器?:   
1. 继承java.lang.ClassLoader 2. 重写findClass()方法 3. 调用defineClass()方法将字节数组转换为Class对象 
public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class findClass(String name) throws ClassNotFoundException { // 1. 构造 class 文件的绝对路径 String fileName = classPath + \"/\" + name.replace(\'.\', \'/\') + \".class\"; try { // 2. 读取 class 文件的字节码 byte[] classBytes = Files.readAllBytes(Paths.get(fileName)); // 3. 调用 defineClass,把字节码转换为 Class 对象 return defineClass(name, classBytes, 0, classBytes.length); } catch (IOException e) { throw new ClassNotFoundException(\"类未找到: \" + name, e); } }}
  1. 双亲委派模型的好处?:

1. 安全性

  • 保证 Java 核心类库不会被篡改或伪造。

  • 比如:你不能自己写一个 java.lang.String 类并被应用加载器加载,因为加载请求会被委托给 Bootstrap ClassLoader(引导类加载器),只有它能加载核心类库。

2. 避免类的重复加载

  • 每个类只会被加载一次(由同一个类加载器)。

  • 如果父加载器已经加载过某个类,子加载器不会重复加载,节省内存,避免冲突。

3. 保证类的一致性

  • 保证同一个类在 JVM 中的唯一性(由“类的全限定名+加载它的类加载器”唯一确定)。

  • 避免出现“同名不同类”的问题,防止类型转换异常(ClassCastException)。

4. 易于维护和扩展

  • 类加载器之间职责分明,结构清晰。

  • 应用开发者只需关注自己的类加载器,不用担心核心类库的加载细节。

5. 有利于模块化和隔离

  • 不同的类加载器可以加载不同来源的类,实现模块隔离(如 Tomcat、OSGi、J2EE 容器等)

JVM 内存模型

1. 什么是 Java 内存模型?

Java 内存模型(JMM, Java Memory Model)是 Java 虚拟机规范中定义的一套关于多线程读写共享变量的规则和约定,它规定了:

  • 变量的存储方式 (变量存储在主内存,线程有自己的工作内存(缓存/寄存器))
  • 线程之间如何可见和交互这些变量 (线程对变量的操作必须先从主内存拷贝到工作内存,操作后再写回主内存)
  • 如何保证并发下的可见性、有序性和原子性

1.1如何保证并发下的可见性、有序性和原子性

1.可见性(Visibility)定义:一个线程对共享变量的修改,能及时被其他线程看到。

保证方法:

  • volatile 关键字

        修饰变量后,写操作会立即刷新到主内存,读操作会从主内存读取,保证可见性。

        例:volatile boolean flag = false;

  • synchronized 关键字

        进入和退出同步块时,会将工作内存和主内存同步,保证可见性。

  • Lock(如 ReentrantLock)

        加锁和解锁过程也会有内存屏障,保证可见性。

  • final 关键字

        保证对象在构造完成后,其他线程能看到其最终状态。

2. 有序性(Ordering)定义:程序执行的顺序和代码顺序一致(JMM 允许部分重排序,但通过 happen-before 规则保证关键操作的有序性)。

保证方法:

  • volatile 关键字

        禁止特定的指令重排序(volatile 写之前的操作不会被重排序到写之后,volatile 读之后的操作不会被重排序到读之前)。

  • synchronized 关键字

        进入和退出同步块时有内存屏障,保证同步块内外的操作有序。

  • happen-before 规则

        Java 内存模型定义的先行发生关系,保证关键操作的有序性。

3. 原子性(Atomicity)定义:一个操作要么全部完成,要么全部不做,不会被线程切换中断。

保证方法:

  • synchronized 关键字

        同步块内的操作具有原子性。

  • Lock(如 ReentrantLock)

  • 原子类(如 AtomicInteger、AtomicReference)

        J.U.C 包下的原子类通过 CAS(Compare-And-Swap)实现原子操作。

  • 局部变量

        局部变量在线程栈上,天然具有原子性(不共享)。

JMM 的核心目标是屏蔽各种硬件和操作系统的内存访问差异,让 Java 程序在不同平台下有一致的并发语义。

2. JMM 的主要内容

1. 主内存与工作内存

  • 主内存(Main Memory):所有线程共享的内存区域,Java 中的实例变量、静态变量都存储在这里。
  • 工作内存(Working Memory):每个线程私有的内存区域,存储了该线程使用到的变量的主内存副本。

线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接操作主内存。

2. 线程间通信

  • 一个线程对共享变量的修改,必须刷新回主内存,其他线程才能看到。
  • 线程间变量的可见性依赖于主内存的同步。

3. JMM 的三大特性

1. 原子性(Atomicity)

  • 一个操作要么全部完成,要么全部不做,不会被线程切换中断。
  • Java 基本数据类型的读写是原子的(long 和 double 除外,JDK8 以后也保证了原子性)。

2. 可见性(Visibility)

  • 一个线程对共享变量的修改,能及时被其他线程看到。
  • 关键字 volatile、synchronized、final 都能保证可见性。

3. 有序性(Ordering)

  • 程序执行的顺序按照代码的先后顺序执行(编译器和 CPU 可能会优化重排序)。
  • synchronized 和 volatile 可以部分禁止重排序。

4. JMM 的“八大操作”

JMM 定义了线程与主内存之间的交互有8种原子操作:

  • lock(锁定)
  • unlock(解锁)
  • read(读取主内存到工作内存)
  • load(把read的值放入工作内存变量副本)
  • use(工作内存变量用于计算)
  • assign(将计算结果赋值给工作内存变量)
  • store(把工作内存变量写回主内存)
  • write(将store的值写入主内存变量)

5. 关键字与 JMM

  • volatile:保证可见性和禁止指令重排序(部分有序性),但不保证原子性。
  • synchronized:保证原子性、可见性和有序性。
  • final:保证初始化后的可见性。

6. 经典问题

  • 可见性问题:一个线程修改了变量,另一个线程看不到。
public class VisibilityDemo { static boolean running = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (running) { // do something } System.out.println(\"Thread stopped.\"); }); t.start(); Thread.sleep(1000); running = false; // 主线程修改变量 System.out.println(\"Main thread set running = false\"); }}/**可能结果:主线程已经把 running 设为 false,但子线程可能永远不会停止,因为它看不到主线程的修改(JVM 缓存了变量)。解决方法:用 volatile 修饰变量:static volatile boolean running = true;**/
  • 有序性问题:指令重排序导致并发 bug。
/**可能结果:按照直觉,x 和 y 至少有一个应该是 1,但实际可能都为 0。这是因为 a=1 和 x=b 可能被重排序,b=1 和 y=a 也可能被重排序,导致两个线程都在对方赋值前读取了初始值。解决方法:用 volatile 或 synchronized 保证有序性。*/public class ReorderDemo { static int a = 0, b = 0; static int x = 0, y = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i  { a = 1; x = b; }); Thread t2 = new Thread(() -> { b = 1; y = a; }); t1.start(); t2.start(); t1.join(); t2.join(); if (x == 0 && y == 0) { System.out.println(\"出现了 x=0, y=0\"); break; } } }}
  • 原子性问题:多个线程同时操作同一个变量,导致数据不一致。
/**期望结果:1000 * 1000 = 1,000,000实际结果:通常远小于 1,000,000,因为 count++ 不是原子操作,多个线程同时读写导致丢失。解决方法:用 synchronized 或 AtomicInteger 替代。*/public class AtomicityDemo { static int count = 0; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[1000]; for (int i = 0; i  { for (int j = 0; j < 1000; j++) {  count++; // 非原子操作 } }); threads[i].start(); } for (Thread t : threads) t.join(); System.out.println(\"count = \" + count); }}

重排序

1. 什么是重排序(Reordering)

重排序是指:为了提高程序执行效率,编译器和处理器在不影响单线程语义的前提下,可以对指令的执行顺序进行优化调整。

  • 编译器优化重排序:编译器在生成字节码或机器码时改变指令顺序。
  • 处理器重排序:CPU 在执行时,为了流水线、乱序执行等优化,也可能改变指令实际执行顺序。

注意:重排序不会影响单线程程序的最终结果,但在多线程环境下,可能导致线程间的可见性和有序性问题,产生并发 bug。

2. 什么是 happen-before(先行发生)原则

happen-before 是 Java 内存模型(JMM)中定义的多线程间操作的有序性规则,用来保证某些操作的结果对其他线程可见。

核心含义:

如果操作A happen-before 操作B,那么A的结果对B可见,且A的执行顺序在B之前。

happen-before 的主要规则

  1. 程序顺序规则

  • 在一个线程内,代码的执行顺序,前面的操作 happen-before 后面的操作。
  1. 监视器锁规则(synchronized)

  • 对一个锁的解锁 happen-before 之后对同一个锁的加锁。
  1. volatile 变量规则

  • 对一个 volatile 变量的写操作 happen-before 后面对同一个变量的读操作。
  1. 传递性

  • 如果A happen-before B,B happen-before C,则A happen-before C。
  1. 线程启动规则

  • 线程A启动线程B(即调用B.start()),则A中对共享变量的修改对B可见。
  1. 线程终结规则

  • 线程A等待线程B结束(如B.join()),则B中对共享变量的修改对A可见。
  1. 线程中断规则

  • 对线程interrupt() happen-before 检测到中断(isInterrupted())。
  1. 对象终结规则        

  • 对象的构造函数执行结束 happen-before 该对象的 finalize() 方法开始

3. 重排序与 happen-before 的关系

  • 重排序可能导致多线程下的可见性和有序性问题。
  • happen-before 规则定义了哪些操作之间必须保证“先行发生”,JVM 和编译器必须禁止这些操作之间的重排序。

4. volatile 的作用

  • volatile 关键字可以禁止对该变量的读写操作与前后的普通操作发生重排序。
  • 保证写入 volatile 变量之前的操作,对其他线程可见。

5. 面试常用例子

双重检查锁(DCL)单例模式:

/***2. new Singleton() 实际做了什么?instance = new Singleton(); 这行代码不是原子操作,它大致可以分为三步:分配内存:为 Singleton 分配内存空间调用构造方法:初始化 Singleton 对象将 instance 指向分配的内存地址(此时 instance 不为 null)3. 指令重排序的风险JVM 和 CPU 为了优化性能,可能会对上述三步进行重排序,比如:步骤1(分配内存)步骤3(将 instance 指向内存地址)步骤2(调用构造方法初始化)也就是说,instance 已经不为 null,但对象还没初始化完成4. 多线程下的危险场景假设线程A和线程B同时进入getInstance():线程A进入,发现instance == null,进入同步块。线程A执行到instance = new Singleton();,发生了重排序,先把 instance 指向了内存地址(步骤3),但对象还没初始化(步骤2还没执行)。线程B进入,发现instance != null,直接返回 instance。线程B拿到的是一个还没初始化完成的对象,使用时就会出错(比如成员变量为默认值,甚至抛出异常)。5. 解决办法用 volatile 修饰 instance:**/public class Singleton {//volatile 保证 instance 的写操作和读操作之间有 happen-before 关系,防止重排序导致的“半初始化”问题。 private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) {  instance = new Singleton(); // 可能发生指令重排序 } } } return instance; }}

6. 总结

  • 重排序:编译器/CPU 为优化性能而调整指令顺序。
  • happen-before:JMM 规定的多线程操作间的有序性约束,保证数据可见性和正确性。
  • volatile/synchronized 等关键字可以建立 happen-before 关系,防止重排序带来的并发问题。

volatile

volatile 修饰的变量可以防止特定的重排序,但不是所有重排序都被禁止。

1. volatile 的“禁止重排序”语义

  • 写操作:对一个 volatile 变量的写操作,之前的所有操作在内存语义上都不能被重排序到 volatile 写之后。
  • 读操作:对一个 volatile 变量的读操作,之后的所有操作在内存语义上都不能被重排序到 volatile 读之前。

简化理解:

  • volatile 写之前的操作不能被重排序到 volatile 写之后。
  • volatile 读之后的操作不能被重排序到 volatile 读之前。

2. volatile 不能禁止的重排序

  • volatile 变量之间的操作,JVM 只保证可见性和部分有序性。
  • 普通变量之间的操作,如果没有和 volatile 变量的读写发生依赖,仍然可能被重排序。

3. 例子说明

int a = 0;volatile int v = 0; //a = 1; //这里,a = 1 不能被重排序到 v = 1 之后v = 1;volatile int v = 0;int a = 0; //这里,a = 1 不能被重排序到 v = 1 之前v = 1;a = 1;

4. volatile 不能保证原子性

  • volatile 只能保证可见性和部分有序性,不能保证原子性。
  • 例如:count++ 不是原子操作,即使 count 是 volatile 修饰的,也不能保证线程安全。

5. 总结

  • volatile 变量的读写会建立“内存屏障”,禁止特定的重排序。
  • 但不是所有重排序都被禁止,普通变量之间的操作仍可能被重排序。
  • volatile 主要用于保证多线程下的可见性和部分有序性,不是万能的并发同步工具。
  • volatile 只保证与它相关的操作的有序性,普通变量之间如果没有和 volatile 变量的读写依赖,仍然可能被重排序。
  • 这就是为什么在并发编程中,不能仅靠 volatile 保证所有操作的顺序。
  • 普通变量之间的操作,如果没有和 volatile 变量的读写发生依赖,仍然可能被重排序。

内存屏障

1. 什么是内存屏障?

内存屏障是一种用于控制 CPU 或编译器对内存操作顺序的特殊指令。

它的作用是禁止特定的读写操作被重排序,从而保证多线程环境下的可见性和有序性。


2. 为什么需要内存屏障?

  • 现代 CPU 和编译器为了优化性能,会对指令进行重排序。
  • 在单线程下没问题,但在多线程下,重排序可能导致数据不一致、可见性问题。
  • 内存屏障就是用来强制某些操作的执行顺序,保证并发安全。

3. 内存屏障的类型

常见的内存屏障有以下几种:

类型 作用 Store Barrier(写屏障) 保证屏障前的写操作对其他处理器可见,不能被重排序到屏障后面 Load Barrier(读屏障) 保证屏障后的读操作不会被重排序到屏障前面 Full Barrier(全屏障) 既禁止读重排序,也禁止写重排序 StoreLoad Barrier 屏障前的写,屏障后的读都不能重排序(最强屏障,常用于锁实现)

4. Java 中的内存屏障

  • Java 语言层面没有直接的“内存屏障”指令,但 JVM 会在实现 volatile、synchronized、原子类等并发工具时,在字节码层面插入内存屏障。
  • 例如:
  • volatile 写操作后会插入 Store Barrier
  • volatile 读操作前会插入 Load Barrier
  • synchronized 的加锁/解锁也会插入 Full Barrier

5. 例子说明

5.1 指令重排序问题

// 线程1a = 1; // 1flag = true; // 2 (volatile)// 线程2if (flag) { // 3 print(a); // 4}
  • 没有内存屏障时,1 和 2 可能被重排序,导致线程2看到 flag 为 true,但 a 还是 0。
  • 有了 volatile,JVM 会在 2 后插入 Store Barrier,保证 1 一定在 2 之前完成。

5.2 JVM 层面的伪代码

a = 1;StoreStoreBarrier; // volatile 写屏障flag = true;

6. 底层实现

  • 在 x86 架构下,常见的内存屏障指令有 mfence、sfence、lfence。
  • ARM 架构有 dmb、dsb、isb 等。

7. 总结

  • 内存屏障用于保证多线程下的可见性和有序性,防止指令重排序带来的并发 bug。
  • Java 程序员通常不直接操作内存屏障,但要理解 volatile、synchronized 等并发工具的底层原理。
  • 内存屏障是高性能并发编程的基础。

垃圾回收

判断一个对象是否可被回收
        

1. 引用计数法(Reference Counting)

  • 原理:每个对象有一个引用计数器,每当有一个引用指向它,计数+1;引用失效,计数-1。计数为0时对象可回收。
  • 缺点:无法处理循环引用。
  • 应用:早期的 JVM、CPython、COM 对象等。

2. 可达性分析法(Reachability Analysis)(Java主流)

  • 原理:以一组“GC Roots”为起点,沿着引用链查找,能到达的对象为“可达”,不可达的对象为“可回收”。
  • GC Roots 包括:栈中的引用、静态字段、JNI引用等。
  • 优点:能正确处理循环引用。
  • 应用:Java、C#、现代主流虚拟机。
  • Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:

    • 虚拟机栈中引用的对象
    • 本地方法栈中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中的常量引用的对象

3. 方法区的回收

因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。

主要是对常量池的回收和对类的卸载。

垃圾回收算法

1. 标记-清除(Mark-Sweep)算法

  • 过程:
  1. 标记:从 GC Roots 出发,标记所有可达对象。
  1. 清除:回收所有未被标记的对象。
  • 判断依据:未被标记的对象即为“可回收”。

2. 标记-整理(Mark-Compact)算法

  • 过程:
  1. 标记所有可达对象。
  1. 将所有存活对象向一端移动,清理无效空间。
  • 判断依据:未被移动的对象即为“可回收”。

3. 复制算法(Copying/Scavenge)

  • 过程:
  1. 将内存分为两块,每次只用一块。
  1. 标记存活对象并复制到另一块,剩下的全部回收。
  • 判断依据:未被复制的对象即为“可回收”。
  • 应用:新生代垃圾回收(如 HotSpot 的 Eden/Survivor 区)。

4. 分代收集算法(Generational Collection)

  • 原理:将堆分为新生代、老年代,不同区域用不同算法。
  • 新生代:复制算法。
  • 老年代:标记-清除或标记-整理。
  • 判断依据:各自区域内的可达性分析

垃圾收集器

新生代收集器

1. Serial 收集器
  • 单线程、简单高效
  • 适合单核、内存小的场景
  • Stop-The-World(STW)时间较长
2. ParNew 收集器
  • Serial 的多线程版本
  • 适合多核 CPU
  • 常与 CMS 搭配使用
3. Parallel Scavenge 收集器
  • 多线程、注重吞吐量
  • 适合后台运算、批量处理等对响应时间要求不高的场景
  • 可配合 Parallel Old

老年代收集器

4. Serial Old 收集器
  • Serial 的老年代版本,单线程
5. Parallel Old 收集器
  • Parallel Scavenge 的老年代版本,多线程
6. CMS(Concurrent Mark Sweep)收集器
  • 低停顿,关注响应时间
  • 并发标记、并发清除,适合对延迟敏感的应用
  • 会产生内存碎片

新生代+老年代协作

  • ParNew + CMS:低延迟场景常用组合
  • Parallel Scavenge + Parallel Old:高吞吐量场景常用组合

全年代收集器

7. G1(Garbage First)收集器
  • 面向服务端应用,兼顾低延迟和高吞吐
  • 把堆划分为多个小 Region,按需回收
  • 支持可预测的停顿时间(Pause Time Goals)
  • JDK 9+ 默认收集器
8. ZGC(JDK 11+)
  • 超低延迟(<10ms),大堆场景
  • 并发回收,停顿极短
  • 目前支持 Linux/x64、macOS、Windows
9. Shenandoah(JDK 12+)
  • 类似 ZGC,超低延迟
  • RedHat 贡献,支持多平台

选择收集器的建议

  • 单核/小内存/测试环境:Serial
  • 多核/高吞吐量:Parallel Scavenge + Parallel Old
  • 低延迟/响应快:CMS、G1、ZGC、Shenandoah
  • 大堆/超低延迟:ZGC、Shenandoah

内存分配与回收策略

一、内存分配策略

1. 对象优先在新生代分配

  • Java 堆被分为新生代(Eden + Survivor)和老年代。
  • 绝大多数对象在 Eden 区分配,只有 Survivor 区空间不够时才会直接分配到老年代。

2. 大对象直接进入老年代

  • 大对象(如大数组、长字符串)如果超过阈值(可通过 -XX:PretenureSizeThreshold 设置),会直接分配到老年代,避免在新生代频繁复制。

3. 长期存活的对象进入老年代

  • 对象每经历一次 Minor GC,年龄+1。
  • 达到一定年龄(默认15,可通过 -XX:MaxTenuringThreshold 设置)后,会被晋升到老年代。

4. 动态对象年龄判定

  • 如果 Survivor 区相同年龄所有对象大小总和大于 Survivor 区一半,年龄大于等于该年龄的对象直接进入老年代。

5. 空间分配担保

  • 在发生 Minor GC 前,JVM 会检查老年代最大可用连续空间是否大于新生代所有对象总和。
  • 如果不成立,则会触发 Full GC(老年代回收)

二、内存回收策略

1. Minor GC(新生代收集)

  • 只回收新生代(Eden + Survivor)。
  • 频繁,速度快,Stop-The-World。

2. Major GC / Full GC(老年代/全堆收集)

  • 回收老年代(Major GC)或整个堆(Full GC)。
  • 频率低,耗时长,Stop-The-World。

三、典型 JVM 参数

  • -Xms/-Xmx:堆初始/最大大小
  • -Xmn:新生代大小
  • -XX:SurvivorRatio:Eden:Survivor 区比例
  • -XX:PretenureSizeThreshold:大对象直接进入老年代的阈值
  • -XX:MaxTenuringThreshold:对象晋升老年代的年龄阈值

JVM 内存结构(运行时数据区)

JVM 在运行 Java 程序时,会把内存划分为若干个不同的区域,每个区域有不同的作用和生命周期。主要包括以下几个部分:

1. 程序计数器(Program Counter Register)

  • 每个线程私有的一块小内存空间。
  • 记录当前线程所执行的字节码的行号指示器。
  • 如果线程正在执行 Java 方法,计数器记录正在执行的虚拟机字节码指令地址;如果执行的是 Native 方法,计数器值为空。
  • 唯一一个不会出现 OutOfMemoryError 的区域。

2. Java 虚拟机栈(JVM Stack)

  • 每个线程私有,生命周期与线程一致。
  • 每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量、操作数栈、动态链接、方法出口等信息。
  • 方法调用就是栈帧的入栈和出栈过程。
  • 可能抛出 StackOverflowError(栈深度过大)和 OutOfMemoryError(无法分配内存)。

3. 本地方法栈(Native Method Stack)

  • 与 JVM 栈类似,但为 Native 方法服务(如 C/C++ 方法)。
  • 不是所有 JVM 都实现本地方法栈,HotSpot 把它和 JVM 栈合二为一。

4. Java 堆(Heap)

  • 所有线程共享的内存区域。
  • 存放对象实例和数组,是垃圾收集器管理的主要区域(GC Heap)。
  • 堆可以进一步细分为新生代(Young Generation)和老年代(Old Generation),新生代又分 Eden 区和两个 Survivor 区。
  • 可能抛出 OutOfMemoryError。

5. 方法区(Method Area)

  • 所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。
  • 在 HotSpot 早期实现中叫做永久代(PermGen),JDK8 以后改为元空间(Metaspace),存放在本地内存。
  • 可能抛出 OutOfMemoryError。

ThreadDump

一、什么是 Thread Dump

Thread Dump(线程快照)是 JVM 在某一时刻所有线程的运行状态、调用栈、锁持有情况等信息的文本输出。

常用于排查死锁、线程阻塞、CPU飙高、线程泄漏等问题。


二、如何抓取 Thread Dump

1. 使用命令行工具

1.1 jstack(最常用)
jstack  > threaddump.txt
1.2 kill -3 (Linux/Unix)
  • 给 Java 进程发送 SIGQUIT 信号,JVM 会把 Thread Dump 打印到进程的标准输出(通常是控制台或日志文件)。
  • 适用于生产环境,不会杀死进程。
1.3 jcmd
jcmd  Thread.print > threaddump.txt
  • 功能类似 jstack,但更强大。
1.4 通过 JVisualVM、JConsole 等可视化工具
  • 连接到目标 JVM,点击“线程”或“线程 Dump”按钮即可。

三、Thread Dump 典型内容

  • 每个线程的名称、优先级、ID、状态(如 RUNNABLE、WAITING、BLOCKED、TIMED_WAITING)
  • 当前执行的代码行
  • 持有/等待的锁
  • 死锁信息(如有)

四、Thread Dump 分析方法

1. 关注线程状态

  • RUNNABLE:正在运行或就绪
  • WAITING/TIMED_WAITING:等待条件、锁、sleep、wait、join
  • BLOCKED:等待获取锁
  • TERMINATED:已结束

2. 死锁分析

  • Thread Dump 末尾会有 Found one Java-level deadlock,并列出涉及的线程和锁。
  • 重点查找 waiting to lock 和 locked 的对象。

3. 阻塞/性能分析

  • 查找大量线程处于 BLOCKED 状态,分析它们都在等待哪个锁。
  • 查找 CPU 飙高时,哪个线程一直 RUNNABLE,分析其调用栈。

4. 线程泄漏

  • 线程数异常多,查找线程名和创建栈,定位线程未关闭的代码。

5. 典型 Thread Dump 片段

\"main\" #1 prio=5 os_prio=31 tid=0x00007f8b8b800000 nid=0x3e03 waiting on condition [0x000070000b1e9000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) ... Locked ownable synchronizers: -  (a java.util.concurrent.locks.ReentrantLock$NonfairSync)

五、常用分析技巧

  • 死锁:查找 Found one Java-level deadlock。
  • 阻塞:查找大量线程 BLOCKED,分析锁对象。
  • 高CPU:查找一直 RUNNABLE 的线程,分析其调用栈。
  • 线程泄漏:查找线程名、数量、创建栈。

六、常用工具

  • FastThread(阿里开源,Thread Dump 可视化分析)
  • JVisualVM、JConsole
  • IntelliJ IDEA 的 Thread Dump 分析器