JVM核心笔记

4 阅读8分钟

一、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. 类加载过程

类从加载到可用一般经历:

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化

3. 双亲委派模型

类加载器层次:

Bootstrap ClassLoader
    ↑
Extension ClassLoader
    ↑
Application ClassLoader

加载规则:

  • 先让父加载器加载
  • 父加载器无法加载时,子加载器才自己加载

优点:

  • 保证安全
  • 避免重复加载

经典问题:为什么 String 不能被替换?

  • 因为 StringBootstrap ClassLoader 加载,应用层无法随意替换核心类。

三、对象创建过程

Java 中创建对象:

User user = new User();

JVM 内部大致过程:

  1. 类加载检查
  2. 分配内存
  3. 初始化零值
  4. 设置对象头
  5. 执行构造方法

对象内存结构

对象通常由三部分组成:

  • 对象头
  • 实例数据
  • 对齐填充

四、垃圾回收(GC)

1. GC 的作用

GC 用来回收 不再使用的对象,释放内存。


2. 判断对象是否死亡的方法

(1)引用计数法

对象每被引用一次,计数加 1;引用失效,计数减 1。

缺点:

  • 无法解决循环引用问题

(2)可达性分析

JVM 实际使用的是 可达性分析算法

GC Roots 出发,看对象是否可达。

常见 GC Roots:

  • 栈中的引用
  • 静态变量引用的对象
  • 常量引用的对象
  • JNI 引用的对象

如果对象到 GC Roots 不可达,就可以被回收。


五、GC 算法

常见 4 种算法:

  • 标记-清除
  • 复制算法
  • 标记-整理
  • 分代收集

1. 标记-清除

流程:

  1. 标记垃圾对象
  2. 清除垃圾对象

优点:

  • 实现简单

缺点:

  • 会产生内存碎片

2. 复制算法

把存活对象复制到另一块区域。

新生代常见结构:

Eden + S0 + S1

优点:

  • 没有内存碎片
  • 回收效率高

缺点:

  • 需要额外空间

3. 标记-整理

流程:

  1. 标记
  2. 移动存活对象
  3. 清理边界外内存

优点:

  • 没有碎片

适用场景:

  • 老年代

4. 分代收集

JVM 的核心思想:

  • 新生代对象存活率低,适合 复制算法
  • 老年代对象存活率高,适合 标记-整理

六、GC 收集器

常见收集器:

  • Serial
  • ParNew
  • CMS
  • G1
  • ZGC

1. Serial GC

特点:

  • 单线程
  • Stop The World

适用场景:

  • 客户端、小内存场景

2. CMS

特点:

  • 低停顿
  • 追求响应时间

流程:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清理

缺点:

  • 会产生内存碎片
  • 对 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 一样?

  1. 避免堆动态扩容
  2. 减少扩容带来的停顿
  3. 提高系统稳定性

生产推荐:

-Xms4g -Xmx4g

2. -Xmx:最大堆内存

作用:
设置 JVM 堆能使用的最大内存。

例如:

-Xmx8g

表示:

  • 堆最大可使用 8GB

堆关系:

Heap = Young + Old

所以:

Xmx = Young + Old

影响:

  • Xmx 太小:容易频繁 GC
  • Xmx 太大: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*