一、JVM 运行时内存结构
JVM 运行时数据区主要包括:
- Heap(堆)
- Method Area(方法区)
- Java Stack(虚拟机栈)
- Native Method Stack(本地方法栈)
- Program Counter Register(程序计数器)
1. Heap(堆)
作用:
- 存放对象
- 存放数组
例如:
User user = new User();
int[] arr = new int[10];
这些对象和数组都在 堆 中。
堆的结构:
Heap
├─ Young Generation(新生代)
│ ├─ Eden
│ ├─ Survivor0
│ └─ Survivor1
└─ Old Generation(老年代)
对象生命周期:
new 对象
↓
Eden
↓
Minor GC
↓
Survivor
↓
年龄增长
↓
Old
2. Method Area(方法区)
作用:
存储:
- 类信息
- 运行时常量池
- 静态变量
- JIT 编译后的代码
JDK 版本区别:
- JDK 7:PermGen(永久代)
- JDK 8:Metaspace(元空间)
JDK8 以后特点:
- 元空间使用的是 本地内存
- 不再使用 PermGen
3. Java Stack(虚拟机栈)
特点:
- 线程私有
- 每个线程都有一个独立的栈
栈中存什么:
- 局部变量
- 方法调用信息
- 操作数栈
- 返回地址等
方法调用对应栈帧:
main()
test()
add()
栈中的压栈顺序:
add
test
main
每调用一个方法,就会创建一个 栈帧(Stack Frame) 。
4. Program Counter Register(程序计数器)
作用:
- 记录当前线程执行到哪一条字节码指令
可以理解为:
- 当前线程的“执行位置指针”
特点:
- 线程私有
- 是一块很小的内存区域
5. Native Method Stack(本地方法栈)
作用:
- 为 JVM 调用本地方法(Native 方法)服务
例如:
native void test();
二、类加载机制
1. 类加载器的作用
类加载器(ClassLoader)负责把 .class 文件加载到 JVM 中。
2. 类加载过程
类从加载到可用一般经历:
- 加载
- 验证
- 准备
- 解析
- 初始化
3. 双亲委派模型
类加载器层次:
Bootstrap ClassLoader
↑
Extension ClassLoader
↑
Application ClassLoader
加载规则:
- 先让父加载器加载
- 父加载器无法加载时,子加载器才自己加载
优点:
- 保证安全
- 避免重复加载
经典问题:为什么 String 不能被替换?
- 因为
String由 Bootstrap ClassLoader 加载,应用层无法随意替换核心类。
三、对象创建过程
Java 中创建对象:
User user = new User();
JVM 内部大致过程:
- 类加载检查
- 分配内存
- 初始化零值
- 设置对象头
- 执行构造方法
对象内存结构
对象通常由三部分组成:
- 对象头
- 实例数据
- 对齐填充
四、垃圾回收(GC)
1. GC 的作用
GC 用来回收 不再使用的对象,释放内存。
2. 判断对象是否死亡的方法
(1)引用计数法
对象每被引用一次,计数加 1;引用失效,计数减 1。
缺点:
- 无法解决循环引用问题
(2)可达性分析
JVM 实际使用的是 可达性分析算法。
从 GC Roots 出发,看对象是否可达。
常见 GC Roots:
- 栈中的引用
- 静态变量引用的对象
- 常量引用的对象
- JNI 引用的对象
如果对象到 GC Roots 不可达,就可以被回收。
五、GC 算法
常见 4 种算法:
- 标记-清除
- 复制算法
- 标记-整理
- 分代收集
1. 标记-清除
流程:
- 标记垃圾对象
- 清除垃圾对象
优点:
- 实现简单
缺点:
- 会产生内存碎片
2. 复制算法
把存活对象复制到另一块区域。
新生代常见结构:
Eden + S0 + S1
优点:
- 没有内存碎片
- 回收效率高
缺点:
- 需要额外空间
3. 标记-整理
流程:
- 标记
- 移动存活对象
- 清理边界外内存
优点:
- 没有碎片
适用场景:
- 老年代
4. 分代收集
JVM 的核心思想:
- 新生代对象存活率低,适合 复制算法
- 老年代对象存活率高,适合 标记-整理
六、GC 收集器
常见收集器:
- Serial
- ParNew
- CMS
- G1
- ZGC
1. Serial GC
特点:
- 单线程
- Stop The World
适用场景:
- 客户端、小内存场景
2. CMS
特点:
- 低停顿
- 追求响应时间
流程:
- 初始标记
- 并发标记
- 重新标记
- 并发清理
缺点:
- 会产生内存碎片
- 对 CPU 比较敏感
3. G1 GC(重点)
特点:
- 把堆分成多个 Region
- 可预测停顿时间
- 适合较大堆内存场景
优势:
- 兼顾吞吐量和停顿时间
- 现代生产环境中非常常用
4. ZGC
特点:
- 超低延迟
- 停顿时间非常短
适用场景:
- 大内存、低延迟系统
七、JVM 调优核心参数
最重要的四个参数:
-Xms-Xmx-Xmn-Xss
八、-Xms、-Xmx、-Xmn、-Xss 详细笔记
1. -Xms:初始堆内存
作用:
设置 JVM 启动时堆的初始大小。
例如:
-Xms2g
表示:
- JVM 启动时,初始堆大小为 2GB
例子:
java -Xms2g -Xmx4g -jar app.jar
表示:
- 启动时堆 = 2GB
- 最大堆 = 4GB
为什么生产环境常把 Xms 设为和 Xmx 一样?
- 避免堆动态扩容
- 减少扩容带来的停顿
- 提高系统稳定性
生产推荐:
-Xms4g -Xmx4g
2. -Xmx:最大堆内存
作用:
设置 JVM 堆能使用的最大内存。
例如:
-Xmx8g
表示:
- 堆最大可使用 8GB
堆关系:
Heap = Young + Old
所以:
Xmx = Young + Old
影响:
Xmx太小:容易频繁 GCXmx太大:Full GC 时间可能变长,系统总内存压力也更大
经验:
- 不要把机器内存全部给 JVM
- 一般预留操作系统、本地内存、线程栈、直接内存等空间
3. -Xmn:年轻代大小
作用:
设置新生代大小。
例如:
-Xmn2g
表示:
- 年轻代 = 2GB
年轻代结构:
Young
├─ Eden
├─ S0
└─ S1
通常比例为:
Eden : S0 : S1 = 8 : 1 : 1
例如:
-Xmn1g
大致可理解为:
- Eden ≈ 800MB
- S0 ≈ 100MB
- S1 ≈ 100MB
影响:
Xmn太小:Minor GC 频繁Xmn太大:老年代变小,可能更容易 Full GC
经验:
- 年轻代通常可以是堆的 1/3 左右
- 但是否手动设置
Xmn,要看使用的 GC
注意:
- 在 G1 GC 下,通常不建议强依赖
-Xmn手工固定年轻代,因为 G1 会自行调整分区和代际比例。
4. -Xss:线程栈大小
作用:
设置 每个线程 的栈大小。
例如:
-Xss1m
表示:
- 每个线程栈大小为 1MB
栈里存什么:
- 局部变量
- 方法调用信息
- 操作数栈
- 返回地址
例如:
void test() {
int a = 10;
}
变量 a 一般在栈帧里。
影响:
Xss 太小
可能出现:
StackOverflowError
常见场景:
- 递归层次过深
- 单个线程调用链太深
Xss 太大
会导致:
- 每个线程占用更多内存
- 可创建线程数减少
所以:
线程总数 ≈ 可用内存 / 每线程栈大小
九、四个参数关系总结
| 参数 | 作用 | 主要影响 |
|---|---|---|
-Xms | 初始堆大小 | 启动时堆空间 |
-Xmx | 最大堆大小 | JVM 可使用的最大堆内存 |
-Xmn | 年轻代大小 | Minor GC 频率、新老年代比例 |
-Xss | 每线程栈大小 | 线程数量、递归深度 |
十、常见生产配置示例
1. 常规服务配置
java \
-Xms8g \
-Xmx8g \
-Xss1m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-jar app.jar
2. 如果使用 Parallel / CMS 等,可能会看到
java \
-Xms8g \
-Xmx8g \
-Xmn3g \
-Xss1m \
-jar app.jar
十一、JVM 问题排查工具
常用工具:
| 工具 | 作用 |
|---|---|
jps | 查看 Java 进程 |
jstat | 查看 GC 情况 |
jmap | 查看堆内存、导出 dump |
jstack | 查看线程栈 |
MAT | 分析内存泄漏 |
Arthas | 在线诊断 |
十二、生产问题排查思路
1. Full GC 频繁
排查流程:
jstat 看 GC 频率
↓
jmap -histo 看大对象
↓
jmap -dump 导出堆
↓
MAT 分析是否内存泄漏
2. CPU 飙高
排查流程:
top / top -Hp
↓
找到高 CPU 线程
↓
jstack 看线程堆栈
↓
定位死循环 / 锁竞争 / 频繁计算
3. 内存持续上涨
排查流程:
jmap -dump
↓
MAT 分析
↓
看是否有对象被长时间引用
十三、面试高频答题点
1. 为什么 -Xms 通常要等于 -Xmx?
答:
- 避免堆扩容
- 减少扩容带来的停顿
- 提高系统稳定性
2. -Xmn 影响什么?
答:
- 影响年轻代大小
- 影响 Minor GC 的频率
- 间接影响老年代大小
3. -Xss 影响什么?
答:
- 影响单个线程栈深度
- 影响系统最多可创建线程数
4. Heap 的组成是什么?
答:
Heap = Young + Old
Young 中通常包含:
Eden + Survivor0 + Survivor1
十四、速记版
JVM 四大参数速记
-Xms:初始堆大小-Xmx:最大堆大小-Xmn:年轻代大小-Xss:每个线程栈大小
记忆口诀
Xms:启动给多少堆Xmx:最多能用多少堆Xmn:年轻代占多少Xss:每个线程栈多大
十五、补充纠正点
1. -Xmn 不是所有 GC 都推荐手动设
尤其是 G1 GC 场景下,通常更倾向让 JVM 自己动态调节年轻代,而不是强行固定 -Xmn。
2. GC 日志参数在新版本 JDK 中有变化
老写法常见:
-XX:+PrintGCDetails
JDK 9+ 更推荐:
-Xlog:gc*