背景

本文是《Java 后端从小白到大神》修仙系列第二十一篇,正式进入Java后端世界,本篇文章主要聊Java基础。若想详细学习请点击首篇博文,我们开始把。

文章概览

  1. JVM基础与架构
  2. JVM 内存区域与垃圾回收
  3. 类加载机制
  4. JVM 性能调优
  5. 高级主题与实战
  6. 实践项目

JVM

1. JVM 基础与架构

1. JVM 概述

JVM 的作用与核心职责,JVM(Java Virtual Machine,Java 虚拟机) 是 Java 技术的核心组件,负责将 Java 代码转换为可执行的计算机指令,并管理代码的运行环境。它是 “一次编写,到处运行”(Write Once, Run Anywhere)。核心职责,字节码解释与执行,内存管理与垃圾回收,安全与隔离,多线程与并发支持。Oracle 发布的《Java 虚拟机规范》定义了 JVM 的抽象行为标准,包括字节码格式、类加载流程、内存模型等。核心内容,字节码指令集(如 invokevirtual、getfield),类文件结构(魔数 0xCAFEBABE、常量池),运行时数据区(堆、栈、方法区),异常处理机制。JVM 规范可以有多种实现,如HotSpot、GraalVM、OpenJ9、Zing。Java 代码执行流程:源码 → 字节码 → JVM 执行。

2. JVM 内存模型(JMM)

Java 内存模型(JMM)定义了线程主内存之间的交互规则,核心目标是解决多线程并发中的可见性有序性原子性问题。

1. 主内存与工作内存

  • 主内存(Main Memory)
    所有线程共享的内存区域,存储实例变量(成员变量)、类变量(静态变量)。
  • 工作内存(Working Memory)
    每个线程私有的内存区域,保存该线程使用的变量的副本。
    关键规则
    • 线程不能直接操作主内存中的变量,必须通过工作内存间接操作。
    • 线程间变量值的传递需通过主内存完成(见下图)。
线程A工作内存 → 主内存 → 线程B工作内存  

2. 内存间交互操作

JMM 定义了 6 种原子操作,用于主内存与工作内存间的数据同步:

操作 作用
read 从主内存读取变量到工作内存(仅传输,未赋值)。
load read 得到的变量值赋给工作内存中的变量副本。
use 线程执行时,从工作内存读取变量值到操作栈(如计算、方法调用)。
assign 将操作栈中的新值赋给工作内存中的变量副本(修改变量值)。
store 将工作内存中的变量副本传回主内存(仅传输,未写入)。
write store 传回的变量值写入主内存的变量中。

规则与顺序

  • readloadstorewrite 必须成对出现且顺序执行。
  • 不允许丢弃 assign 操作(修改后必须同步到主内存)。
  • 未发生 assign 的变量不允许同步到主内存。

3. volatile 关键字与内存可见性

volatile 是 Java 提供的一种 轻量级同步机制,它的底层机制主要依赖于内存可见性 和禁止指令重排序,其核心是 CPU 内存屏障(Memory Barrier)缓存一致性协议(MESI)

1. volatile 的底层实现机制
(1) 保证可见性:MESI 协议(缓存一致性协议)

在多核 CPU 下,每个线程通常会把数据缓存到自己的 CPU 缓存 中,导致不同线程看到的数据可能不一致。volatile 通过 内存屏障 + CPU 缓存一致性协议(MESI),保证变量的可见性:

  • volatile 变量修改后,JVM 会在汇编层面插入 lock 前缀的指令,比如 lock addl $0x0, (%rsp),相当于触发 缓存一致性协议(MESI 协议),强制:
    1. 将当前 CPU 缓存中的变量刷新到主内存
    2. 使其他 CPU 缓存中的该变量失效,下次使用时必须重新从主内存读取
(2) 保证有序性:内存屏障(Memory Barrier)

在 CPU 指令执行时,为了优化性能,可能会进行 指令重排序(Instruction Reordering),即:

  • 代码编写顺序 ≠ 实际执行顺序
  • 编译器和 CPU 可能会调整指令顺序,以提升性能
如何禁止指令重排序?
  • volatile 变量的读写操作前后,JVM 插入内存屏障(Memory Barrier),来防止编译器和 CPU 进行 指令重排序
  • 主要的内存屏障:
    • 写屏障(Store Barrier):确保 volatile 变量的修改 对其他线程立即可见
    • 读屏障(Load Barrier):确保 volatile 变量的读取 只能在其前面的指令执行完之后进行
2. volatile 的汇编指令

通过 javap -c -verbose 反编译 volatile 代码,我们可以看到 lock 指令:

1
2
3
4
5
6
7
public class VolatileExample {
    volatile int count = 0;

    public void increase() {
        count++;
    }
}

反编译后:

0x10: lock addl $0x1, 0x10(%rsp)  ; `lock` 指令触发缓存一致性协议,保证可见性
  • lock 指令会触发 MESI 协议,让其他 CPU 的缓存失效,从主内存读取最新值,保证 可见性
  • lock 也会 防止指令重排序,保证 有序性
3. volatile 的局限性
  1. 不保证原子性

    • volatile 仅保证可见性和有序性,但不保证操作的原子性
    • 例如 count++ 其实是三步:
      1. 读取 count
      2. +1 计算
      3. 写入 count
    • 在多线程环境下,多个线程可能同时读取相同的 count,然后分别 +1,再写回,导致数据丢失(竞态条件)
    • 解决方案:使用 AtomicIntegersynchronized
  2. 不支持互斥

    • volatile 不能用于实现临界区(Critical Section),因为它不能保证多个线程对同一资源的互斥访问
    • synchronized 适用于保证互斥volatile 只适用于状态标记、单例模式中的双重检查锁等场景。
4. volatile 适用场景
  1. 标志位(状态变量)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    class Task {
        private volatile boolean running = true;
    
        public void stop() {
            running = false; // 其他线程可以立刻看到最新值
        }
    
        public void run() {
            while (running) {
                // 执行任务
            }
        }
    }
    
    • 由于 volatile 变量的修改对所有线程立即可见,适合用作标志位,如任务终止信号。
  2. 单例模式(双重检查锁)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    class Singleton {
        private static volatile Singleton instance;
    
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    
    • volatile 保证 instance 变量在不同线程间的可见性,防止指令重排序,确保对象正确初始化

4. 原子性、可见性、有序性

原子性(Atomicity)
  • 定义:操作不可分割,要么全部执行,要么不执行。
  • 实现方式
    • 使用 synchronizedLock 锁。
    • 使用原子类(如 AtomicInteger)。
可见性(Visibility)
  • 定义:一个线程修改共享变量后,其他线程能立即看到最新值。
  • 实现方式
    • volatile 关键字。
    • synchronized(解锁前将变量同步到主内存)。
    • final 关键字(不可变变量的初始化安全)。
有序性(Ordering)
  • 定义:程序执行顺序符合代码的先后顺序(禁止指令重排序)。
  • 破坏有序性的原因
    • 编译器优化(如方法内联)。
    • 处理器指令级并行优化(如流水线、乱序执行)。
  • 实现方式
    • volatile 关键字(插入内存屏障)。
    • synchronized(锁的互斥性隐式保证有序性)。
    • happens-before 原则(JMM 定义的内存操作顺序规则)。

5. JMM 的 happens-before 原则

在 Java 内存模型(JMM)中,happens-before(先行发生)定义线程间内存可见性的规则,保证某些操作的执行顺序,让多线程程序运行时不会出现意外的并发问题,核心:happens-before 规则决定了 Java 线程间的可见性,保证了 JMM 的正确执行。

简单来说

  • 如果 A happens-before B,那么A 的结果对 B 可见,B 不能重排序到 A 之前。
  • 如果 A 不 happens-before B,那么B 可能看不到 A 的修改,甚至可能乱序执行。

JMM 规定了 8 种 happens-before 关系,如果两个操作满足其中之一,就保证它们的执行顺序和可见性。

(1) 单线程内的程序顺序规则(Program Order Rule)

同一个线程内,代码按顺序执行,前面的操作 happens-before 后面的操作:

1
2
int a = 1;  // (1)
int b = 2;  // (2)

保证:语句 (1) 先于语句 (2) 执行,(2) 一定能看到 (1) 的结果。

(2) 监视器锁(Lock)规则

一个 unlock 操作 happens-before 之后的 lock 操作(对同一把锁)。

1
2
3
4
5
6
7
synchronized (lock) {
    count++;  // (1) 释放锁
}
// 在另一个线程
synchronized (lock) {
    int n = count; // (2) 获取锁
}

保证

  • (1) unlock 之后,(2) lock 之前的所有操作对 (2) 可见。
  • 防止多个线程同时执行 synchronized 块。
(3) volatile 变量规则

volatile 变量的写操作 happens-before 之后的读操作

1
2
3
4
5
6
7
8
9
volatile boolean flag = false;

Thread A:
flag = true;  // (1)

Thread B:
if (flag) {   // (2) 一定能看到 (1)
    // 代码执行
}

保证

  • 线程 A 修改 flag 之后,线程 B 立即可见,(2) 读取 flag 时一定能看到 (1) 的结果。
  • 这个规则防止指令重排序。
(4) 线程 start() 规则

线程 start() 之前的操作 happens-before 线程的 run() 方法

1
2
3
4
5
Thread t = new Thread(() -> {
    int r = count; // (2) 一定能看到 (1)
});
count = 10;  // (1)
t.start();   // (3)

保证

  • (1) 在 (3) start() 之前执行,那么 (2) 线程的 run() 方法一定能看到 (1) 的结果。
(5) 线程 join() 规则

线程 join() 之前的操作 happens-before join() 之后的操作

1
2
3
4
5
6
Thread t = new Thread(() -> {
    count = 10;  // (1)
});
t.start();
t.join();  // (2)
int n = count;  // (3) 一定能看到 (1)

保证

  • (1) 在 (2) join() 之前执行,那么 (3) 一定能看到 (1) 的结果。
(6) 线程 interrupt() 规则

interrupt() 先执行,isInterrupted() 结果可见

1
2
3
4
5
6
7
Thread t = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 执行任务
    }
});
t.start();
t.interrupt();  // (1)

保证

  • (1) interrupt() 先执行,isInterrupted() 一定能检测到这个中断信号。
(7) final 变量规则

构造函数对 final 变量的赋值 happens-before 该对象被其他线程使用

1
2
3
4
5
6
7
8
class A {
    final int x;
    A() {
        x = 100;  // (1)
    }
}
A a = new A();  // (2)
int y = a.x;  // (3) 一定能看到 (1)

保证

  • (1) 赋值后,(3) 一定能看到 x = 100
(8) 传递性规则

如果 A happens-before B,且 B happens-before C,那么 A happens-before C

1
2
3
4
5
6
7
volatile int a = 0;

Thread 1:
a = 10;   // (1)

Thread 2:
int x = a;  // (2)
  • (1) happens-before (2),所以 (2) 一定能看到 a = 10
(9) happens-before 和 as-if-serial 规则的区别

happens-before

  • 多线程的可见性规则
  • 保证内存同步
  • 跨线程生效

as-if-serial

  • 单线程中的重排序优化
  • 不能改变程序的最终结果
  • 只影响性能,不影响语义
1
2
int a = 1;   // (1)
int b = 2;   // (2)

即使 CPU 交换了 (1) 和 (2) 的顺序,也不影响最终结果。

(10) 表格总结
happens-before 规则 作用
程序顺序 单线程代码按顺序执行
锁(synchronized) 解锁 happens-before 之后的加锁
volatile 变量 volatile 写 happens-before 之后的读
start() 规则 start() happens-before run()
join() 规则 线程执行完 happens-before join()
interrupt() 规则 interrupt() happens-before isInterrupted()
final 规则 final 变量初始化 happens-before 其他线程读
传递性 A happens-before B,B happens-before C,⇒ A happens-before C

2. JVM 内存区域与垃圾回收

1. 运行时数据区

1. 程序计数器(PC Register)

  • 作用:存储当前线程执行的字节码指令地址(即下一条将要执行的指令)。
  • 特点
    • 每个线程都有独立的 PC 寄存器(线程私有)。
    • 如果线程执行的是 Java 方法,则存储下一条指令的地址
    • 如果线程执行的是 Native 方法,则 PC 计数器为空(Undefined)。

示例代码(看不见 PC 寄存器,但可以推测其作用):

1
2
3
4
5
6
7
8
9
// java最后都会编译成字节码,PC动态更新记录字节码的每一行指令
public class PCRegisterExample {
    public static void main(String[] args) {
        int a = 10;  // PC 指向这行代码
        int b = 20;  // PC 变更到这里
        int c = a + b;  // PC 继续向下执行
        System.out.println(c);  // PC 继续执行
    }
}

分析

  • PC 计数器会随着方法指令流动而变化,每行代码执行时,PC 计数器都会记录当前执行的位置。

2. Java 虚拟机栈(Stack)与栈帧结构

  • 作用:存储方法调用时的局部变量、操作数栈、动态链接、方法返回地址等。
  • 特点
    • 线程私有,每个线程都有自己的Java 虚拟机栈
    • 每调用一次方法就会创建一个栈帧,存放该方法的局部变量和操作数栈。
    • 方法执行完毕后,栈帧销毁,栈空间回收。
    • 如果栈深度超出 JVM 允许的最大范围,会抛出 StackOverflowError
    • 操作数栈是一个后进先出(LIFO)的栈结构,操作数栈用于存储中间计算结果和操作数。
    • 栈帧既包括局部变量表(Local Variable Table)、操作数栈(Operand Stack)、动态连接(Dynamic Linking)方法返回地址(Return Address)、附加信息。

示例代码(递归导致 StackOverflowError):

1
2
3
4
5
6
7
8
9
public class StackOverflowExample {
    public static void recursiveMethod() {
        recursiveMethod();  // 无限递归调用,导致栈溢出
    }

    public static void main(String[] args) {
        recursiveMethod();
    }
}

分析

  • 每次方法调用,都会创建新的栈帧,直到栈空间耗尽,抛出 StackOverflowError

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

  • 作用:用于执行 Native 方法(C 语言实现的 JNI 方法)。
  • 特点
    • 和 Java 虚拟机栈类似,但它是为本地方法(C 语言等)服务的
    • 可能出现 StackOverflowErrorOutOfMemoryError
    • 线程私有

示例代码(调用本地方法):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class NativeMethodExample {
    static {
        System.loadLibrary("nativeLib"); // 加载本地库
    }

    public native void nativeMethod(); // 声明本地方法

    public static void main(String[] args) {
        new NativeMethodExample().nativeMethod(); // 调用本地方法
    }
}

分析

  • nativeMethod() 运行在 Native 方法栈,而不是 Java 虚拟机栈。

4. 堆(Heap)的分代模型

(1)概述
  • 作用:存储所有对象(包括实例变量和数组)。
  • 特点
    • 线程共享,整个 JVM 只有一个堆
    • **垃圾回收(GC)**主要发生在堆上。
    • 采用分代管理,提高垃圾回收效率。
(2)堆的分代模型
区域 说明
年轻代(Young Generation) 存放新创建的对象,分为 Eden、Survivor0、Survivor1
老年代(Old Generation) 存放长生命周期对象
元空间(Metaspace) 存放类元信息,取代原来的方法区

示例代码(堆内存分配):

1
2
3
4
5
6
7
public class HeapMemoryExample {
    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            new Object();  // 创建对象,占用堆内存
        }
    }
}

分析

  • 大量创建对象,会触发年轻代 GC(Minor GC)。
  • 如果对象存活足够久,会晋升到 老年代

5. 方法区(Method Area)与元空间(Metaspace)

  • 作用:存储 类的元数据(类名、修饰符、父类、接口、字段、方法等)、类的字节码、方法字节码、静态变量、运行时常量池、JIT编译后的代码、类加载信息。
  • 特点
    • JDK 8 之前:方法区在堆中,称为“永久代(PermGen)”。
    • JDK 8 及以后:方法区使用本地内存(Metaspace),称为元空间,不受堆大小限制。

示例代码(动态创建类,占用方法区):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

public class MetaspaceExample {
    // 1. 静态变量(存储在方法区)
    private static int staticVar = 100;

    // 2. 运行时常量池
    private static final String constantStr = "Constant";

    // 3. 方法字节码(存储在方法区)
    public static void testMethod() {
        int a = 10; // 局部变量(存储在栈中)
        System.out.println("Method Executed");
    }

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MetaspaceExample.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
            enhancer.create();  // 4. 不断创建新类,占用元空间
        }
    }
}

分析

  • 这段代码不断创建新类,最终会导致 OutOfMemoryError: Metaspace

6. 常量池(Constant Pool)与运行时常量池(Runtime Constant Pool)

对比项 常量池(Class Constant Pool) 运行时常量池(Runtime Constant Pool)
存储位置 .class 文件的常量池部分 JVM 方法区(Metaspace)
存储内容 编译期静态常量(字面量、符号引用) 运行时加载的动态常量
作用 提供字节码解析用的符号引用 运行时解析后的直接引用
加载时机 编译期由编译器写入 .class 文件 类加载时,解析 .class 的常量池数据
是否可扩展 不可变(固定在 .class 文件中) 可扩展(可动态加入新的常量,如 String.intern()

当 JVM 加载 .class 文件时,会把 .class 文件里的常量池加载到内存中,形成运行时常量池,是常量池在运行时的表现形式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class ConstantPoolExample {
    public static void main(String[] args) {
        // 1. 常量池中的字面量(编译期确定)
        final String str1 = "Hello";
        final String str2 = "World";
        String str3 = "HelloWorld"; // 存在于常量池中

        // 2. 运行时常量池的动态添加
        String str4 = (str1 + str2).intern(); // 运行时计算后放入常量池
        System.out.println(str3 == str4); // true,str4 被放入运行时常量池
    }
}

分析

  • "HelloWorld" 在编译期已存入 .class 文件的 常量池
  • str4 = (str1 + str2).intern(); 运行时动态计算,然后尝试放入 运行时常量池
  • str3 == str4 结果为 true,因为 str4 通过 intern() 方法,返回的是 运行时常量池 的引用。

2. 垃圾回收(GC)

Java 虚拟机(JVM)的垃圾回收机制用于回收不再使用的对象,优化内存使用,提升应用性能。以下是 GC 相关的核心概念:

1. 对象存活性判断

JVM 需要判断对象是否仍然存活,常见方法有引用计数法可达性分析

(1) 引用计数法(Reference Counting)
  • 原理:给每个对象维护一个引用计数器,引用增加时计数 +1,引用减少时计数 -1,计数为 0 的对象可被回收。
  • 缺点:无法处理循环引用,可能导致内存泄漏。

示例(循环引用导致对象无法被回收):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Node {
    Node next;
    public void setNext(Node next) {
        this.next = next;
    }
}

public class ReferenceCountingGC {
    public static void main(String[] args) {
        Node a = new Node();
        Node b = new Node();
        a.setNext(b);
        b.setNext(a); // 循环引用
        a = null;
        b = null;
        // 此时 a 和 b 仍然互相引用,但无法被外部访问,GC 无法回收它们(若使用引用计数法)
    }
}
(2) 可达性分析(Reachability Analysis)
  • 原理:以GC Roots 作为起点,进行可达性遍历(Reachability Search)。
  • GC Roots 包括
    • 栈中的局部变量(方法栈帧中的变量)
    • 方法区中的静态变量
    • 方法区中的常量引用
    • 本地方法栈中的 JNI 引用
  • 对象不可达(没有从 GC Roots 直达的引用链)时,才会被回收。

示例(对象变成不可达状态后可被回收):

1
2
3
4
5
6
public class ReachabilityGC {
    public static void main(String[] args) {
        Object obj = new Object(); // obj 是 GC Root 可达的
        obj = null; // 现在 obj 变成不可达,GC 可以回收它
    }
}

2. GC 算法

(1) 标记-清除(Mark-Sweep)
  • 步骤
    1. 标记:从 GC Roots 遍历所有可达对象,做标记。
    2. 清除:未被标记的对象被回收。
  • 优点:实现简单,无需移动对象。
  • 缺点
    • 产生内存碎片,可能导致大对象无法分配。
    • 清理速度较慢。
(2) 标记-整理(Mark-Compact)
  • 步骤
    1. 标记:标记可达对象。
    2. 整理:将存活对象移动到内存一端,回收所有未标记对象的空间。
  • 优点:消除内存碎片。
  • 缺点:对象移动需要额外成本。
(3) 复制算法(Copying)
  • 步骤
    1. 将对象分配在 Eden 区。
    2. 存活对象复制到 Survivor 区。
    3. Survivor 空间不够时,晋升到老年代。
  • 优点:适用于短生命周期对象,无碎片问题。
  • 缺点:需要额外的内存空间

3. 分代收集理论与 GC 触发条件

JVM 使用分代垃圾回收策略

  • 年轻代(Young Generation)
    • 采用复制算法(Copying)。
    • 触发 Minor GC(回收年轻代)。
  • 老年代(Old Generation)
    • 采用标记-整理算法(Mark-Compact)。
    • 触发 Major GC / Full GC(回收整个堆)。
  • 元空间(Metaspace)
    • 存储类元信息
    • 当 Metaspace 内存不足时,会触发 GC。
GC 触发条件
  • Minor GC(新生代 GC)
    • 触发时机:Eden 区满时触发。
    • 影响范围:仅清理新生代,存活对象移至 Survivor 区或老年代。
  • Major GC(老年代 GC)
    • 触发时机:老年代空间不足。
    • 影响范围:清理老年代,时间较长。
  • Full GC(全堆 GC)
    • 触发时机:
      • 老年代满了。
      • System.gc() 被调用(可能触发)。
      • Metaspace 空间不足。
    • 影响范围:清理整个堆,通常比 Minor GC 慢得多。

4. 垃圾收集器对比

年轻代垃圾收集器
GC 类型 特点 适用场景
Serial 单线程 GC,适用于单核 CPU Client 端
ParNew Serial 的多线程版本 适用于 多核 CPU
Parallel Scavenge 追求高吞吐量,适合后台任务 吞吐量优先应用
老年代 & 全堆垃圾收集器
GC 类型 特点 适用场景
CMS(Concurrent Mark-Sweep) 低停顿,并发收集 低延迟应用(Web 服务器)
G1(Garbage First) 以区域(Region)为单位回收,适合大内存 低延迟,适合大内存
ZGC(Z Garbage Collector) 超低停顿时间(10ms 以内) 适用于 10GB 以上堆内存
Shenandoah 类似 ZGC,低停顿时间 适用于大型应用

5. GC 日志分析与调优参数

常见的 GC 日志格式
1
[GC类型 (触发原因)  回收前使用量->回收后使用量(总容量), 回收耗时]
启用 GC 日志
1
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

示例 GC 日志

1
2
[GC (Allocation Failure)  512K->256K(1024K), 0.002s]
[Full GC (Metadata GC Threshold)  1024K->512K(2048K), 0.05s]
常见 JVM 调优参数
参数 作用
-Xms 初始堆大小
-Xmx 最大堆大小
-XX:NewRatio 新生代/老年代比例
-XX:SurvivorRatio Eden/Survivor 比例
-XX:+UseG1GC 使用 G1 GC
-XX:+UseZGC 使用 ZGC

3. 类加载机制

Java 类加载(Class Loading)是 JVM 运行时动态加载类的过程,涉及 加载(Loading)、链接(Linking)、初始化(Initialization) 三个主要阶段。

1. 类加载过程

1. 类加载过程

(1)加载(Loading)
  • JVM 通过类加载器(ClassLoader)找到 .class 文件并读取字节码。
  • 在方法区中创建类的运行时数据结构,并生成 java.lang.Class 对象。

示例

1
2
3
4
5
public class TestClass {
    static {
        System.out.println("TestClass Loaded!");
    }
}

TestClass 被第一次使用时,JVM 会触发加载。

(2)链接(Linking)

链接阶段包含 验证(Verify)、准备(Prepare)、解析(Resolve) 三个步骤:

  1. 验证(Verify)

    • 确保字节码的合法性(例如:防止非法访问、格式错误等)。
    • 如果验证失败,会抛出 java.lang.VerifyError
  2. 准备(Prepare)

    • 为类的静态变量分配内存,并设置默认值(未执行初始化赋值)。
    1
    2
    3
    
    public class Demo {
        static int a = 10; // 这里不会赋值,只是默认初始化为 0
    }
    
  3. 解析(Resolve)

    • 符号引用(Symbolic References) 转换为 直接引用(Direct References)

    • 例如:

      1
      
      String s = "Hello"; 
      

      这里 "Hello" 是一个符号引用,解析后指向实际的 String 对象。

(3) 初始化(Initialization)
  • 执行类的静态代码块、静态变量的初始化。
  • JVM 触发初始化的情况
    • new 关键字创建对象。
    • 访问类的静态变量或方法。
    • 通过 Class.forName("XXX") 反射加载类。

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class InitExample {
    static {
        System.out.println("Class InitExample Initialized!");
    }

    static int num = 100;

    public static void main(String[] args) {
        System.out.println(InitExample.num); // 访问静态变量,触发类加载
    }
}

2. 类加载器层次结构

JVM 通过类加载器(ClassLoader)动态加载类,类加载器是分层的。

1
2
3
4
5
6
7
Bootstrap ClassLoader (启动类加载器)
Extension ClassLoader (扩展类加载器)
Application ClassLoader (应用类加载器)
Custom ClassLoader (自定义类加载器)
类加载器 作用 加载路径
Bootstrap ClassLoader 加载 JDK 内部核心类 rt.jarjava.base
Extension ClassLoader 加载扩展类库 lib/ext/ 目录
Application ClassLoader 加载用户代码 classpath
Custom ClassLoader 开发者自定义类加载器 可加载任意自定义路径

3. 双亲委派模型

双亲委派机制(Parent Delegation Model):

  1. 类加载请求先向上委派,从 Application -> Extension -> Bootstrap 逐级查找。
  2. 如果父加载器找不到类,则由当前类加载器加载。

示例:模拟双亲委派

1
2
3
4
5
6
7
8
public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader cl = ClassLoaderTest.class.getClassLoader();
        System.out.println("ClassLoader: " + cl);
        System.out.println("Parent ClassLoader: " + cl.getParent());
        System.out.println("Grandparent ClassLoader: " + cl.getParent().getParent()); // 可能为 null
    }
}

输出

ClassLoader: sun.misc.Launcher$AppClassLoader@...
Parent ClassLoader: sun.misc.Launcher$ExtClassLoader@...
Grandparent ClassLoader: null (Bootstrap 是 C++ 实现的)

4. 破坏双亲委派模型的场景

(1)Java SPI 机制

Java SPI(Service Provider Interface)是一种服务发现机制,它允许第三方为某个接口提供实现。其核心思想是将接口定义和具体实现分离,使得程序可以在运行时动态地发现和加载服务的实现类。这种机制的好处在于提高了程序的可扩展性和解耦性,让开发者可以通过配置文件来灵活地切换服务的实现。

示例:SPI 加载

SPI 加载
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 步骤 1:定义服务接口
// 首先,需要定义一个服务接口,这个接口将作为服务的抽象定义。

// 定义服务接口
public interface HelloService {
    void sayHello();
}


// 步骤 2:实现服务接口
// 接着,创建该接口的具体实现类。这里我们创建两个不同的实现类。

// 实现类 1
public class EnglishHelloService implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("Hello!");
    }
}

// 实现类 2
public class ChineseHelloService implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("你好!");
    }
}


// 步骤 3:配置服务提供者
// 在 `src/main/resources` 目录下创建 `META-INF/services` 文件夹,然后在该文件夹中创建一个以服务接口的全限定名命名的文件,这里是 `com.example.HelloService`(假设接口的包名为 `com.example`)。在这个文件中,每行填写一个服务实现类的全限定名。

com.example.EnglishHelloService
com.example.ChineseHelloService


// 步骤 4:使用 ServiceLoader 加载服务
// 在代码中使用 `ServiceLoader` 来加载并使用服务实现类。

import java.util.ServiceLoader;

public class Main {
    public static void main(String[] args) {
        // 使用 ServiceLoader 加载服务
        ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class);

        // 遍历所有服务实现类并调用方法
        for (HelloService service : serviceLoader) {
            service.sayHello();
        }
    }
}
1
2
// META-INF/services/java.sql.Driver
com.mysql.cj.jdbc.Driver

Java 的 ServiceLoader 通过线程上下文类加载器(Thread Context ClassLoader,TCCL)加载 MySQL 驱动,而不是 Bootstrap 加载器。

(2)OSGi 模块化系统

OSGi 允许多个版本的同一类共存,因此不能依赖双亲委派,而是由 各个模块的类加载器 独立加载类。

2. 字节码与执行引擎

Java 的字节码与执行引擎涉及多个核心概念,主要包括 Class 文件结构、字节码指令、解释器(Interpreter)与 JIT(C1/C2)、逃逸分析等优化技术。

1. Class 文件结构

Java 源代码经过 javac 编译后会生成 .class 文件,它是 JVM 可识别的字节码文件,主要由以下结构组成:

结构 作用
魔数(Magic Number) 标识 Class 文件,固定值 0xCAFEBABE
Class 文件版本 例如 Java 8 -> 52.0, Java 11 -> 55.0
常量池(Constant Pool) 存储字面量、方法引用、类信息等
访问标志(Access Flags) 标识类的修饰符(public、abstract、final 等)
类信息(This Class/Super Class) 定义当前类及其父类
接口信息(Interfaces) 记录实现的接口
字段表(Fields) 存储类的字段信息(变量)
方法表(Methods) 存储类的方法及其字节码
属性表(Attributes) 存储 CodeLineNumberTable 等信息,易可理解是方发表的补充

示例:解析 Class 文件

1
javap -verbose Test.class

可以看到 Constant poolMethods 等详细信息。

2. 字节码指令集

JVM 执行 Java 代码时依赖字节码指令,常见指令如下:

(1)算术运算
指令 作用
iadd int 加法
isub int 减法
imul int 乘法
idiv int 除法
irem 取余
(2)类型转换
指令 作用
i2f int 转 float
d2i double 转 int
i2b int 转 byte
(3)对象操作
指令 作用
new 创建对象
getfield 读取实例字段
putfield 写入实例字段
getstatic 读取静态字段
putstatic 写入静态字段
(4)方法调用
指令 作用
invokestatic 调用静态方法
invokevirtual 调用普通实例方法
invokespecial 调用私有、构造或父类方法
invokeinterface 调用接口方法
invokedynamic 处理 Lambda 和动态调用

示例:查看字节码

1
2
3
4
5
public class BytecodeDemo {
    public int add(int a, int b) {
        return a + b;
    }
}

查看编译后的字节码:

1
javap -c BytecodeDemo

输出

  public int add(int, int);
    Code:
       0: iload_1  // 加载第一个参数
       1: iload_2  // 加载第二个参数
       2: iadd     // 进行加法操作
       3: ireturn  // 返回结果

3. 解释器(Interpreter)与 JIT 编译器

(1)解释器
  • 逐行解释执行字节码,速度较慢,但启动快。
  • 适用于短时间运行的程序,如 CLI 工具。
(2)编译器
  • 将热点字节码编译为机器码,提高执行效率。
  • Java 采用 C1(Client 编译器)和 C2(Server 编译器)。
    • C1(Client JIT):优化轻量,适合 GUI 应用(低延迟)。
    • C2(Server JIT):更激进优化,适合高性能服务器(吞吐量优先)。

示例:启用 JIT 监控

1
2
-XX:+PrintCompilation  # 查看 JIT 编译过程
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation  # 记录 JIT 日志

4. JIT 优化技术

JIT 编译器在优化代码时,会使用 逃逸分析 进行优化。

(1)分析对象是否在方法外部被引用
  • 未逃逸 → 可进行栈上分配
  • 未逃逸 → 可进行标量替换
  • 部分逃逸 → 可能进行同步消除
(2)栈上分配

在 Java 中,默认情况下对象是在堆上分配内存的。但对于未逃逸的对象,JVM 可以将其分配在栈上。当方法执行结束后,栈上的内存会自动释放,无需进行垃圾回收,从而减少了堆内存的压力和垃圾回收的开销。

1
2
3
4
5
6
class EscapeAnalysisDemo {
    public static void alloc() {
        // 局部对象,没有逃逸
        User user = new User();
    }
}

JVM 可能优化为:

1
2
// 直接在栈上分配,无需堆内存
User user = <stack memory>;
(3)标量替换

标量是指不可再分的数据类型,如基本数据类型(int、double 等)。如果一个未逃逸的对象可以被分解为多个标量,JVM 会将对象替换为这些标量,直接在栈上分配这些标量,而不是创建对象。这样可以减少对象的创建和内存分配开销。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class ScalarReplaceDemo {
    static class Point {
        int x, y;
    }

    public static void foo() {
        Point p = new Point(); // 可能被优化
        p.x = 1;
        p.y = 2;
    }
}

JIT 可能优化为:

1
2
int x = 1;
int y = 2; // 直接使用两个变量替代 Point 对象
(4)同步消除

如果一个对象是部分逃逸的,即不会被多个线程同时访问,那么对该对象的同步操作(如 synchronized 关键字)是多余的。JVM 可以通过同步消除技术,将这些不必要的同步操作去除,从而提高程序的性能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class SynchronizationEliminationExample {
    public static void main(String[] args) {
        for (int i = 0; i < 1000000; i++) {
            createString();
        }
    }

    public static String createString() {
        // 这个对象部分逃逸,可能会进行同步消除
        StringBuffer sb = new StringBuffer();
        sb.append("Hello");
        sb.append(" World");
        return sb.toString();
    }
}

在 createString 方法中,StringBuffer 对象仅在方法内部使用,不会被多个线程同时访问,JVM 可能会消除 StringBuffer 内部的同步操作,提高性能。

4. JVM 性能调优

1. 调优方法论

Java 性能调优是一项系统性工程,涉及 监控、问题定位、JVM 参数优化 等多个方面。以下是调优的核心方法论。

1. 性能监控指标

在调优 JVM 时,需要关注以下关键指标:

指标 描述 常用工具
GC 时间 GC 触发频率、回收时间 jstat -gc,GC 日志
吞吐量 CPU 时间中执行业务代码的比例 jstat -gcutil,JFR
停顿时间 GC 造成的线程暂停时间 GC LogsG1 GC Logs
内存泄漏 长时间占用的对象无法被回收 MATJProfiler

示例:使用 jstat 监控 GC

1
jstat -gc <pid> 1000  # 每秒采样一次 GC 数据

2. 常见性能问题定位

2.1 CPU 高负载
  • 现象:应用 CPU 使用率长期过高,影响系统响应。
  • 排查工具
    • top / htop:查看 CPU 使用情况
    • jstack:线程栈分析,生成进程所有线程的堆栈快照信息
    • perf / async-profiler:热点分析

示例:使用 top 找到高 CPU 线程

1
top -> H -> P -> 记录 <pid>

然后将 <pid> 转换为 16 进制:

1
printf "%x\n" <pid> -> <十六进制线程 ID>

再用 jstack 定位:

1
jstack <pid> | grep <十六进制线程 ID> -A 30
2.2 内存溢出(OutOfMemoryError, OOM)
  • 现象:JVM 报 OutOfMemoryError,导致应用崩溃。
  • 排查工具
    • jmap -heap <pid>:查看堆内存
    • MAT / JProfiler:分析堆转储

示例:导出堆快照

1
jmap -dump:format=b,file=heap.hprof <pid>

然后用 Eclipse Memory Analyzer (MAT) 进行分析。

2.3 线程阻塞(Thread Block)
  • 现象:应用无响应,线程池任务堆积。
  • 排查工具
    • jstack:查看线程状态
    • jconsole / VisualVM:监控线程

示例:分析死锁

1
jstack <pid> | grep -A 20 "Found one Java-level deadlock"

3. JVM 参数分类与调优

JVM 参数可以分为以下几类:

3.1 堆内存相关
参数 作用
-Xms<size> 初始堆大小
-Xmx<size> 最大堆大小
-Xmn<size> 年轻代大小
-XX:NewRatio=<n> 老年代:年轻代比例

示例:设置堆大小

1
java -Xms1g -Xmx2g -Xmn512m MyApp
3.2 GC 相关
参数 作用
-XX:+UseG1GC 启用 G1 GC
-XX:+UseZGC 启用 ZGC
-XX:MaxGCPauseMillis=<n> 设定最大 GC 停顿时间
-XX:InitiatingHeapOccupancyPercent=<n> 触发 GC 的堆使用率

示例:优化 G1 GC

1
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDetails MyApp
3.3 线程 & 栈
参数 作用
-Xss<size> 设置线程栈大小
-XX:ThreadStackSize=<n> 线程默认栈大小

示例:增大线程栈

1
java -Xss1m MyApp

2. 工具使用

这些工具在 Java 性能分析和调优中扮演重要角色:

1. 命令行工具

  • jps:列出当前运行的 JVM 进程

    1
    
    jps -lvm
    
  • jstat:监控 GC、类加载等

    1
    
    jstat -gc <pid> 1000
    
  • jmap:导出堆转储,查看对象分布

    1
    
    jmap -dump:format=b,file=heap.hprof <pid>
    
  • jstack:分析线程堆栈,排查死锁

    1
    
    jstack -l <pid>
    
  • jinfo:获取/修改 JVM 运行时参数

    1
    
    jinfo -flags <pid>
    

2. 图形化工具

  • JConsole:实时监控线程、GC、内存
  • VisualVM:监控 GC、线程快照、内存分析
  • JMC(Java Mission Control):配合 Flight Recorder 进行性能分析

3. 线上诊断工具

  • Arthas:热加载、方法跟踪

    1
    2
    3
    4
    5
    
    1. 下载 arthas-boot.jar
    curl -O https://arthas.aliyun.com/arthas-boot.jar
    
    2. 安装 arthas,安装前随便启动一个 java 进程,执行下面命令,根据提示选择一个java进程
    java -jar arthas-boot.jar
    
  • Async-Profiler:火焰图分析 CPU/内存

4. 内存分析工具

  • MAT(Memory Analyzer Tool):分析 heap.hprof,排查内存泄漏。

5. 高级主题与实战

1. 锁升级

偏向锁/轻量级锁/重量级锁(synchronized 实现)

Java synchronized 采用 偏向锁 -> 轻量级锁 -> 重量级锁自适应升级策略,优化性能:

锁类型 特点 适用场景
偏向锁 线程无竞争,CAS 替代加锁 线程基本不会竞争
轻量级锁 CAS + 自旋锁,避免阻塞 竞争少,锁时间短
重量级锁 线程阻塞,依赖 OS 互斥锁 竞争激烈,锁时间长

示例:synchronized 代码块的锁升级

1
2
3
4
5
6
7
8
9
class LockTest {
    private Object lock = new Object();

    public void test() {
        synchronized (lock) {  // 偏向锁 -> 轻量级锁 -> 重量级锁
            System.out.println("Lock acquired");
        }
    }
}

可以用 -XX:+PrintGCApplicationStoppedTime -XX:+PrintSafepointStatistics 观察锁的状态变化。

2. 实战案例

1. 内存泄漏排查(MAT 分析堆转储)

步骤
  1. 生成 heap dump

    1
    
    jmap -dump:format=b,file=heap.hprof <pid>
    
  2. 使用 MAT 进行分析

    • MAT下载地址:下载 MAT
    • 打开 heap.hprof
    • 选择 “Dominator Tree” 查找大对象
    • 使用 “Leak Suspects Report” 定位可能泄漏的对象
案例
  • 单例模式导致的内存泄漏

    1
    2
    3
    4
    
    public class Singleton {
        private static final Singleton instance = new Singleton();
        private List<Object> cache = new ArrayList<>(); // 长期持有对象
    }
    

    解决方案:改为 WeakReference 或主动清理。

2. GC 调优实战(优化 Full GC 频率)

步骤
  1. 启用 GC 日志

    1
    
    -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
    
  2. 观察 Full GC 触发情况

    • jstat -gcutil <pid> 1000
    • grep Full GC gc.log
案例
  • 频繁 Full GC 影响吞吐

    1
    
    [Full GC (Allocation Failure)  512M->256M(1024M)]
    

    优化方案

    • 调大堆内存-Xmx2g -Xms2g
    • 调整新生代比例-XX:NewRatio=2
    • 使用 G1/ZGC-XX:+UseG1GC

3. 高并发场景下的锁竞争优化

步骤
  1. 使用 jstack 分析线程状态

    1
    
    jstack -l <pid>
    
  2. 定位 BLOCKED 线程

  3. 代码优化

    • 减少 synchronized 粒度
    • 使用 ReentrantLock 替代 synchronized
案例
  • 锁竞争导致吞吐下降

    1
    2
    3
    4
    5
    6
    
    class Counter {
        private int count = 0;
        public synchronized void increment() {
            count++;
        }
    }
    

    优化方案

    • 改用 AtomicInteger 避免锁
    1
    2
    3
    4
    
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet();
    }
    

4. JIT 编译日志分析(-XX:+PrintCompilation

步骤
  1. 启用 JIT 编译日志

    1
    
    -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation
    
  2. 查看编译热点

    1
    
    -XX:+PrintCompilation
    
案例
  • 关键代码未被 JIT 编译

    1
    
    1526   324  !   4 com.example.Test::method (43 bytes)
    

    优化方案

    • 增加热度(如循环调用)
    • 避免过大方法(拆分)
    • 使用 -XX:+TieredCompilation

3. 现代 JVM 发展

这些新技术都围绕 性能优化、并发改进、跨语言支持 进行增强,我们来逐一解析。

1. GraalVM 特性与多语言支持

GraalVM 是一个高性能、多语言的 JVM 发行版,支持 Java、JavaScript、Python、Ruby 等多种语言,并提供 AOT 编译Truffle 语言框架Polyglot API

核心特性
  • 即时编译(JIT):优化字节码执行
  • AOT 编译(Native Image):将 Java 代码编译为本地可执行文件
  • 多语言支持(Polyglot API):Java 与其他语言互操作
  • Truffle 语言框架:构建自定义语言解释器
示例:Java 调用 JavaScript
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import org.graalvm.polyglot.*;

public class PolyglotExample {
    public static void main(String[] args) {
        try (Context context = Context.create()) {
            int result = context.eval("js", "2 + 3").asInt();
            System.out.println("Result: " + result);
        }
    }
}

2. Project Loom(虚拟线程与协程)

JDK 21 引入 虚拟线程(Virtual Threads),它们是 轻量级线程,大幅提升高并发处理能力。

虚拟线程 vs. 传统线程
对比项 平台线程(Platform Thread) 虚拟线程(Virtual Thread)
调度方式 由 OS 负责 由 JVM 负责
线程数量 受 OS 限制 可创建百万级线程
阻塞影响 影响整个线程 仅影响当前协程
示例:创建虚拟线程
1
2
3
4
5
6
7
8
9
public class LoomExample {
    public static void main(String[] args) throws InterruptedException {
        Thread.startVirtualThread(() -> {
            System.out.println("Hello from virtual thread!");
        });

        Thread.sleep(1000);
    }
}
最佳应用场景
  • 高并发 I/O 任务
  • Web 服务器(如 Tomcat 已支持)
  • 事件驱动架构

3. ZGC/Shenandoah 低延迟 GC 原理

目标:减少 GC 停顿时间,适用于 低延迟应用

GC 类型 停顿时间 特点
ZGC <1ms 适用于 TB 级大内存
Shenandoah 10ms 以内 适用于响应式应用
ZGC(Zero Pause GC)
  • 并发执行:垃圾回收时,应用线程 几乎不暂停
  • 支持超大堆(最大支持 16TB

启用 ZGC

1
java -XX:+UseZGC -Xmx16g MyApp
Shenandoah GC
  • 并发压缩:边回收边整理
  • 适用于低延迟、金融、电商等系统

启用 Shenandoah

1
java -XX:+UseShenandoahGC -Xmx16g MyApp

4. 动态 CDS(Class Data Sharing)与 AOT 编译

Class Data Sharing(CDS)
  • 减少 JVM 启动时间(缓存 class 数据)
  • 共享 JAR 文件的 class 信息,降低内存占用

启用 CDS

1
2
java -Xshare:dump
java -Xshare:on -cp myapp.jar MyMainClass
AOT 编译(Ahead-Of-Time Compilation)
  • GraalVM 提供 AOT 编译,可提前编译 .class 到本地代码
  • 适用于小型容器/微服务

示例:使用 native-image

1
2
native-image -jar myapp.jar myapp
./myapp  # 直接运行,无需 JVM

6. 实践项目

1. 自定义类加载器实现热部署

核心原理

  • 双亲委派机制:自定义类加载器一般会重写 findClass 方法,而不是 loadClass,以避免父类加载器缓存已加载的类。
  • 热部署:每次重新加载类时,创建新的 ClassLoader 实例,确保新的 .class 文件生效。

实现步骤

  1. 监视 classes/ 目录下的 .class 文件变化。
  2. 发现修改后,使用 URLClassLoader自定义 ClassLoader 重新加载类。
  3. 利用反射调用新版本的方法。

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class HotSwapClassLoader extends ClassLoader {
    private String classPath;

    public HotSwapClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classData = loadClassData(name);
            return defineClass(name, classData, 0, classData.length);
        } catch (Exception e) {
            throw new ClassNotFoundException();
        }
    }

    private byte[] loadClassData(String name) throws IOException {
        String filePath = classPath + name.replace(".", "/") + ".class";
        return Files.readAllBytes(Paths.get(filePath));
    }
}

2. 通过字节码增强技术(ASM/Javassist)实现简单 AOP

核心原理

  • ASM/Javassist 可以动态修改字节码,实现方法级别的拦截(类似 Spring AOP)。
  • 目标:在目标方法执行前后插入日志或权限校验代码。

Javassist 示例:在方法前后插入代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class AOPEnhancer {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get("com.example.TargetClass");
        CtMethod method = ctClass.getDeclaredMethod("targetMethod");

        method.insertBefore("System.out.println(\"Before execution\");");
        method.insertAfter("System.out.println(\"After execution\");");

        ctClass.writeFile("./output/");
    }
}

应用场景

  • 日志增强(方法执行前后记录日志)
  • 权限校验(在方法执行前检查权限)
  • 性能监控(记录方法执行时间)

3. 模拟内存泄漏并利用工具定位

内存泄漏案例 1:静态集合导致对象无法释放

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class MemoryLeakExample {
    private static final List<byte[]> memoryLeakList = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        while (true) {
            memoryLeakList.add(new byte[1024 * 1024]); // 每次添加 1MB
            Thread.sleep(100);
        }
    }
}

分析工具

  • JConsole/VisualVM:查看堆内存变化
  • MAT(Memory Analyzer Tool):分析 heap dump,找到泄漏对象

使用 jmap 导出堆快照并分析

1
2
jmap -dump:format=b,file=heap.bin <pid>
mat heap.bin

总结

Java虚拟机的内容是必须掌握的。