Java JVM 知识点全面详细梳理

一、JVM 概述与体系结构

1. JVM 是什么?

Java 虚拟机(JVM)是运行 Java 字节码的虚拟计算机,它提供了跨平台运行 Java 程序的能力。”一次编写,到处运行”的核心就是 JVM。

2. JVM 体系结构

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
JVM 体系结构:
┌─────────────────────────────────────────────────────────────┐
│ JVM │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 运行时数据区 (Runtime Data Areas) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │ │
│ │ │ 方法区 │ │ 堆区 │ │ 虚拟机栈 │ │ │
│ │ │ (Method Area)│ │ (Heap) │ │ (Java Stack) │ │ │
│ │ └─────────────┘ └─────────────┘ └───────────────┘ │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 程序计数器 │ │ 本地方法栈 │ │ │
│ │ │ (PC Register)│ │(Native Stack)│ │ │
│ │ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 执行引擎 (Execution Engine) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │ │
│ │ │ 解释器 │ │ JIT编译器 │ │ 垃圾回收器 │ │ │
│ │ │(Interpreter)│ │ (JIT Compiler)│ │ (GC) │ │ │
│ │ └─────────────┘ └─────────────┘ └───────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 类加载器子系统 (ClassLoader Subsystem) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │ │
│ │ │ 加载 │ │ 链接 │ │ 初始化 │ │ │
│ │ │ (Loading) │ │ (Linking) │ │ (Initialization)│ │ │
│ │ └─────────────┘ └─────────────┘ └───────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

二、运行时数据区 (Runtime Data Areas)

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

  • 线程私有,记录当前线程执行的字节码指令地址
  • 执行 Native 方法时,值为空(Undefined)
  • 不会发生 OutOfMemoryError

2. Java 虚拟机栈 (Java Virtual Machine Stacks)

  • 线程私有,生命周期与线程相同
  • 存储栈帧(Stack Frame),每个方法对应一个栈帧
  • 可能抛出 StackOverflowError 和 OutOfMemoryError
1
2
3
4
5
6
7
8
9
10
// 栈溢出示例
public class StackOverflowExample {
public static void recursiveCall() {
recursiveCall(); // 无限递归导致栈溢出
}

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

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

  • 为 Native 方法服务
  • 与虚拟机栈类似,可能抛出 StackOverflowError 和 OutOfMemoryError

4. Java 堆 (Java Heap)

  • 线程共享,存放对象实例和数组
  • 垃圾收集器管理的主要区域
  • 可以细分为新生代和老年代
  • 可能抛出 OutOfMemoryError
1
2
3
4
5
6
7
8
9
10
11
12
// 堆内存溢出示例
import java.util.ArrayList;
import java.util.List;

public class HeapOOMExample {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object()); // 不断创建对象导致堆溢出
}
}
}

5. 方法区 (Method Area)

  • 线程共享,存储已被加载的类信息、常量、静态变量等
  • JDK 8 之前称为”永久代”(PermGen),JDK 8 及之后称为”元空间”(Metaspace)
  • 可能抛出 OutOfMemoryError
1
2
3
4
5
6
7
8
9
10
11
12
13
// 方法区内存溢出示例 (JDK 7 及之前)
import java.util.ArrayList;
import java.util.List;

public class MethodAreaOOMExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern()); // 不断将字符串添加到常量池
}
}
}

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

  • 方法区的一部分,存放编译期生成的各种字面量和符号引用

7. 直接内存 (Direct Memory)

  • 不是 JVM 运行时数据区的一部分,但会被频繁使用
  • NIO 使用 Native 函数库直接分配堆外内存
  • 可能抛出 OutOfMemoryError

三、垃圾收集机制 (Garbage Collection)

1. 对象存活判断

  • 引用计数法:简单但无法解决循环引用问题
  • 可达性分析:通过 GC Roots 对象作为起点,向下搜索,不可达的对象将被回收

2. GC Roots 对象包括

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

3. 引用类型

  • 强引用:普遍存在的引用,不会被 GC 回收
  • 软引用:内存不足时会被回收
  • 弱引用:只能生存到下一次 GC 前
  • 虚引用:无法通过虚引用获取对象实例,主要用于跟踪对象被回收的状态
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 java.lang.ref.*;

public class ReferenceExample {
public static void main(String[] args) {
// 强引用
Object strongRef = new Object();

// 软引用
SoftReference<Object> softRef = new SoftReference<>(new Object());

// 弱引用
WeakReference<Object> weakRef = new WeakReference<>(new Object());

// 虚引用
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);

System.gc(); // 建议JVM进行垃圾回收

System.out.println("强引用: " + strongRef);
System.out.println("软引用: " + softRef.get());
System.out.println("弱引用: " + weakRef.get()); // 可能为null
System.out.println("虚引用: " + phantomRef.get()); // 总是null
}
}

4. 垃圾收集算法

  • 标记-清除:先标记要回收的对象,然后清除(产生碎片)
  • 复制:将内存分为两块,每次使用一块,将存活对象复制到另一块(浪费空间)
  • 标记-整理:标记存活对象,然后让所有存活对象向一端移动(无碎片)
  • 分代收集:根据对象存活周期将内存划分为新生代和老年代,采用不同的收集算法

5. 垃圾收集器

  • Serial:单线程,新生代收集器,复制算法
  • ParNew:Serial 的多线程版本
  • Parallel Scavenge:吞吐量优先的新生代收集器,复制算法
  • Serial Old:Serial 的老年代版本,标记-整理算法
  • Parallel Old:Parallel Scavenge 的老年代版本,标记-整理算法
  • CMS:以获取最短回收停顿时间为目标,标记-清除算法
  • G1:面向服务端应用的垃圾收集器,整体基于标记-整理,局部基于复制算法
  • ZGC:JDK 11 引入的低延迟垃圾收集器
  • Shenandoah:低暂停时间的垃圾收集器

四、类加载机制 (Class Loading Mechanism)

1. 类加载过程

1
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

2. 类加载器

  • 启动类加载器 (Bootstrap ClassLoader):加载 JRE 核心类库
  • 扩展类加载器 (Extension ClassLoader):加载 JRE 扩展目录中的类
  • 应用程序类加载器 (Application ClassLoader):加载用户类路径上的类
  • 自定义类加载器:用户自定义的类加载器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类加载器示例
public class ClassLoaderExample {
public static void main(String[] args) {
// 获取当前类的类加载器
ClassLoader loader = ClassLoaderExample.class.getClassLoader();
System.out.println("当前类加载器: " + loader);

// 获取父类加载器
System.out.println("父类加载器: " + loader.getParent());

// 获取祖父类加载器(启动类加载器,由C++实现,Java中为null)
System.out.println("祖父类加载器: " + loader.getParent().getParent());

// 查看加载了哪些类
System.out.println("String类的类加载器: " + String.class.getClassLoader());
}
}

3. 双亲委派模型

  • 类加载器收到加载请求时,不会自己尝试加载,而是委派给父类加载器
  • 只有当父类加载器无法完成加载时,子加载器才会尝试自己加载
  • 保证 Java 核心库的类型安全
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
// 自定义类加载器示例
import java.io.*;

public class CustomClassLoader extends ClassLoader {
private String classPath;

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

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

private byte[] loadClassData(String name) throws IOException {
name = name.replace(".", "/");
String fileName = classPath + "/" + name + ".class";

try (InputStream is = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {

int len;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}

return baos.toByteArray();
}
}
}

4. 打破双亲委派模型

  • 线程上下文类加载器 (Thread Context ClassLoader)
  • OSGi 模块化系统

五、JVM 性能监控与调优

1. JVM 参数类型

  • 标准参数:以 - 开头,如 -version, -help
  • X 参数:以 -X 开头,如 -Xmx, -Xms
  • XX 参数:以 -XX 开头,用于开发调试,如 -XX:+PrintGC

2. 常用 JVM 参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 堆内存设置
-Xms512m # 初始堆大小
-Xmx1024m # 最大堆大小
-Xmn256m # 新生代大小
-XX:NewRatio=2 # 老年代与新生代的比例
-XX:SurvivorRatio=8 # Eden区与Survivor区的比例

# 垃圾收集器选择
-XX:+UseSerialGC # 使用Serial收集器
-XX:+UseParallelGC # 使用Parallel Scavenge收集器
-XX:+UseConcMarkSweepGC # 使用CMS收集器
-XX:+UseG1GC # 使用G1收集器

# GC日志配置
-XX:+PrintGC # 输出GC日志
-XX:+PrintGCDetails # 输出GC详细日志
-XX:+PrintGCTimeStamps # 输出GC时间戳
-Xloggc:gc.log # 将GC日志输出到文件

# 其他配置
-XX:+HeapDumpOnOutOfMemoryError # 内存溢出时生成堆转储文件
-XX:HeapDumpPath=./heap.dump # 堆转储文件路径
-XX:MaxMetaspaceSize=256m # 元空间最大大小

3. JVM 监控工具

  • jps:查看 Java 进程
  • jstat:查看 JVM 统计信息
  • jinfo:查看和修改 JVM 参数
  • jmap:生成堆转储文件
  • jhat:分析堆转储文件
  • jstack:生成线程转储文件
  • VisualVM:图形化监控工具
  • JConsole:Java 监控和管理控制台
1
2
3
4
5
6
7
8
# 使用jstat监控GC情况
jstat -gc <pid> 1000 10 # 每1秒监控一次,共10次

# 使用jmap生成堆转储
jmap -dump:format=b,file=heap.bin <pid>

# 使用jstack查看线程状态
jstack <pid>

4. 内存泄漏排查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 内存泄漏示例
import java.util.*;

public class MemoryLeakExample {
private static List<Object> list = new ArrayList<>();

public static void main(String[] args) {
while (true) {
list.add(new Object());
// 模拟业务逻辑,但对象始终被list引用,无法被GC回收
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

排查步骤:

  1. 使用 jps 获取进程 ID
  2. 使用 jstat -gc <pid> 观察 GC 情况
  3. 使用 jmap -histo:live <pid> 查看对象分布
  4. 使用 jmap -dump:format=b,file=heap.bin <pid> 生成堆转储
  5. 使用 MAT 或 VisualVM 分析堆转储文件

六、JVM 执行引擎与字节码

1. 解释执行与编译执行

  • 解释执行:逐条解释字节码并执行,启动快但执行慢
  • 编译执行:将字节码编译成本地代码后执行,启动慢但执行快

2. JIT 编译器

  • 客户端编译器 (C1):快速编译,优化较少
  • 服务端编译器 (C2):慢速编译,深度优化
  • 分层编译:JDK 7 引入,结合 C1 和 C2 的优点

3. 字节码指令

Java 字节码由单字节操作码和操作数组成,常见指令:

  • 加载和存储:iload, istore, aload, astore
  • 运算指令:iadd, isub, imul, idiv
  • 类型转换:i2l, i2f, i2d
  • 对象操作:new, getfield, putfield, invokevirtual
  • 控制转移:ifeq, ifne, goto
  • 方法调用和返回:invokestatic, invokevirtual, ireturn
1
2
3
4
5
6
// 简单的字节码示例
public class BytecodeExample {
public int add(int a, int b) {
return a + b;
}
}

对应的字节码:

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

七、JVM 内存模型与线程

1. Java 内存模型 (JMM)

  • 主内存:所有线程共享的内存区域
  • 工作内存:每个线程私有的内存区域
  • 内存间交互操作:lock, unlock, read, load, use, assign, store, write

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

  • 原子性:基本数据类型的访问读写是原子性的,synchronized 块之间的操作也是原子性的
  • 可见性:volatile 保证可见性,synchronized 和 final 也能保证可见性
  • 有序性:volatile 禁止指令重排序,synchronized 保证有序性

3. volatile 关键字

  • 保证变量的可见性
  • 禁止指令重排序
  • 不保证原子性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// volatile示例
public class VolatileExample {
private volatile boolean flag = false;

public void writer() {
flag = true; // 写操作
}

public void reader() {
if (flag) { // 读操作
// 执行相应逻辑
}
}
}

4. synchronized 关键字

  • 保证原子性、可见性和有序性
  • 修饰实例方法:锁是当前实例对象
  • 修饰静态方法:锁是当前类的 Class 对象
  • 修饰代码块:锁是括号里配置的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// synchronized示例
public class SynchronizedExample {
private int count = 0;

public synchronized void increment() {
count++; // 原子操作
}

public void incrementBlock() {
synchronized (this) {
count++; // 同步代码块
}
}
}

5. 锁优化技术

  • 偏向锁:无竞争情况下,消除同步开销
  • 轻量级锁:竞争不激烈时,使用CAS操作避免互斥量开销
  • 自旋锁:竞争激烈时,让线程执行忙循环等待锁释放
  • 锁消除:编译器对不可能存在共享数据竞争的锁进行消除
  • 锁粗化:将多个连续的锁操作合并为一个范围更大的锁

八、JVM 实战问题与解决方案

1. 内存溢出 (OutOfMemoryError)

解决方案

  • 增加堆内存大小 (-Xmx)
  • 检查是否存在内存泄漏
  • 优化对象创建和使用
  • 使用内存分析工具定位问题

2. 栈溢出 (StackOverflowError)

解决方案

  • 增加栈深度 (-Xss)
  • 检查递归调用是否无限循环
  • 优化算法,减少递归深度

3. GC 频繁导致性能下降

解决方案

  • 调整堆大小和新生代/老年代比例
  • 选择合适的垃圾收集器
  • 优化对象创建和生命周期管理

4. 死锁 (Deadlock)

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
// 死锁示例
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread 1 acquired both locks");
}
}
});

Thread t2 = new Thread(() -> {
synchronized (lock2) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread 2 acquired both locks");
}
}
});

t1.start();
t2.start();
}
}

解决方案

  • 使用 jstack 检测死锁
  • 保证锁的获取顺序一致
  • 使用超时机制 (tryLock)
  • 减少同步代码块的范围

九、JVM 版本特性

1. JDK 8 特性

  • 元空间取代永久代
  • Lambda 表达式和函数式编程
  • Stream API
  • 新的日期时间 API

2. JDK 11 特性

  • ZGC 垃圾收集器(实验性)
  • HTTP Client API
  • 局部变量类型推断 (var)
  • 启动单文件源代码程序

3. JDK 17 特性

  • 密封类 (Sealed Classes)
  • 模式匹配 switch
  • 新的垃圾收集器 (Shenandoah 成为标准)
  • 移除 Applet API

总结

JVM 是 Java 技术的核心,深入理解 JVM 对于编写高性能、稳定的 Java 应用程序至关重要。本文全面梳理了 JVM 的核心知识点,包括:

  1. JVM 体系结构:了解 JVM 的组成部分和工作原理
  2. 运行时数据区:掌握各内存区域的作用和特点
  3. 垃圾收集机制:理解 GC 原理、算法和收集器
  4. 类加载机制:熟悉类加载过程和双亲委派模型
  5. 性能监控与调优:掌握 JVM 参数和监控工具的使用
  6. 执行引擎与字节码:了解 JVM 执行原理和字节码指令
  7. 内存模型与线程:理解 JMM 和线程同步机制
  8. 实战问题与解决方案:掌握常见问题的排查和解决方法

JVM 常见面试题及频繁 GC 场景排查

一、JVM 常见面试题

1. JVM 内存结构

问:JVM 内存分为哪些区域?各自的作用是什么?


JVM 内存主要分为以下几个区域:

  1. 程序计数器:线程私有,记录当前线程执行的字节码指令地址
  2. Java 虚拟机栈:线程私有,存储栈帧,每个方法对应一个栈帧
  3. 本地方法栈:为 Native 方法服务
  4. Java 堆:线程共享,存放对象实例和数组,是 GC 管理的主要区域
  5. 方法区:线程共享,存储类信息、常量、静态变量等(JDK 8+ 称为元空间)
  6. 运行时常量池:方法区的一部分,存放编译期生成的字面量和符号引用

2. 垃圾回收机制

问:如何判断对象是否可以被回收?


主要有两种方法:

  1. 引用计数法:给对象添加引用计数器,有引用时计数器加1,引用失效时减1,为0时可回收(但无法解决循环引用问题)
  2. 可达性分析:从 GC Roots 对象作为起点,向下搜索,不可达的对象将被回收

GC Roots 对象包括

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

3. 垃圾收集算法

问:常见的垃圾收集算法有哪些?各自的特点是什么?

  1. 标记-清除:先标记要回收的对象,然后清除(产生内存碎片)
  2. 复制:将内存分为两块,每次使用一块,将存活对象复制到另一块(浪费空间,无碎片)
  3. 标记-整理:标记存活对象,然后让所有存活对象向一端移动(无碎片,适合老年代)
  4. 分代收集:根据对象存活周期将内存划分为新生代和老年代,采用不同的收集算法

4. 垃圾收集器

问:常见的垃圾收集器有哪些?各自的特点和适用场景是什么?

  1. Serial:单线程,新生代收集器,复制算法,适合客户端应用
  2. ParNew:Serial 的多线程版本,适合多核服务器
  3. Parallel Scavenge:吞吐量优先的新生代收集器,复制算法
  4. Serial Old:Serial 的老年代版本,标记-整理算法
  5. Parallel Old:Parallel Scavenge 的老年代版本,标记-整理算法
  6. CMS:以获取最短回收停顿时间为目标,标记-清除算法,适合响应时间敏感的应用
  7. G1:面向服务端应用的垃圾收集器,整体基于标记-整理,局部基于复制算法
  8. ZGC:JDK 11+ 的低延迟垃圾收集器,适合大内存应用
  9. Shenandoah:低暂停时间的垃圾收集器,与 ZGC 竞争

5. 类加载机制

问:什么是双亲委派模型?为什么要使用双亲委派模型?


双亲委派模型是 Java 类加载器的工作机制:

  1. 类加载器收到加载请求时,不会自己尝试加载,而是委派给父类加载器
  2. 只有当父类加载器无法完成加载时,子加载器才会尝试自己加载

优点

  • 保证 Java 核心库的类型安全
  • 避免类的重复加载
  • 保证类的唯一性

6. JVM 调优

问:常用的 JVM 调优参数有哪些?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 堆内存设置
-Xms512m # 初始堆大小
-Xmx1024m # 最大堆大小
-Xmn256m # 新生代大小
-XX:NewRatio=2 # 老年代与新生代的比例
-XX:SurvivorRatio=8 # Eden区与Survivor区的比例

# 垃圾收集器选择
-XX:+UseG1GC # 使用G1收集器
-XX:+UseConcMarkSweepGC # 使用CMS收集器

# GC日志配置
-XX:+PrintGC # 输出GC日志
-XX:+PrintGCDetails # 输出GC详细日志
-XX:+PrintGCTimeStamps # 输出GC时间戳
-Xloggc:gc.log # 将GC日志输出到文件

# 其他配置
-XX:+HeapDumpOnOutOfMemoryError # 内存溢出时生成堆转储文件
-XX:HeapDumpPath=./heap.dump # 堆转储文件路径
-XX:MaxMetaspaceSize=256m # 元空间最大大小

7. 内存泄漏与内存溢出

问:什么是内存泄漏?如何排查内存泄漏?


内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费。

排查方法

  1. 使用 jps 查看 Java 进程
  2. 使用 jstat -gc <pid> 观察 GC 情况
  3. 使用 jmap -histo:live <pid> 查看对象分布
  4. 使用 jmap -dump:format=b,file=heap.bin <pid> 生成堆转储
  5. 使用 MAT 或 VisualVM 分析堆转储文件

二、频繁 GC 场景排查实战

场景描述

假设我们有一个线上电商应用,最近出现频繁的 Full GC,导致应用响应变慢,偶尔出现超时情况。

排查步骤

1. 确认 GC 问题

首先使用 jps 命令查看 Java 进程 ID:

1
jps -l

然后使用 jstat 命令查看 GC 情况:

1
jstat -gcutil <pid> 1000 10  # 每1秒输出一次GC情况,共10次

输出示例:

1
2
3
4
5
6
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00 99.74 68.13 95.61 94.23 91.45 2120 185.421 153 85.234 270.655
0.00 99.74 82.17 95.61 94.23 91.45 2120 185.421 153 85.234 270.655
0.00 99.74 92.58 95.61 94.23 91.45 2120 185.421 153 85.234 270.655
0.00 99.74 98.73 95.61 94.23 91.45 2121 185.433 153 85.234 270.667
0.00 0.00 7.25 96.08 94.23 91.45 2122 185.445 154 85.456 270.901

从输出可以看到:

  • 老年代使用率 (O) 持续在 95% 以上
  • Full GC 次数 (FGC) 较多,且 Full GC 时间 (FGCT) 较长
  • 新生代 GC 频繁但效果不明显

2. 生成堆转储文件

使用 jmap 命令生成堆转储文件:

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

3. 分析堆转储文件

使用 Eclipse MAT 或 VisualVM 分析堆转储文件:

  1. 打开 MAT,加载 heap.bin 文件
  2. 查看 “Histogram”(直方图),按对象数量或大小排序
  3. 发现有一个 HashMap 对象特别大,占用了大量内存
  4. 查看该 HashMap 的引用链,发现是一个缓存对象

4. 分析代码

找到对应的缓存实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ProductCache {
private static Map<Long, Product> cache = new HashMap<>();

public static Product getProduct(Long id) {
if (!cache.containsKey(id)) {
Product product = loadFromDB(id);
cache.put(id, product);
}
return cache.get(id);
}

// 缺少缓存清理机制
}

发现问题:缓存没有大小限制和清理策略,随着时间推移会无限增长,导致内存泄漏。

5. 查看 GC 日志

如果配置了 GC 日志,查看 GC 日志文件:

1
2
# 查看GC日志
tail -f gc.log

GC 日志示例:

1
2
3
4
5
2023-07-15T10:23:45.123+0800: 185.433: [Full GC (Allocation Failure) 
[PSYoungGen: 2048K->0K(256000K)]
[ParOldGen: 954621K->954621K(1024000K)] 956669K->954621K(1280000K)
[Metaspace: 42345K->42345K(1085440K)], 1.2345678 secs]
[Times: user=4.56 sys=0.12, real=1.23 secs]

从 GC 日志可以看到:

  • Full GC 原因是 “Allocation Failure”(分配失败)
  • 老年代回收前后大小几乎没有变化,说明有很多对象无法被回收
  • Full GC 耗时较长(1.23秒)

6. 实时监控

使用 jstack 查看线程状态:

1
jstack <pid>

查看是否有线程阻塞或死锁情况。

7. 解决方案

根据分析结果,提出解决方案:

  1. 短期解决方案

    • 增加堆内存大小:-Xmx4g -Xms4g
    • 增加老年代比例:-XX:NewRatio=3
    • 重启应用,暂时缓解问题
  2. 长期解决方案

    • 修改缓存实现,添加大小限制和过期策略
    • 使用 WeakHashMap 或专门的缓存框架(如 Caffeine、Ehcache)
    • 添加缓存监控和统计

修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ProductCache {
// 使用LRU缓存,最大1000个元素
private static Map<Long, Product> cache = new LinkedHashMap<Long, Product>(1000, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Long, Product> eldest) {
return size() > 1000;
}
};

public static Product getProduct(Long id) {
synchronized (cache) {
if (!cache.containsKey(id)) {
Product product = loadFromDB(id);
cache.put(id, product);
}
return cache.get(id);
}
}

// 添加定期清理过期缓存的方法
public static void cleanExpiredCache() {
// 清理逻辑
}
}

8. 验证解决方案

部署修改后的代码,继续监控 GC 情况:

1
jstat -gcutil <pid> 1000 10

输出示例(修复后):

1
2
3
4
5
6
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00 50.23 25.67 45.61 94.23 91.45 120 12.421 3 1.234 13.655
0.00 50.23 45.89 45.61 94.23 91.45 120 12.421 3 1.234 13.655
0.00 50.23 68.34 45.61 94.23 91.45 120 12.421 3 1.234 13.655
0.00 50.23 89.12 45.61 94.23 91.45 120 12.421 3 1.234 13.655
0.00 0.00 5.25 46.08 94.23 91.45 121 12.433 3 1.234 13.667

从输出可以看到:

  • 老年代使用率 (O) 降低到 45% 左右
  • Full GC 次数 (FGC) 大大减少
  • GC 时间显著缩短

总结

频繁 GC 问题的排查流程:

  1. 使用 jps 获取进程 ID
  2. 使用 jstat 观察 GC 情况
  3. 使用 jmap 生成堆转储文件
  4. 使用 MAT 或 VisualVM 分析堆转储
  5. 查看 GC 日志(如果有)
  6. 使用 jstack 查看线程状态
  7. 分析代码,找到问题根源
  8. 制定并实施解决方案
  9. 验证解决方案效果

通过系统化的排查方法,可以有效地定位和解决 JVM 性能问题,提高应用的稳定性和性能。