JVM虚拟机深度学习文档
快速了解JVM 虚拟机:从小白到工作实战
适用对象:Java 初学者、后端开发、准备面试、想真正理解线上问题的人。
学习目标:看完后能讲清楚 JVM 的运行原理,能看懂常见 JVM 参数,能用 JVM 思维分析工作中的性能、内存、GC、类加载问题。
推荐 Java 版本:JDK 8、JDK 11、JDK 17 都适用。本文会重点说明 JDK 8 和 JDK 11+ 的差异。
1. 先用一句话理解 JVM
JVM,全称 Java Virtual Machine,中文叫 Java 虚拟机。
一句话理解:
JVM 是 Java 程序和操作系统之间的一层运行环境,它负责加载 class 文件、管理内存、执行字节码、回收垃圾、优化代码运行速度。
你写的 Java 代码不是直接被 Windows、Linux、macOS 执行的,而是先被编译成 .class 字节码,然后交给 JVM 执行。
执行链路如下:
Java 源代码 .java
↓ javac 编译
字节码文件 .class
↓ JVM 加载并执行
机器指令
↓ 操作系统和 CPU 执行
程序运行结果
1.1 为什么 Java 能跨平台
Java 常说“一次编写,到处运行”,原因不是 Java 代码本身能跨平台,而是不同平台有不同版本的 JVM。
同一份 .class 字节码
↓
Windows JVM -> Windows 上运行
Linux JVM -> Linux 上运行
macOS JVM -> macOS 上运行
辅助理解:
Java 代码就像普通话写的说明书。
JVM 就像各地的翻译人员。
Windows、Linux、macOS 就像不同语言环境。
只要每个地方都有懂普通话的翻译,说明书就能被执行。
1.2 JVM 在工作中有什么用
很多人觉得 JVM 是面试八股,但它在工作中非常有用。
常见场景:
| 工作问题 | JVM 相关知识 |
|---|---|
| 服务突然 OOM | 堆内存、方法区、直接内存、内存泄漏 |
| 接口突然变慢 | GC 停顿、JIT 编译、线程阻塞 |
| 服务启动很慢 | 类加载、Spring Bean 初始化、JIT 预热 |
| CPU 飙高 | 线程栈、死循环、频繁 GC、即时编译 |
| 容器内存被打爆 | JVM 参数、堆外内存、容器感知 |
| 修改代码不生效 | 类加载器、缓存、热部署 |
| 线上偶发卡顿 | Stop The World、Full GC、锁竞争 |
2. 学 JVM 要建立的整体地图
JVM 很大,但可以分成 6 块学习:
1. 类加载机制:class 文件如何进入 JVM
2. 运行时内存:对象、变量、方法调用放在哪里
3. 字节码执行:JVM 如何执行代码
4. 垃圾回收:不用的对象如何被清理
5. 性能调优:如何定位慢、卡、OOM、CPU 高
6. 工作实践:怎么把 JVM 知识用到项目中
一张总图:
Java 源代码
|
v
javac 编译
|
v
.class 文件
|
v
类加载子系统
|
v
------------------------------------------------
| JVM 运行时数据区 |
| 堆 | 方法区/元空间 | 栈 | 本地方法栈 | 程序计数器 |
------------------------------------------------
|
v
执行引擎解释或编译执行
|
v
操作系统 / CPU / 内存
学习建议:
先理解对象放在哪里,再理解对象怎么创建,最后理解对象什么时候被回收。
3. JVM、JRE、JDK 的区别
很多小白容易混淆这三个概念。
| 名称 | 全称 | 作用 | 面向谁 |
|---|---|---|---|
| JVM | Java Virtual Machine | 执行 Java 字节码 | 程序运行 |
| JRE | Java Runtime Environment | JVM + 运行时类库 | 只运行 Java 程序的人 |
| JDK | Java Development Kit | JRE + 开发工具 | Java 开发者 |
关系:
JDK
├─ JRE
│ ├─ JVM
│ └─ Java 基础类库
└─ javac、jps、jstack、jmap、jstat 等开发工具
工作建议:
- 开发机器安装 JDK。
- 线上服务器如果需要排查问题,也建议安装 JDK,而不是只安装 JRE。
- 因为
jstack、jmap、jstat、jcmd等排查工具都在 JDK 里。
4. JVM 运行时内存区域
这是 JVM 最核心的基础。
JVM 运行时内存可以理解为 Java 程序运行时使用的一套“内存房间”。
JVM 运行时数据区
├─ 堆 Heap
├─ 方法区 Method Area / 元空间 Metaspace
├─ Java 虚拟机栈 JVM Stack
├─ 本地方法栈 Native Method Stack
└─ 程序计数器 Program Counter Register
4.1 堆 Heap
堆是 JVM 中最大的一块内存区域。
主要作用:
存放对象实例和数组。
例如:
User user = new User();
int[] nums = new int[10];
这里的 new User() 对象和 new int[10] 数组主要放在堆里。
辅助理解:
堆就像一个大型仓库。
Java 程序运行过程中创建的大部分对象,都被放进这个仓库。
垃圾回收器就像仓库管理员,定期清理没人使用的物品。
特点:
- 线程共享。
- JVM 启动时创建。
- 是垃圾回收最主要管理区域。
- 容易出现
java.lang.OutOfMemoryError: Java heap space。
常见参数:
-Xms512m # 堆初始大小
-Xmx512m # 堆最大大小
工作建议:
线上服务通常建议 -Xms 和 -Xmx 设置成一样,避免运行过程中堆频繁扩容或收缩带来抖动。
示例:
java -Xms2g -Xmx2g -jar app.jar
4.2 方法区 Method Area 和元空间 Metaspace
方法区用于存放类相关信息,例如:
- 类名。
- 方法信息。
- 字段信息。
- 常量。
- 静态变量相关信息。
- JIT 编译后的代码缓存相关内容。
JDK 8 之前,HotSpot JVM 使用永久代 PermGen 实现方法区。
JDK 8 及以后,永久代被移除,改为元空间 Metaspace。
差异:
| 版本 | 实现方式 | 内存位置 |
|---|---|---|
| JDK 7 及以前 | 永久代 PermGen | JVM 堆内存的一部分或紧邻堆实现 |
| JDK 8 及以后 | 元空间 Metaspace | 本地内存 Native Memory |
辅助理解:
堆里放的是一个个对象。
方法区或元空间里放的是对象所属类的说明书。
User user = new User();
User 对象本身主要在堆里。
User 这个类的结构信息在方法区或元空间里。
常见参数:
JDK 8 及以后:
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
常见错误:
java.lang.OutOfMemoryError: Metaspace
常见原因:
- 动态生成了大量类,例如 CGLIB、Javassist、ByteBuddy。
- 应用频繁热部署,旧类加载器无法释放。
- 元空间上限设置太小。
4.3 Java 虚拟机栈 JVM Stack
每个线程都有自己的 Java 虚拟机栈。
主要作用:
保存方法调用过程中的局部变量、操作数栈、返回地址等信息。
每调用一个方法,JVM 就会创建一个栈帧 Stack Frame。
方法调用结束后,对应栈帧出栈。
示例代码:
public void a() {
int x = 1;
b();
}
public void b() {
int y = 2;
c();
}
public void c() {
int z = 3;
}
执行过程:
调用 a():a 栈帧入栈
调用 b():b 栈帧入栈
调用 c():c 栈帧入栈
c() 执行完:c 栈帧出栈
b() 执行完:b 栈帧出栈
a() 执行完:a 栈帧出栈
辅助理解:
栈就像一摞盘子。
最后放上去的盘子,最先拿下来。
方法调用也是这样,最后调用的方法最先返回。
常见错误:
java.lang.StackOverflowError
常见原因:
- 无限递归。
- 方法调用层级过深。
- 单个线程栈设置太小。
示例:
public class StackOverflowDemo {
public static void main(String[] args) {
test();
}
private static void test() {
test();
}
}
常见参数:
-Xss1m
说明:
-Xss用于设置每个线程的栈大小。- 栈越大,单个线程能支持的方法调用深度越深。
- 栈越大,同样内存下能创建的线程数越少。
工作启发:
如果服务创建大量线程,不能只看堆内存。
每个线程都会占用独立栈内存,线程过多也可能导致内存被打满。
4.4 本地方法栈 Native Method Stack
本地方法栈服务于 Native 方法。
Native 方法通常是用 C、C++ 等语言实现的方法。
例如:
public native int hashCode();
工作中一般不需要深入操作本地方法栈,但要知道:
- Java 底层很多功能会调用 Native 方法。
- NIO、压缩、加密、网络、文件操作可能涉及本地代码。
- 本地内存问题不一定体现在 Java 堆里。
4.5 程序计数器 Program Counter Register
程序计数器是一块很小的线程私有内存。
主要作用:
记录当前线程执行到哪一条字节码指令。
辅助理解:
程序计数器像书签。
线程被 CPU 暂停后,下次恢复执行时,需要知道上次读到了哪一页。
特点:
- 线程私有。
- 占用很小。
- JVM 规范中唯一一个不会发生 OutOfMemoryError 的区域。
5. 对象是怎么创建出来的
当你写下:
User user = new User();
JVM 大致会做这些事:
1. 检查 User 类是否已经被加载。
2. 如果没有加载,先触发类加载。
3. 在堆中为 User 对象分配内存。
4. 将对象内存初始化为零值。
5. 设置对象头信息。
6. 执行构造方法。
7. 将对象引用赋值给变量 user。
5.1 对象内存布局
HotSpot JVM 中,一个普通对象大致由三部分组成:
对象头 Header
实例数据 Instance Data
对齐填充 Padding
| 部分 | 内容 | 说明 |
|---|---|---|
| 对象头 | Mark Word、类型指针等 | 存储锁状态、GC 年龄、哈希码、类元信息指针 |
| 实例数据 | 成员变量值 | 对象真正保存的数据 |
| 对齐填充 | 补齐字节 | JVM 对象大小通常按 8 字节对齐 |
辅助理解:
对象头像快递面单,记录这个包裹的基础信息。
实例数据像快递箱里的真实商品。
对齐填充像箱子里为了填满空间放的泡沫。
5.2 对象引用是什么
代码:
User user = new User();
这里的 user 不是对象本身,而是对象引用。
可以理解为:
对象在堆仓库里。
user 是一张写着仓库地址的小纸条。
所以当你写:
User user1 = new User();
User user2 = user1;
含义是:
user1 和 user2 两张纸条指向同一个对象。
这也是为什么修改 user2 的属性,user1 看到的也会变化。
6. 类加载机制
Java 类不是一开始全部加载进 JVM 的,而是在需要时才加载。
类加载过程:
加载 Loading
↓
验证 Verification
↓
准备 Preparation
↓
解析 Resolution
↓
初始化 Initialization
↓
使用 Using
↓
卸载 Unloading
其中验证、准备、解析统称为连接 Linking。
6.1 加载 Loading
加载阶段做三件事:
1. 通过类的全限定名获取 class 字节码。
2. 将字节码表示的静态存储结构转换为方法区运行时数据结构。
3. 在堆中生成一个 Class 对象,作为访问类元信息的入口。
来源可以是:
- 本地
.class文件。 - Jar 包。
- 网络下载。
- 动态代理运行时生成。
6.2 验证 Verification
验证 class 文件是否合法、安全。
例如:
- 文件格式是否正确。
- 字节码是否符合 JVM 规范。
- 是否会危害 JVM 安全。
辅助理解:
验证阶段就像安检。
不是所有 class 文件都能直接进入 JVM,必须先确认它安全合法。
6.3 准备 Preparation
准备阶段为类变量分配内存并设置初始零值。
注意,这里是类变量,也就是 static 变量。
示例:
public class Demo {
public static int count = 10;
}
准备阶段:
count = 0
初始化阶段:
count = 10
6.4 解析 Resolution
解析阶段将符号引用转换为直接引用。
小白理解即可:
符号引用像“我要找 com.example.User 这个人”。
直接引用像“这个人就在内存地址 xxx”。
6.5 初始化 Initialization
初始化阶段才真正执行类构造器 <clinit>()。
它会执行:
- 静态变量赋值。
- 静态代码块。
示例:
public class Demo {
public static int count = 10;
static {
count = 20;
}
}
最终 count 是 20。
6.6 类什么时候会初始化
常见触发时机:
- new 一个对象。
- 访问类的静态变量。
- 调用类的静态方法。
- 使用反射访问类。
- 初始化子类时,如果父类还没初始化,会先初始化父类。
- JVM 启动时执行 main 方法所在类。
示例:
public class User {
static {
System.out.println("User init");
}
}
public class Main {
public static void main(String[] args) {
new User();
}
}
执行 new User() 时,会触发 User 类初始化。
7. 类加载器和双亲委派模型
类加载器 ClassLoader 负责把 class 文件加载到 JVM 中。
常见类加载器:
| 类加载器 | 作用 |
|---|---|
| Bootstrap ClassLoader | 加载 Java 核心类库,例如 java.lang.String |
| Extension ClassLoader | JDK 8 中加载扩展类库 |
| Platform ClassLoader | JDK 9+ 中替代部分扩展类加载职责 |
| Application ClassLoader | 加载应用 classpath 下的业务类和依赖 Jar |
| Custom ClassLoader | 自定义类加载器,例如 Tomcat、插件框架 |
7.1 双亲委派是什么
双亲委派流程:
应用类加载器收到加载请求
↓
先交给父加载器
↓
父加载器继续往上交
↓
最顶层父加载器尝试加载
↓
父加载器加载不了,再由子加载器加载
辅助理解:
员工遇到问题先上报主管。
主管解决不了再交回员工自己处理。
7.2 为什么要双亲委派
主要目的:
保护 Java 核心类库,避免核心类被随意替换。
例如你自己写一个类:
package java.lang;
public class String {
}
如果没有双亲委派,你可能替换掉 JDK 自带的 java.lang.String,这会造成严重安全问题。
有双亲委派后,加载 java.lang.String 会优先交给 Bootstrap ClassLoader,由它加载 JDK 自带版本。
7.3 工作中的类加载问题
常见问题:
| 问题 | 可能原因 |
|---|---|
| ClassNotFoundException | classpath 中没有对应类 |
| NoClassDefFoundError | 编译时有,运行时没有,或类初始化失败 |
| NoSuchMethodError | 依赖版本冲突,运行时加载了旧版本类 |
| ClassCastException | 同名类被不同类加载器加载 |
工作启发:
遇到奇怪的类错误,不要只看代码。要看运行时到底加载了哪个 Jar、哪个版本、哪个 ClassLoader。
排查命令示例:
java -verbose:class -jar app.jar
或在 Spring Boot 中查看依赖树:
mvn dependency:tree
8. 字节码和执行引擎
Java 编译后生成的是字节码,不是机器码。
8.1 什么是字节码
字节码是 JVM 能识别的一套指令。
示例 Java 代码:
public int add(int a, int b) {
return a + b;
}
可以使用下面命令查看字节码:
javac Demo.java
javap -c Demo
可能看到类似:
0: iload_1
1: iload_2
2: iadd
3: ireturn
简单理解:
| 字节码 | 含义 |
|---|---|
| iload_1 | 加载第 1 个 int 参数 |
| iload_2 | 加载第 2 个 int 参数 |
| iadd | 执行 int 加法 |
| ireturn | 返回 int 结果 |
8.2 解释执行和即时编译
JVM 执行字节码有两种主要方式:
| 方式 | 说明 | 特点 |
|---|---|---|
| 解释执行 | 一条一条解释字节码 | 启动快,长期运行性能一般 |
| JIT 编译 | 将热点代码编译成本地机器码 | 启动预热后性能高 |
辅助理解:
解释执行像同声传译,听一句翻译一句。
JIT 编译像把常用文章提前翻译好,下次直接读译文。
8.3 什么是热点代码
热点代码就是被频繁执行的代码,例如:
- 高频接口中的核心方法。
- 循环体中的代码。
- 被大量请求调用的方法。
JVM 会统计代码执行次数,当达到一定阈值后,JIT 会把热点代码编译成本地机器码,提高执行速度。
工作启发:
Java 服务刚启动时可能性能没有达到最佳,因为热点代码还没有被 JIT 充分编译。
这就是所谓的预热。
8.4 为什么线上服务刚启动会有抖动
可能原因:
- Spring 容器刚初始化完成,缓存还没热起来。
- JIT 还没有编译热点代码。
- 数据库连接池、Redis 连接池刚建立。
- 类还在陆续加载。
工作建议:
- 重要服务上线后先做健康检查。
- 流量切入不要瞬间打满。
- 可以使用灰度发布,让服务逐步承接流量。
9. 垃圾回收 GC 基础
GC,全称 Garbage Collection,垃圾回收。
它负责清理不再使用的对象,释放内存。
9.1 什么对象是垃圾
简单说:
如果一个对象再也无法被程序访问,它就是垃圾。
示例:
public void test() {
User user = new User();
user = null;
}
当 user = null 后,如果没有其他引用指向这个对象,它就可能成为垃圾。
9.2 引用计数法
引用计数法思路:
对象被引用一次,计数加 1。
引用失效一次,计数减 1。
计数为 0,就认为是垃圾。
缺点:无法解决循环引用。
示例:
class A {
B b;
}
class B {
A a;
}
如果 A 引用 B,B 又引用 A,但外部已经无法访问它们,引用计数仍然不为 0。
所以主流 JVM 不是主要依靠引用计数判断对象是否存活。
9.3 可达性分析法
HotSpot JVM 主要使用可达性分析。
核心思路:
从一组 GC Roots 出发,沿着引用链往下找。
能找到的对象是存活对象。
找不到的对象就是可回收对象。
辅助理解:
GC Roots 像树根。
对象引用像树枝。
能从树根连到的叶子还活着。
连不到树根的枝叶就是枯枝,可以剪掉。
常见 GC Roots:
- 虚拟机栈中引用的对象。
- 方法区中静态变量引用的对象。
- 方法区中常量引用的对象。
- Native 方法引用的对象。
- 活跃线程对象。
10. Java 对象引用类型
Java 中引用不只有普通引用,还可以分成强、软、弱、虚四种。
| 引用类型 | 回收时机 | 常见用途 |
|---|---|---|
| 强引用 Strong Reference | 只要还被强引用,就不会被回收 | 普通对象引用 |
| 软引用 Soft Reference | 内存不足时可能回收 | 缓存 |
| 弱引用 Weak Reference | 下一次 GC 时回收 | ThreadLocalMap 的 key |
| 虚引用 Phantom Reference | 无法直接获取对象,用于回收通知 | 直接内存释放跟踪 |
10.1 强引用
最常见的引用:
User user = new User();
只要 user 还指向对象,对象就不会被回收。
10.2 软引用
软引用适合做缓存:
SoftReference<byte[]> ref = new SoftReference<>(new byte[1024 * 1024]);
当内存不足时,软引用对象可能被回收。
10.3 弱引用
弱引用只要发生 GC,就可能被回收。
WeakReference<User> ref = new WeakReference<>(new User());
工作重点:
ThreadLocalMap 的 key 使用弱引用,但 value 是强引用。
如果线程长期不结束,又不 remove,仍可能导致内存泄漏。
ThreadLocal 正确使用方式:
private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
public void handle() {
try {
USER_ID.set("10001");
// 业务处理
} finally {
USER_ID.remove();
}
}
11. 堆内存分代模型
多数垃圾回收器基于分代思想。
核心经验:
大多数对象朝生夕死,少数对象长期存活。
堆通常可以分为:
堆 Heap
├─ 新生代 Young Generation
│ ├─ Eden 区
│ ├─ Survivor From 区
│ └─ Survivor To 区
└─ 老年代 Old Generation
11.1 新生代
新对象大多数先进入 Eden 区。
当 Eden 区满了,会触发 Minor GC。
存活下来的对象会进入 Survivor 区。
对象在 Survivor 区多次熬过 GC 后,会晋升到老年代。
11.2 老年代
老年代存放生命周期较长的对象。
例如:
- Spring 单例 Bean。
- 本地缓存。
- 线程池对象。
- 长期存在的连接池对象。
- 大对象。
老年代空间不足时,可能触发 Major GC 或 Full GC。
11.3 Eden 和 Survivor 的关系
默认情况下,新生代比例常见为:
Eden : From Survivor : To Survivor = 8 : 1 : 1
辅助理解:
Eden 像新员工试用区。
Survivor 像观察区。
老年代像正式长期员工区。
对象如果每次 GC 都还活着,说明它可能是长期对象,于是逐渐晋升到老年代。
12. 常见 GC 类型
12.1 Minor GC
Minor GC 主要回收新生代。
特点:
- 触发频繁。
- 速度相对较快。
- 会发生 Stop The World。
12.2 Major GC
Major GC 通常指老年代 GC。
不同资料中定义略有差异,有些地方会把 Major GC 和 Full GC 混用。
12.3 Full GC
Full GC 通常会回收整个堆以及方法区或元空间相关资源。
特点:
- 停顿时间较长。
- 对线上服务影响较大。
- 频繁 Full GC 是严重性能问题信号。
工作启发:
Minor GC 频繁不一定严重,要看停顿时间和回收效果。
Full GC 频繁通常需要重点排查。
13. Stop The World
Stop The World,简称 STW。
意思是:
GC 执行某些阶段时,会暂停所有用户线程,等 GC 完成后再恢复。
辅助理解:
餐厅要大扫除时,先暂停接客。
服务员和厨师都停下来,清洁人员打扫完,再继续营业。
为什么需要 STW:
如果程序一边修改对象引用关系,GC 一边判断对象是否存活,结果可能不准确。
工作影响:
- 接口响应时间突然变长。
- 服务短时间无响应。
- 心跳超时。
- RPC 调用超时。
工作建议:
排查接口偶发慢请求时,一定要结合 GC 日志看是否有 STW 停顿。
14. 垃圾回收算法
14.1 标记-清除 Mark-Sweep
步骤:
1. 标记存活对象。
2. 清除未标记对象。
优点:
- 思路简单。
缺点:
- 会产生内存碎片。
- 清理后内存不连续。
辅助理解:
像在教室里把没人用的座位清出来,但空位分散在各处。
14.2 标记-复制 Mark-Copy
步骤:
1. 将内存分成两块。
2. 只使用其中一块。
3. GC 时把存活对象复制到另一块。
4. 清空原来的整块内存。
优点:
- 没有碎片。
- 回收效率高。
缺点:
- 可用内存变少。
- 存活对象多时复制成本高。
适合:
- 新生代,因为大多数新对象很快死亡,存活对象少。
14.3 标记-整理 Mark-Compact
步骤:
1. 标记存活对象。
2. 将存活对象向一端移动。
3. 清理边界外的内存。
优点:
- 没有内存碎片。
- 适合老年代。
缺点:
- 移动对象成本较高。
辅助理解:
像整理书架,把还要的书都靠左排整齐,右边空出来整块空间。
15. 常见垃圾回收器
不同 JDK 版本默认 GC 不一样。
| GC | 特点 | 常见版本 |
|---|---|---|
| Serial GC | 单线程,简单 | 客户端、小内存场景 |
| Parallel GC | 吞吐量优先 | JDK 8 默认常见选择 |
| CMS GC | 低停顿,已逐步淘汰 | JDK 8 常见,JDK 14 移除 |
| G1 GC | 平衡吞吐和停顿,可预测停顿 | JDK 9+ 默认 |
| ZGC | 超低停顿 | JDK 11+ 可用,JDK 15 生产可用 |
| Shenandoah | 超低停顿 | 部分 OpenJDK 版本 |
15.1 Parallel GC
特点:
- 多线程 GC。
- 吞吐量优先。
- 适合后台任务、批处理。
参数:
-XX:+UseParallelGC
15.2 CMS GC
CMS 全称 Concurrent Mark Sweep。
特点:
- 目标是减少老年代回收停顿。
- 使用标记-清除算法。
- 会产生内存碎片。
- JDK 9 开始标记废弃,JDK 14 移除。
参数:
-XX:+UseConcMarkSweepGC
15.3 G1 GC
G1 全称 Garbage First。
它不再简单把堆切成连续的新生代和老年代,而是拆成很多 Region。
特点:
- 适合大堆内存。
- 可以设置期望最大停顿时间。
- JDK 9 以后默认 GC。
- 工作中非常常见。
参数:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
辅助理解:
传统 GC 像按整片城区清理垃圾。
G1 像把城市分成很多小区,优先清理垃圾最多、收益最高的小区。
15.4 ZGC
ZGC 目标是超低停顿。
特点:
- 停顿时间通常非常短。
- 适合大内存、低延迟服务。
- 对 JDK 版本要求较高。
参数:
-XX:+UseZGC
工作建议:
不要盲目追新 GC。
如果系统稳定、延迟可接受,优先保守。
如果明确有大堆低延迟需求,再评估 G1、ZGC 等方案。
16. JVM 常用参数
16.1 堆内存参数
-Xms2g
-Xmx2g
说明:
-Xms:初始堆大小。-Xmx:最大堆大小。
建议:
线上服务通常设置为一样,例如 -Xms2g -Xmx2g。
16.2 栈内存参数
-Xss1m
说明:
- 每个线程的栈大小。
注意:
线程越多,栈总占用越大。
16.3 元空间参数
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
说明:
MetaspaceSize:触发元空间 GC 的初始阈值。MaxMetaspaceSize:元空间最大大小。
16.4 GC 日志参数
JDK 8:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/logs/app-gc.log
JDK 9+:
-Xlog:gc*:file=/data/logs/app-gc.log:time,uptime,level,tags:filecount=10,filesize=100M
工作建议:
生产环境建议一定开启 GC 日志。
没有 GC 日志,排查卡顿和内存问题会非常被动。
16.5 OOM 自动 Dump 参数
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump
说明:
- 发生 OOM 时自动导出堆快照。
- 后续可以用 MAT、JProfiler、VisualVM 分析。
工作建议:
线上服务建议配置 OOM dump,但 dump 目录要有足够磁盘空间。
16.6 推荐基础启动参数模板
JDK 8 示例:
java \
-Xms2g \
-Xmx2g \
-Xss1m \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/dump \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-Xloggc:/data/logs/app-gc.log \
-jar app.jar
JDK 11+ 示例:
java \
-Xms2g \
-Xmx2g \
-Xss1m \
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/dump \
-Xlog:gc*:file=/data/logs/app-gc.log:time,uptime,level,tags:filecount=10,filesize=100M \
-jar app.jar
17. JVM 常用排查工具
17.1 jps:查看 Java 进程
jps -l
示例输出:
12345 com.example.Application
12345 就是 Java 进程 PID。
17.2 jinfo:查看 JVM 参数
jinfo -flags 12345
作用:
- 查看进程启动参数。
- 确认线上实际参数是否和配置一致。
17.3 jstat:查看 GC 情况
jstat -gcutil 12345 1000 10
含义:
- 每 1 秒打印一次。
- 总共打印 10 次。
重点关注:
| 字段 | 含义 |
|---|---|
| S0、S1 | Survivor 使用率 |
| E | Eden 使用率 |
| O | Old 老年代使用率 |
| M | Metaspace 使用率 |
| YGC | Young GC 次数 |
| YGCT | Young GC 总耗时 |
| FGC | Full GC 次数 |
| FGCT | Full GC 总耗时 |
| GCT | GC 总耗时 |
工作判断:
如果 FGC 持续增加,且 Old 区使用率降不下来,要重点怀疑内存泄漏或堆太小。
17.4 jstack:查看线程栈
jstack 12345 > /tmp/jstack.txt
常用于排查:
- CPU 高。
- 死锁。
- 线程阻塞。
- 请求卡死。
17.5 jmap:导出堆快照
jmap -dump:format=b,file=/tmp/heap.hprof 12345
注意:
导出大堆快照可能导致服务短暂停顿,生产环境要谨慎操作。
17.6 jcmd:综合诊断工具
查看可用命令:
jcmd 12345 help
查看 JVM 参数:
jcmd 12345 VM.flags
触发堆 dump:
jcmd 12345 GC.heap_dump /tmp/heap.hprof
查看线程:
jcmd 12345 Thread.print
工作建议:
JDK 8 以后,jcmd 很强大,很多场景可以优先使用 jcmd。
18. 线上 CPU 飙高怎么排查
这是 JVM 工作实战中非常常见的问题。
18.1 排查步骤
第 1 步:找到 CPU 高的 Java 进程。
top
假设 Java 进程 PID 是 12345。
第 2 步:查看进程内哪个线程 CPU 高。
top -Hp 12345
假设线程 ID 是 12370。
第 3 步:将线程 ID 转成 16 进制。
printf '%x\n' 12370
假设输出:
3052
第 4 步:导出线程栈。
jstack 12345 > /tmp/jstack.txt
第 5 步:在 jstack 文件中搜索 3052。
grep -n "3052" /tmp/jstack.txt
找到对应线程栈后,看它正在执行什么代码。
18.2 常见原因
| 原因 | 表现 |
|---|---|
| 死循环 | 某个线程一直 RUNNABLE |
| 正则回溯 | CPU 高,栈里有 Pattern 相关方法 |
| JSON 大对象序列化 | 栈里有 Jackson、Fastjson、Gson |
| 频繁 GC | GC 线程占 CPU,jstat 中 GC 次数猛增 |
| 加密压缩 | 栈里有 Cipher、Zip、GZIP 等 |
工作启发:
CPU 高不要先猜代码问题。
先定位是哪个线程高,再看线程栈,最后对应到代码。
19. 线上 OOM 怎么排查
OOM,全称 OutOfMemoryError。
不同 OOM 对应不同内存区域。
19.1 常见 OOM 类型
| 错误 | 含义 | 常见原因 |
|---|---|---|
| Java heap space | 堆内存不足 | 对象太多、内存泄漏、堆太小 |
| GC overhead limit exceeded | GC 花太多时间但回收很少 | 堆接近耗尽 |
| Metaspace | 元空间不足 | 类太多、动态代理过多、热部署泄漏 |
| Direct buffer memory | 直接内存不足 | NIO、Netty、堆外内存未释放 |
| unable to create new native thread | 无法创建线程 | 线程太多、系统资源不足 |
19.2 排查思路
1. 看 OOM 报错类型。
2. 找 JVM 启动参数。
3. 找 GC 日志。
4. 找 heap dump。
5. 分析哪些对象占用最多。
6. 判断是正常业务量增长,还是对象无法释放。
19.3 堆 OOM 排查
建议启动参数提前配置:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump
发生 OOM 后,用 MAT 分析 .hprof 文件。
MAT 下载地址:
https://www.eclipse.org/mat/downloads.php
重点看:
- Leak Suspects Report。
- Dominator Tree。
- 大对象列表。
- 哪些集合对象非常大,例如
ArrayList、HashMap。
工作启发:
堆 OOM 不一定是堆太小,很多时候是对象被错误地长期引用,导致 GC 无法回收。
19.4 线程 OOM 排查
错误:
java.lang.OutOfMemoryError: unable to create new native thread
常见原因:
- 无限制创建线程。
- 线程池参数不合理。
- 每个请求创建新线程。
- 系统 ulimit 限制太低。
排查命令:
ps -eLf | grep java | wc -l
ulimit -u
工作建议:
业务代码不要随手 new Thread。
统一使用线程池,并设置合理的核心线程数、最大线程数和队列长度。
20. 内存泄漏怎么理解
内存泄漏不是对象丢了,而是对象不该继续存在,却仍然被引用,导致 GC 无法回收。
辅助理解:
你已经不需要某个快递箱了,但它还被登记在仓库系统里。
仓库管理员以为它还有用,就不会清理。
20.1 常见内存泄漏场景
| 场景 | 问题 |
|---|---|
| 静态集合缓存 | 数据只增不删 |
| ThreadLocal 未 remove | 线程池线程长期存活,value 无法释放 |
| 监听器未注销 | 对象被事件源长期引用 |
| 连接未关闭 | 数据库、Redis、HTTP 连接泄漏 |
| 大对象放入 Session | 用户多时内存暴涨 |
| 本地缓存无上限 | HashMap、ConcurrentHashMap 不断增长 |
20.2 静态集合泄漏示例
错误示例:
public class CacheDemo {
private static final Map<String, Object> CACHE = new HashMap<>();
public void put(String key, Object value) {
CACHE.put(key, value);
}
}
如果这个 Map 只增不删,最终可能 OOM。
改进建议:
- 设置最大容量。
- 设置过期时间。
- 使用 Caffeine、Guava Cache、Redis 等成熟方案。
Caffeine 示例:
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
20.3 ThreadLocal 泄漏示例
错误示例:
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
public void handle(User user) {
CURRENT_USER.set(user);
doSomething();
}
正确示例:
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
public void handle(User user) {
try {
CURRENT_USER.set(user);
doSomething();
} finally {
CURRENT_USER.remove();
}
}
21. GC 日志怎么看
GC 日志是排查 JVM 问题的关键证据。
21.1 JDK 8 GC 日志示例
2026-07-14T10:00:00.123+0800: 12.345: [GC pause (G1 Evacuation Pause) (young), 0.0256789 secs]
[Eden: 512.0M(512.0M)->0.0B(480.0M) Survivors: 32.0M->64.0M Heap: 1.2G(2.0G)->800.0M(2.0G)]
重点看:
| 内容 | 说明 |
|---|---|
| GC 类型 | Young GC、Mixed GC、Full GC |
| 停顿时间 | 例如 0.025 秒 |
| GC 前后堆变化 | 是否回收了大量内存 |
| Old 区变化 | 老年代是否持续上涨 |
| Full GC 次数 | 是否频繁 |
21.2 如何判断 GC 是否异常
重点不是只看次数,而是看:
1. 每次 GC 停顿多久。
2. GC 后内存有没有明显下降。
3. Full GC 是否频繁。
4. 老年代是否持续上涨不回落。
5. GC 是否和接口慢请求时间吻合。
21.3 推荐 GC 日志分析工具
| 工具 | 链接 | 说明 |
|---|---|---|
| GCeasy | https://gceasy.io/ | 在线 GC 日志分析 |
| GCViewer | https://github.com/chewiebug/GCViewer | 本地 GC 日志查看工具 |
| IBM GCMV | https://www.ibm.com/support/pages/ibm-monitoring-and-diagnostic-tools-garbage-collection-and-memory-visualizer-gcmv | GC 和内存可视化工具 |
注意:
如果 GC 日志包含敏感路径、机器名或业务信息,上传在线工具前要先脱敏。
22. JVM 在 Spring Boot 工作中的启发
22.1 不要无限制使用本地缓存
很多服务为了提高性能,会把数据放进本地 Map。
问题是:
本地缓存就是 JVM 堆内存的一部分。
缓存越大,堆压力越大,GC 压力越大。
建议:
- 使用有上限的缓存。
- 设置过期时间。
- 大数据量缓存放 Redis。
- 不要把用户级大对象长期放本地内存。
22.2 接口返回大对象会增加 GC 压力
例如一次查询 10 万条数据并返回:
List<Order> orders = orderMapper.selectAll();
return orders;
问题:
- 大量 Order 对象进入堆。
- JSON 序列化还会产生额外临时对象。
- 网络传输慢。
- 客户端也可能扛不住。
建议:
- 分页查询。
- 限制最大导出量。
- 大文件异步导出。
- 流式处理。
22.3 线程池不是越大越好
线程池过大可能导致:
- 上下文切换变多。
- 栈内存占用变大。
- 下游数据库、Redis 被打爆。
- 请求排队和超时更严重。
建议:
线程池大小要结合 CPU 核数、任务类型、下游能力、响应时间目标一起设置。
22.4 日志也会影响 JVM
大量日志可能导致:
- 字符串对象大量创建。
- 磁盘 IO 压力变大。
- 异步日志队列堆积。
- 日志参数拼接带来额外开销。
错误示例:
log.debug("user info: " + JSON.toJSONString(user));
更好写法:
if (log.isDebugEnabled()) {
log.debug("user info: {}", JSON.toJSONString(user));
}
22.5 大事务会增加内存和 GC 压力
大事务中可能持有大量对象、连接、锁。
建议:
- 控制事务范围。
- 批量处理分批提交。
- 不要在事务中做远程调用。
- 不要在事务中做长时间文件处理。
23. JVM 和容器 Docker / Kubernetes
现在很多 Java 服务运行在 Docker 或 Kubernetes 中。
23.1 容器中为什么容易 OOMKilled
容器有内存限制,例如 2G。
但 JVM 使用的内存不只有堆:
JVM 总内存 ≈ 堆 + 元空间 + 线程栈 + 直接内存 + Code Cache + JVM 自身开销 + Native 内存
如果你设置:
-Xmx2g
而容器限制也是 2G,那么很容易被容器杀掉。
23.2 容器内存参数建议
假设容器限制 2G,不建议堆直接设置 2G。
可以考虑:
-Xms1g -Xmx1g
或使用百分比参数:
-XX:InitialRAMPercentage=50.0
-XX:MaxRAMPercentage=70.0
工作建议:
容器内存一定要给堆外内存留余量。
不要让 -Xmx 等于容器 memory limit。
24. 一套实用 JVM 排查流程
24.1 接口变慢
排查顺序:
1. 看应用日志,确认慢请求时间点。
2. 看 GC 日志,确认是否有长时间 STW。
3. 看 CPU 是否飙高。
4. 看线程栈是否有锁等待、IO 等待、数据库等待。
5. 看下游依赖是否变慢。
6. 看数据库慢 SQL。
24.2 内存持续上涨
排查顺序:
1. jstat 看 Old 区是否持续上涨。
2. 看 Full GC 后 Old 区是否下降。
3. 如果不下降,怀疑内存泄漏。
4. 导出 heap dump。
5. 用 MAT 看大对象和引用链。
6. 定位代码中的长期引用。
24.3 服务频繁重启
排查顺序:
1. 看应用日志是否有 OOM。
2. 看容器事件是否 OOMKilled。
3. 看机器 dmesg 是否被系统杀掉。
4. 看 JVM 参数和容器内存限制。
5. 看健康检查是否过短导致误杀。
Kubernetes 查看命令:
kubectl describe pod pod-name
kubectl logs pod-name --previous
25. 面试和工作都要会的 JVM 问题
25.1 JVM 内存区域有哪些
回答框架:
JVM 运行时内存主要包括堆、方法区或元空间、虚拟机栈、本地方法栈、程序计数器。
堆和方法区是线程共享的,栈、本地方法栈、程序计数器是线程私有的。
堆主要放对象实例,是 GC 重点管理区域。
栈保存方法调用的栈帧。
元空间保存类元信息。
25.2 对象什么时候会被回收
回答框架:
HotSpot 主要通过可达性分析判断对象是否存活。
从 GC Roots 出发,沿引用链能到达的对象是存活对象。
无法到达的对象可能被回收。
GC Roots 包括栈中引用、静态变量引用、常量引用、Native 引用等。
25.3 什么是 Full GC,为什么要关注
回答框架:
Full GC 通常会对整个堆以及方法区或元空间相关资源进行回收。
它通常停顿时间较长,会造成应用线程暂停,也就是 STW。
如果线上频繁 Full GC,可能导致接口超时、吞吐下降、服务不可用。
需要结合 GC 日志、jstat、heap dump 分析原因。
25.4 什么是双亲委派
回答框架:
类加载器加载类时,会先把请求交给父加载器,父加载器无法加载时才由子加载器加载。
这样可以保证 Java 核心类库优先由 Bootstrap ClassLoader 加载,避免核心类被应用代码篡改。
25.5 怎么排查 CPU 飙高
回答框架:
先用 top 找到 CPU 高的 Java 进程,再用 top -Hp 找到进程内 CPU 高的线程。
把线程 ID 转为 16 进制,然后用 jstack 导出线程栈,在栈中搜索对应 nid。
最后根据线程正在执行的方法定位业务代码或 GC、锁竞争等问题。
26. JVM 学习路线
26.1 第一阶段:基础理解
目标:能讲清楚 JVM 是什么、内存怎么分、对象放哪里。
学习内容:
- JVM、JRE、JDK 区别。
- 堆、栈、元空间、程序计数器。
- 对象创建过程。
- 类加载流程。
26.2 第二阶段:GC 理解
目标:能看懂 GC 和常见参数。
学习内容:
- 可达性分析。
- 新生代、老年代。
- Minor GC、Full GC。
- G1、CMS、ZGC。
- GC 日志。
26.3 第三阶段:工具实战
目标:能排查线上问题。
学习内容:
- jps。
- jstat。
- jstack。
- jmap。
- jcmd。
- MAT。
- GC 日志分析工具。
26.4 第四阶段:结合项目优化
目标:能把 JVM 思维用于日常开发。
学习内容:
- 合理设置缓存。
- 控制对象创建。
- 控制线程池。
- 优化大查询和大响应。
- 容器内 JVM 参数。
- 建立线上排查流程。
27. 推荐学习资料
27.1 官方资料
| 资料 | 链接 |
|---|---|
| Java 官方文档 | https://docs.oracle.com/en/java/ |
| HotSpot VM 参数 | https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html |
| OpenJDK | https://openjdk.org/ |
| G1 GC 官方调优指南 | https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html |
27.2 推荐书籍
| 书籍 | 说明 |
|---|---|
| 《深入理解 Java 虚拟机》 | JVM 中文经典书,适合系统学习 |
| 《Java 性能权威指南》 | 偏性能优化和实践 |
| 《Java 并发编程实战》 | 理解线程、并发、锁,对 JVM 排查很有帮助 |
27.3 推荐工具
| 工具 | 用途 |
|---|---|
| VisualVM | 本地 JVM 监控和分析 |
| JConsole | JDK 自带监控工具 |
| MAT | 分析 heap dump |
| Arthas | 在线诊断 Java 应用 |
| GCeasy | 分析 GC 日志 |
Arthas 官方地址:
https://arthas.aliyun.com/
28. 小白实践任务
只看文档容易忘,建议做下面练习。
28.1 查看字节码
写一个 Demo.java:
public class Demo {
public int add(int a, int b) {
return a + b;
}
}
执行:
javac Demo.java
javap -c Demo
目标:理解 Java 会先编译成字节码。
28.2 制造 StackOverflowError
public class StackDemo {
public static void main(String[] args) {
call();
}
private static void call() {
call();
}
}
目标:理解方法调用和栈。
28.3 制造堆 OOM
import java.util.ArrayList;
import java.util.List;
public class HeapOomDemo {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]);
}
}
}
启动:
java -Xms64m -Xmx64m HeapOomDemo
目标:理解堆内存不足。
28.4 使用 jstat 观察 GC
运行 Java 程序后执行:
jps -l
jstat -gcutil 进程PID 1000
目标:观察 Eden、Old、YGC、FGC 的变化。
28.5 使用 jstack 查看线程
jstack 进程PID > /tmp/jstack.txt
目标:理解线程栈可以看到程序卡在哪里。
29. 一句话总结 JVM
如果只记住一段话,记住这个:
JVM 负责把 class 字节码加载到内存中,并通过执行引擎运行代码。
运行过程中,对象主要分配在堆里,方法调用在线程栈里,类信息在元空间里。
垃圾回收器会根据可达性分析清理不用的对象,但 GC 可能带来 Stop The World 停顿。
工作中学习 JVM,不是为了背概念,而是为了能排查 OOM、CPU 高、接口卡顿、类加载冲突和容器内存问题。
30. 工作中的 JVM 检查清单
新项目上线前建议检查:
- 是否设置了合理的
-Xms和-Xmx。 - 是否开启了 GC 日志。
- 是否配置了 OOM 自动 heap dump。
- dump 目录是否有足够磁盘空间。
- 容器内
-Xmx是否小于 memory limit,并给堆外留余量。 - 本地缓存是否有最大容量和过期时间。
- ThreadLocal 是否在 finally 中 remove。
- 线程池是否有边界。
- 大查询是否分页。
- 大文件是否流式处理。
- 是否有监控 JVM 堆、非堆、线程数、GC 次数、GC 停顿。
线上问题发生时建议保留:
- 应用日志。
- GC 日志。
- JVM 启动参数。
- jstack 文件。
- jstat 输出。
- heap dump。
- 容器事件或系统日志。
这些证据越完整,问题越容易定位。
更多推荐




所有评论(0)