为什么要学JVM?
作为Java程序员,你可能已经写过很多代码,但有没有想过:为什么Java代码能"一次编写,到处运行"?为什么有时候会突然报
OutOfMemoryError?为什么同一个程序在不同机器上性能差异这么大?
答案就在JVM。 掌握JVM,你才能从"会写代码"进阶到"理解底层",从被动解决问题到主动优化性能。
一、为什么需要JVM------Java跨平台的秘密武器
1.1 生活化类比:餐厅的翻译官
想象这样一个场景:
- Java程序员 = 写出一份通用菜谱的人
- 不同操作系统 = 只听得懂方言的厨师(Windows厨师、Linux厨师、macOS厨师)
- JVM = 精通所有方言的翻译官
工作流程是这样的:
程序员写出通用菜谱(.java)
↓ 编译
翻译官(JVM)拿到通用菜谱(.class)
↓ 翻译
对应方言的厨师(OS)能看懂的指令
↓ 执行
做出美味的菜肴(程序运行)
核心结论:JVM就像一个"中间层",屏蔽了底层操作系统的差异,让Java代码可以在任何安装了JVM的设备上运行。
1.2 JVM的三大核心职责
| 职责 | 作用 | 生活类比 |
|---|---|---|
| 类加载 | 加载、链接、初始化.class文件 | 餐厅接单,准备食材 |
| 内存管理 | 分配和回收内存 | 餐厅厨房空间管理 |
| 执行引擎 | 解释或编译字节码 | 厨师按照菜谱做菜 |
二、JVM整体架构------一个完整的"生产车间"
2.1 完整架构图
Java 程序运行环境
操作系统与硬件
JVM 虚拟机(运行时)
编译阶段
执行引擎内部
运行时数据区
javac编译
加载.class文件
读写
调用
操作系统
Windows/Linux/macOS
.java源代码
.class字节码文件
类加载器子系统
运行时数据区
执行引擎
本地方法接口
本地方法库
如C/C++库
程序计数器 PC Register
线程私有
记录执行位置
解释器
逐行解释执行
JIT编译器
编译热点代码为本地代码
垃圾回收器 GC
自动回收无用对象
堆 Heap
线程共享
存储对象实例
虚拟机栈 JVM Stack
线程私有
存储方法调用
本地方法栈 Native Stack
线程私有
存储本地方法调用
硬件
CPU/内存/硬盘
性能监控分析器
Profiler
方法区 Method Area
线程共享
存储类信息、常量
2.2 各模块详解
🔸 类加载器子系统(招聘与培训部门)
负责将编译好的.class文件加载到内存中,并进行验证、准备和初始化。
🔸 运行时数据区(餐厅工作空间)
JVM在运行时需要管理的内存区域,就像餐厅需要合理规划厨房空间。
🔸 执行引擎(厨房团队)
真正执行代码的地方,包含解释器、JIT编译器和垃圾回收器。
🔸 本地方法接口与库(外包团队)
当需要调用操作系统底层功能时,通过JNI(Java Native Interface)调用C/C++编写的本地方法。
三、运行时数据区详解------JVM的"空间管理艺术"
3.1 内存区域全景图
JVM 运行时数据区
线程共享区域(所有线程共用)
堆 Heap
❌ 可能OutOfMemoryError
GC主要工作区域
方法区 Method Area
❌ 可能OutOfMemoryError
存储类信息
线程私有区域(每个线程一份)
程序计数器
PC Register
✅ 不会OOM
虚拟机栈
JVM Stack
❌ 可能StackOverflowError
本地方法栈
Native Stack
❌ 可能StackOverflowError
3.2 各区域详细说明
📍 程序计数器(PC Register)
作用:记录当前线程执行到了哪一行字节码指令。
特点:
- ✅ 线程私有,每个线程都有自己的计数器
- ✅ 唯一不会发生内存溢出的区域
- ✅ 非常小,只占几个字节
类比:就像厨师在菜谱上用书签标记"做到第几步了",这样如果被中断,回来后能继续做。
📍 虚拟机栈(JVM Stack)
作用:存储方法调用的信息,包括局部变量、操作数栈、方法出口等。
特点:
- 🔄 每调用一个方法,就创建一个栈帧(Stack Frame)
- 🔄 方法执行完毕,栈帧弹出
- ❌ 栈深度过大会抛出
StackOverflowError - ❌ 栈无法扩展会抛出
OutOfMemoryError
类比:
想象一个叠盘子系统:
- 调用main方法 = 放入第一个盘子
- 调用方法A = 在main盘子上面放A盘子
- 调用方法B = 在A盘子上面放B盘子
- 方法B执行完 = 拿走B盘子,回到A盘子
- 方法A执行完 = 拿走A盘子,回到main盘子
代码示例:栈溢出场景
java
/**
* 栈溢出示例:无限递归调用
* 默认栈深度约10000次就会StackOverflowError
*/
public class StackOverflowDemo {
public static void main(String[] args) {
recursion(1);
}
private static int recursion(int depth) {
System.out.println("当前递归深度: " + depth);
return recursion(depth + 1); // 无限递归
}
}
// 运行结果:
// java.lang.StackOverflowError
📍 堆(Heap)
作用 :存储所有对象实例 和数组。
特点:
- 🔄 线程共享,所有线程都可以访问
- 🔄 GC的主要工作区域
- ❌ 对象过多或过大会抛出
OutOfMemoryError: Java heap space - 🔄 可以通过
-Xms和-Xmx参数调整大小
堆的分代结构:
堆 Heap
新生代
Eden区
新对象分配
Survivor 0区
幸存者区
Survivor 1区
幸存者区
新生代 Young Generation
老年代 Old Generation
各代说明:
| 区域 | 比例 | 说明 | 对象特点 |
|---|---|---|---|
| Eden区 | 8/10新生代 | 新对象主要分配区 | 朝生夕死,GC频繁 |
| Survivor区 | 1/10新生代 | 存放经过GC后幸存的对象 | 每次GC后对象会移动 |
| 老年代 | 剩余堆空间 | 存放长期存活对象 | GC次数少 |
代码示例:堆溢出场景
java
/**
* 堆溢出示例:不断创建大对象
*/
import java.util.ArrayList;
import java.util.List;
public class HeapOOMDemo {
// JVM参数设置: -Xms10m -Xmx10m
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
// 每次分配1MB的对象,永不释放
list.add(new byte[1024 * 1024]);
System.out.println("已分配对象数: " + list.size());
}
}
}
// 运行结果:
// java.lang.OutOfMemoryError: Java heap space
📍 方法区(Method Area)
作用:存储类信息、常量、静态变量、即时编译器编译后的代码等。
特点:
- 🔄 线程共享
- 🔄 在JDK 1.8及以后称为"元空间"(Metaspace)
- ❌ 类加载过多会抛出
OutOfMemoryError: Metaspace - 🔄 1.8版本之前在堆中,1.8之后使用本地内存
类比:就像餐厅的"配方库",存储所有菜谱的标准做法、调料配方等。
3.3 常见内存问题对比表
| 异常类型 | 触发原因 | 解决方案 |
|---|---|---|
StackOverflowError |
递归太深、方法调用层级过多 | 减小递归深度、改用迭代、增大栈大小 -Xss |
OutOfMemoryError: Java heap space |
堆内存不足、对象过多/过大 | 增大堆大小 -Xmx、优化对象创建、检查内存泄漏 |
OutOfMemoryError: Metaspace |
加载类过多、动态代理泛滥 | 增大元空间大小 -XX:MaxMetaspaceSize、检查类加载泄漏 |
四、类加载机制------JVM的"招聘与培训"体系
4.1 类加载生命周期
一个类从被加载到内存,到被卸载出内存,经历了完整的一生:
加载 Loading
读取.class文件
创建Class对象
验证 Verification
确保文件格式正确
字节码安全
准备 Preparation
为静态变量分配内存
设置默认初始值
解析 Resolution
将符号引用转换为
直接引用
初始化 Initialization
执行方法
给静态变量赋初始值
使用 Using
卸载 Unloading
4.2 关键阶段详解
🔸 准备阶段 vs 初始化阶段(重要!)
java
public class StaticVariableDemo {
// 准备阶段:value = 0(int的默认值)
// 初始化阶段:value = 100(代码中指定的初始值)
private static int value = 100;
// 准备阶段:constant = "Hello"(常量在编译期就确定了)
// 初始化阶段:无需执行
private static final String constant = "Hello";
public static void main(String[] args) {
System.out.println(value); // 输出: 100
System.out.println(constant); // 输出: Hello
}
}
注意:准备阶段只会赋"默认初始值"(0、null、false),而不是"代码初始值"!
🔸 初始化的触发条件(面试常考)
类初始化的5种情况:
- 使用
new、getstatic、putstatic、invokestatic指令 - 对类进行反射调用
- 初始化子类时,父类会先初始化
- 虚拟机启动时,初始化包含main方法的类
- 使用动态语言支持时(如JRuby)
4.3 双亲委派模型------类加载的"等级制度"
🏗️ 类加载器层次结构
委托请求
委托请求
委托请求
启动类加载器
Bootstrap ClassLoader
加载JDK核心类
如String/Object/System
扩展类加载器
Extension ClassLoader
加载扩展类
如javax.*
应用程序类加载器
Application ClassLoader
加载用户类路径
即classpath
自定义类加载器
User ClassLoader
按需自定义加载逻辑
🔄 双亲委派工作流程
不能
不能
能
能
不能
收到类加载请求
比如加载java.lang.String
能否自己加载?
委托父加载器
父加载器能否加载?
继续向上委托
父加载器完成加载
到达顶层的启动类加载器
能否加载?
向下传递
子加载器尝试加载
🎯 双亲委派的优势
| 优势 | 说明 | 生活类比 |
|---|---|---|
| 避免重复加载 | 如果父加载器已经加载过,子加载器不会重复加载 | 就像图书馆,一本好书只买一次 |
| 保证安全 | 核心类由顶层加载,防止被篡改 | 就像国家机密由最高层管理,避免被破坏 |
经典问题 :能否自己写一个 java.lang.String 类来替换JDK的String?
答案:不能!因为双亲委派机制会让启动类加载器优先加载JDK自己的String,你自定义的String永远不会被使用。
💥 破坏双亲委派的场景
双亲委派不是绝对的,某些场景需要打破它:
| 场景 | 原因 | 示例 |
|---|---|---|
| SPI机制 | 需要加载第三方实现类 | JDBC驱动加载、JNDI |
| 热部署 | 需要重新加载类而不重启JVM | Tomcat、Web容器 |
| OSGi | 需要模块化加载,类隔离 | 模块化框架 |
代码示例:自定义类加载器打破双亲委派
java
import java.io.*;
/**
* 自定义类加载器:破坏双亲委派模型
* 将指定的.class文件加载到JVM中
*/
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 = loadByte(name);
// 将字节数组转为Class对象
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
private byte[] loadByte(String name) throws Exception {
// 将包名替换为路径
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
五、执行引擎------JVM的"执行团队"
5.1 执行引擎的两种工作模式
字节码
执行引擎
解释器 Interpreter
JIT编译器 Just-In-Time Compiler
逐行解释执行
启动快
执行慢
编译成本地代码
启动慢
执行快
适合热点代码
程序运行
🔸 解释器(Interpreter)
- 工作方式:逐条解释字节码并执行
- 优点:启动快,立即可以运行
- 缺点:执行效率低
- 适用场景:程序刚开始运行,或者代码不会被频繁执行
🔸 JIT编译器(Just-In-Time Compiler)
- 工作方式:将热点代码(被频繁调用的方法)编译成本地机器码
- 优点:执行效率接近C/C++,性能极高
- 缺点:编译需要时间,启动慢
- 适用场景:长时间运行的服务器应用
热点代码识别:
- 方法被多次调用(方法计数器)
- 方法体中的循环体被多次执行(循环计数器)
5.2 常见JIT编译器
| 编译器 | 版本 | 特点 |
|---|---|---|
| Client Compiler (C1) | -Xclient | 编译快,优化少,适合客户端应用 |
| Server Compiler (C2) | -Xserver | 编译慢,优化激进,适合服务器应用 |
| Graal | JDK9+ | 新一代编译器,用Java编写,性能更好 |
分层编译(Tiered Compilation):JVM默认采用分层编译,结合C1和C2的优势。
六、垃圾回收机制------JVM的"自动清洁系统"
6.1 为什么要垃圾回收?
在C/C++中,程序员需要手动 malloc 分配内存,手动 free 释放内存。这带来两个问题:
- 内存泄漏:忘记释放内存,程序越跑越慢
- 悬空指针:释放了还在使用的内存,程序崩溃
Java的GC:自动识别哪些对象不再使用,并回收它们的内存空间。
6.2 如何判断对象是垃圾?
方法1:引用计数法(Reference Counting)
java
// 引用计数示例
Object obj = new Object(); // 计数器 = 1
Object obj2 = obj; // 计数器 = 2
obj = null; // 计数器 = 1
obj2 = null; // 计数器 = 0 → 可回收
缺点:无法解决循环引用问题
java
// 循环引用问题:计数器永远为1,但实际已无外部引用
A a = new A();
B b = new B();
a.b = b; // a持有b的引用
b.a = a; // b持有a的引用
a = null;
b = null; // 但a和b互相引用,计数器为1,无法回收!
方法2:可达性分析(Reachability Analysis)✅ JVM采用
GC Roots 根节点
虚拟机栈中引用的对象
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
对象A
对象B
对象C
对象D
对象E
无法从GC Roots到达
可回收
GC Roots包括:
- 虚拟机栈中引用的对象(方法中的局部变量)
- 方法区中静态属性引用的对象(static变量)
- 方法区中常量引用的对象(final常量)
- 本地方法栈中JNI引用的对象
结论:如果一个对象无法从任何GC Roots到达,则判定为可回收对象。
6.3 三大经典GC算法
🟦 算法1:标记-清除(Mark-Sweep)
内存示意图
已使用
已使用
空闲
已使用
空闲
空闲
已使用
标记-清除算法
标记阶段
从GC Roots遍历
标记存活对象
清除阶段
回收未标记对象
特点:
- ✅ 简单、高效
- ❌ 会产生内存碎片
适用场景:老年代(CMS收集器)
🟩 算法2:复制(Copying)
复制算法 - 新生代
Eden区
8份
Survivor 0区
1份
Survivor 1区
1份
GC前
Eden满对象
S0有少量对象
GC后
存活对象复制到S1
Eden和S0清空
特点:
- ✅ 无内存碎片
- ✅ 回收效率高
- ❌ 内存利用率低(需要浪费一半空间)
适用场景:新生代(对象存活率低)
🟨 算法3:标记-整理(Mark-Compact)
内存示意图 - 整理后
已使用
已使用
已使用
已使用
空闲
空闲
空闲
内存示意图 - 整理前
已使用
已使用
空闲
已使用
空闲
空闲
已使用
标记-整理算法
标记阶段
标记存活对象
整理阶段
将存活对象向一端移动
清除边界外的内存
特点:
- ✅ 无内存碎片
- ✅ 内存利用率高
- ❌ 移动对象开销大
适用场景:老年代(对象存活率高)
6.4 三大算法对比
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 标记-清除 | 简单高效 | 产生内存碎片 | 老年代(CMS) |
| 复制 | 无碎片、速度快 | 空间利用率低 | 新生代 |
| 标记-整理 | 无碎片、利用率高 | 移动开销大 | 老年代(Parallel/G1) |
七、常见垃圾收集器
7.1 收集器演化路线
新一代收集器
老年代收集器
新生代收集器
Serial GC
单线程
ParNew GC
多线程
Parallel Scavenge GC
关注吞吐量
Serial Old GC
单线程
CMS GC
并发低停顿
Parallel Old GC
关注吞吐量
G1 GC
Region化
低延迟
ZGC/Shenandoah GC
极低延迟
7.2 常见收集器对比
| 收集器 | 类型 | 优缺点 | 适用场景 | 使用参数 |
|---|---|---|---|---|
| Serial GC | 串行 | 简单但效率低 | 单核、小内存(客户端) | -XX:+UseSerialGC |
| Parallel GC | 并行 | 吞吐量高 | 批处理、后台计算 | -XX:+UseParallelGC |
| CMS GC | 并发 | 低延迟但可能内存碎片 | 重视响应时间 | -XX:+UseConcMarkSweepGC |
| G1 GC | 分Region | 可预测停顿 | 大内存、低延迟 | -XX:+UseG1GC |
| ZGC | 并发 | 极低延迟 | 超大内存(TB级) | -XX:+UseZGC |
7.3 G1 GC详解(当前主流)
G1(Garbage-First)是JDK 7u4引入的垃圾收集器,目标是替代CMS。
核心特点:
- 可预测停顿时间:可以设置"期望的最大停顿时间"
- Region化内存布局:将堆划分为多个Region,不再区分新生代/老年代
- 并行与并发:充分利用多核CPU优势
G1的内存布局 - Region化
Region 0
Eden
Region 1
Eden
Region 2
Survivor
Region 3
Old
Region 4
Humongous
大对象
Region 5
Free
空闲
Region 6
Eden
Region 7
Old
G1的核心思想:优先回收垃圾最多的Region,因此得名"Garbage-First"。
八、JVM优化实践------从"能用"到"好用"
8.1 常用JVM参数配置
🔸 内存参数
bash
# 堆内存设置
-Xms2g # 初始堆大小
-Xmx2g # 最大堆大小,建议与Xms相等,避免动态扩展开销
-Xmn800m # 新生代大小(不推荐推荐使用-XX:NewRatio)
-XX:NewRatio=2 # 新生代:老年代 = 1:2,新生代占1/3
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
# 元空间设置
-XX:MetaspaceSize=256m # 元空间初始大小
-XX:MaxMetaspaceSize=512m # 元空间最大大小
🔸 垃圾收集器参数
bash
# Serial GC
-XX:+UseSerialGC
# Parallel GC(JDK8默认)
-XX:+UseParallelGC
-XX:ParallelGCThreads=4 # GC线程数
# CMS GC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70 # 老年代使用到70%时触发CMS
# G1 GC(JDK9默认)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 期望最大停顿时间200ms
-XX:G1HeapRegionSize=16m # Region大小
# ZGC(JDK11+)
-XX:+UseZGC
🔸 其他参数
bash
-XX:+PrintGCDetails # 打印GC详情
-XX:+PrintGCTimeStamps # 打印GC时间戳
-Xlog:gc*:file=gc.log # GC日志输出到文件
-XX:+HeapDumpOnOutOfMemoryError # OOM时自动dump堆转储
-XX:HeapDumpPath=./heap.hprof # 堆转储文件路径
8.2 GC收集器选择策略
| 场景 | 推荐收集器 | JVM参数 |
|---|---|---|
| 单核CPU、小内存 | Serial GC | -XX:+UseSerialGC |
| 批处理、后台计算 | Parallel GC | -XX:+UseParallelGC |
| Web应用、响应时间敏感 | G1 GC | -XX:+UseG1GC |
| 超大内存(>32G)、极低延迟 | ZGC | -XX:+UseZGC |
| 低延迟、需要极短停顿 | Shenandoah | -XX:+UseShenandoahGC |
8.3 代码层面优化建议
✅ 建议1:避免创建不必要的大对象
java
// ❌ 错误示例:每次循环都创建大对象
for (int i = 0; i < 10000; i++) {
byte[] data = new byte[1024 * 1024]; // 1MB
process(data);
}
// ✅ 正确示例:复用对象
byte[] data = new byte[1024 * 1024];
for (int i = 0; i < 10000; i++) {
Arrays.fill(data, (byte)0); // 清空数据
process(data);
}
✅ 建议2:合理使用对象池(注意泄漏风险)
java
// 使用Apache Commons Pool2的对象池
import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPool;
public class ObjectPoolDemo {
private ObjectPool<MyObject> pool = new GenericObjectPool<>(new MyObjectFactory());
public void process() throws Exception {
MyObject obj = pool.borrowObject(); // 从池中获取
try {
// 使用对象
obj.doSomething();
} finally {
pool.returnObject(obj); // 归还到池中
}
}
}
✅ 建议3:使用基本类型代替包装类型
java
// ❌ 错误:创建大量Integer对象
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add(i); // 自动装箱,创建Integer对象
}
// ✅ 正确:使用基本类型
int[] array = new int[10000];
for (int i = 0; i < 10000; i++) {
array[i] = i;
}
✅ 建议4:避免频繁创建临时字符串
java
// ❌ 错误:每次循环都创建新字符串
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // 创建大量临时String对象
}
// ✅ 正确:使用StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();
九、常见问题诊断------排查JVM问题的利器
9.1 OOM的5种典型场景
场景1:堆内存溢出
java
/**
* 场景:堆内存溢出
* 原因:对象过多或过大
* JVM参数:-Xms10m -Xmx10m
*/
import java.util.*;
public class HeapOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 1MB
}
}
}
// 报错:java.lang.OutOfMemoryError: Java heap space
场景2:栈溢出
java
/**
* 场景:栈溢出
* 原因:递归过深或方法层级过多
* JVM参数:-Xss128k
*/
public class StackOverflow {
private int depth = 0;
public void recursion() {
depth++;
recursion(); // 无限递归
}
public static void main(String[] args) {
new StackOverflow().recursion();
}
}
// 报错:java.lang.StackOverflowError
场景3:元空间溢出
java
/**
* 场景:元空间溢出
* 原因:加载类过多、动态代理泛滥
* JVM参数:-XX:MaxMetaspaceSize=10m
*/
import net.sf.cglib.proxy.Enhancer;
public class MetaspaceOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create(); // 每次创建新类
}
}
static class OOMObject {}
}
// 报错:java.lang.OutOfMemoryError: Metaspace
场景4:直接内存溢出
java
/**
* 场景:直接内存溢出
* 原因:堆外内存分配过多
* JVM参数:-XX:MaxDirectMemorySize=10m
*/
import java.nio.ByteBuffer;
public class DirectMemoryOOM {
public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
while (true) {
list.add(ByteBuffer.allocateDirect(1024 * 1024)); // 直接内存
}
}
}
// 报错:java.lang.OutOfMemoryError: Direct buffer memory
场景5:GC开销超限
java
/**
* 场景:GC开销超限
* 原因:GC回收时间占比过高(>98%)
* JVM参数:-Xms10m -Xmx10m -XX:+UseG1GC
*/
public class GCOverheadOOM {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]);
list.remove(0); // 保留引用,但GC频繁
}
}
}
// 报错:java.lang.OutOfMemoryError: GC overhead limit exceeded
9.2 诊断工具使用指南
🔸 jstat:JVM统计信息监控工具
bash
# 语法:jstat -options
# 示例1:查看GC统计信息
jstat -gcutil <pid> 1s 10
# 输出:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 0.00 78.45 45.23 98.12 95.34 12 0.234 3 1.567 1.801
# 说明:
# S0/S1: Survivor 0/1 区使用率
# E: Eden区使用率
# O: 老年代使用率
# M: 元空间使用率
# YGC: Young GC次数
# YGCT: Young GC总耗时
# FGC: Full GC次数
# FGCT: Full GC总耗时
# GCT: GC总耗时
# 示例2:每秒输出一次,持续输出
jstat -gc <pid> 1000
🔸 jmap:Java内存映射工具
bash
# 示例1:查看堆内存配置
jmap -heap <pid>
# 示例2:导出堆转储文件(OOM排查必备)
jmap -dump:format=b,file=heap.hprof <pid>
# 示例3:查看堆中对象数量统计
jmap -histo:live <pid> | head -20
# 输出:
# num #instances #bytes class name
# 1: 4521 8234567 [Ljava.lang.Object;
# 2: 34521 5678901 java.util.HashMap$Node
# 3: 234567 4567890 java.lang.String
🔸 jstack:Java堆栈跟踪工具
bash
# 示例1:查看线程堆栈
jstack <pid>
# 示例2:检测死锁
jstack -l <pid>
# 输出示例(死锁):
# Found one Java-level deadlock:
# =============================
# "Thread-1":
# waiting to lock monitor 0x00007f8a3400b598 (object 0x00000006d72aa8b8, a java.lang.Object),
# which is held by "Thread-0"
# "Thread-0":
# waiting to lock monitor 0x00007f8a3400b7c8 (object 0x00000006d72aa8c0, a java.lang.Object),
# which is held by "Thread-1"
🔸 VisualVM:可视化监控工具
使用方式:
- JDK自带,直接运行
jvisualvm - 或下载最新版:https://visualvm.github.io/
主要功能:
- 📊 实时监控CPU、内存、线程
- 📈 GC性能分析
- 📋 线程Dump分析
- 🔍 堆Dump分析
界面截图说明:
┌─────────────────────────────────────────┐
│ VisualVM │
├─────────────────────────────────────────┤
│ 应用程序列表: │
│ ✓ myapp (pid: 12345) │
│ ✓ idea (pid: 67890) │
├─────────────────────────────────────────┤
│ 监控标签页: │
│ - 概览:运行时间、JVM版本 │
│ - 监控:CPU、堆、类、线程实时图表 │
│ - 线程:线程状态、堆栈 │
│ - Profiler:CPU/内存分析 │
└─────────────────────────────────────────┘
9.3 排查OOM问题的标准流程
有
没有
泄漏
溢出
发现OOM异常
是否有堆转储文件?
使用VisualVM/MAT分析
配置OOM自动Dump
-XX:+HeapDumpOnOutOfMemoryError
等待问题复现
查找大对象
是内存泄漏还是内存溢出?
找到泄漏代码并修复
增大堆内存或优化代码
验证修复
十、总结与展望
🎯 核心知识点回顾
| 模块 | 核心概念 | 关键词 |
|---|---|---|
| JVM基础 | 跨平台、屏蔽差异 | 类加载、内存管理、执行引擎 |
| 内存结构 | 堆、栈、方法区、程序计数器 | 线程私有/共享、OOM、StackOverflow |
| 类加载 | 双亲委派模型 | Bootstrap/Extension/App/Custom |
| 执行引擎 | 解释器 + JIT编译 | 热点代码、分层编译 |
| 垃圾回收 | 可达性分析、GC算法 | 标记-清除、复制、标记-整理 |
| GC收集器 | Serial/Parallel/CMS/G1/ZGC | 吞吐量、低延迟、Region |
| JVM调优 | 参数配置、工具使用 | -Xms/-Xmx、jstat/jmap/jstack |
🚀 进阶学习路线
-
初级阶段(本教程内容)
- 理解JVM基本架构
- 掌握内存区域和GC算法
- 会使用jstat/jmap/jstack
-
中级阶段
- 深入理解GC调优
- 掌握MAT、JProfiler等工具
- 学习类加载机制和反射
-
高级阶段
- 研究JIT编译优化
- 学习JVM源码
- 理解并发编程与JVM的关系
💡 最后的建议
"知其然,更要知其所以然"
掌握JVM不仅能让你在出现OOM、GC问题时从容应对,更能让你:
- 写出更高效的代码
- 更好地理解Java并发编程
- 在性能优化时有的放矢
推荐资源:
- 📚 书籍:《深入理解Java虚拟机》(周志明著)
- 🎥 视频:B站 JVM 教程(尚硅谷、黑马)
- 🌐 官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/
愿你从今天开始,从"会用Java"进阶到"理解Java底层"! 🎉
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、转发,让更多人一起学习JVM!