JVM 核心总结

一、JVM 是什么

Java Virtual Machine(Java 虚拟机) ,运行 Java 字节码(.class 文件),实现"一次编译,到处运行"的跨平台特性。

复制代码
Java 源码 (.java) → javac 编译 → 字节码 (.class) → JVM 解析执行 → 操作系统机器码

三者关系:

概念 组成
JDK JRE + 开发工具(javac、jar、javadoc 等)
JRE JVM + Java 基础类库(rt.jar 等)
JVM 类加载器 + 运行时数据区 + 执行引擎

二、JVM 内存区域(运行时数据区)

整体分为 线程私有线程共享 两大部分。

复制代码
┌─────────────────────────────────────────────────────┐
│                    JVM 运行时数据区                   │
├────────────────────┬────────────────────────────────┤
│    线程私有(每线程一份)        │     线程共享(全局)   │
├────────────────────┼────────────────────────────────┤
│ 1. 程序计数器 (PC)    │ 1. 堆 (Heap) ------ GC 主战场       │
│ 2. Java 虚拟机栈      │ 2. 元空间 (Metaspace) JDK8+    │
│ 3. 本地方法栈         │    (JDK7-: 永久代 PermGen)     │
│                    │ 3. 字符串常量池 (JDK7+ 在堆中)     │
└────────────────────┴────────────────────────────────┘

2.1 程序计数器(PC Register)

  • 作用:记录当前线程正在执行的字节码指令地址(行号)。
  • 特点唯一不会抛出 OOM 的区域,线程私有,生命周期与线程相同。

2.2 Java 虚拟机栈(JVM Stack)

  • 存储单元:栈帧(Stack Frame),每个方法调用对应一个栈帧的压栈/出栈。
  • 栈帧组成
组成部分 说明
局部变量表 存储方法参数和局部变量,基本类型存值,对象存引用
操作数栈 字节码指令执行时的操作数入栈/出栈
动态链接 指向运行时常量池中该方法的引用
方法返回地址 方法退出后返回到调用者的位置
  • 异常
    • StackOverflowError:线程请求栈深度 > 虚拟机允许深度(如递归太深)
    • OutOfMemoryError:栈动态扩展时无法申请足够内存

2.3 本地方法栈(Native Method Stack)

  • 与虚拟机栈功能类似,专门服务 native 方法(C/C++ 编写)。
  • HotSpot 将本地方法栈和虚拟机栈合二为一。

2.4 堆(Heap)------ 最重要

  • 所有对象实例和数组 都在堆上分配,是 GC 的主要管理区域

  • 堆的分代结构(JDK8 经典分代模型):

    ┌──────────────────────────────────────────┐
    │ Heap │
    ├────────────────────┬─────────────────────┤
    │ 新生代 Young │ 老年代 Old │
    │ ┌────┬─────┬────┐ │ │
    │ │Eden│ S0 │ S1 │ │ 大对象 / 长期存活 │
    │ │ 8 │ 1 │ 1 │ │ │
    │ └────┴─────┴────┘ │ │
    ├────────────────────┴─────────────────────┤
    │ 比例: Eden : S0 : S1 = 8 : 1 : 1 │
    └──────────────────────────────────────────┘

  • JDK8 变化:移除永久代(PermGen),元空间移至本地内存。

2.5 元空间(Metaspace, JDK8+)

对比项 JDK7 及以前(永久代) JDK8+(元空间)
存储位置 堆内存(JVM 管理) 本地内存(操作系统管理)
存储内容 类信息、常量、静态变量 类信息、常量、静态变量、即时编译代码
大小限制 -XX:MaxPermSize -XX:MaxMetaspaceSize,默认无上限
OOM 原因 类/常量太多 类加载过多

注意:字符串常量池在 JDK7 移到堆中,JDK8 依然在堆中。


三、对象创建与内存布局

3.1 对象内存布局

复制代码
┌──────────────┬──────────────┬──────────────┐
│   对象头      │   实例数据    │   对齐填充    │
│ Object Header│ Instance Data│   Padding    │
└──────────────┴──────────────┴──────────────┘

对象头包含:

组成部分 内容
Mark Word 哈希码(HashCode)、GC 分代年龄、锁状态标志、偏向锁线程 ID 等
类型指针 指向元空间中类的 Class 对象
数组长度 仅数组对象有此字段

对齐填充 :保证对象大小是 8 字节的整数倍

3.2 对象访问方式

方式 原理 特点
句柄访问 栈 → 句柄池 → (实例数据指针 + 类型数据指针) 对象移动时只改句柄,稳定
直接指针(HotSpot) 栈 → (实例数据 + 类型指针) 速度快,HotSpot 默认使用

四、垃圾回收(GC)

4.1 判断对象死亡

引用计数法
  • 对象被引用 +1,引用失效 -1,计数为 0 则回收。
  • 缺点 :无法解决循环引用问题(A ↔ B 互相引用,计数永远不会为 0)。
  • HotSpot 不使用此方法。
可达性分析(HotSpot 使用)

GC Roots 为起点,沿引用链向下搜索,不可达的对象判定为"垃圾"。

GC Roots 包括:

  1. 虚拟机栈(局部变量表)中引用的对象
  2. 方法区中静态变量引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中 JNI(Native 方法)引用的对象
  5. 被同步锁(synchronized)持有的对象
四大引用类型
引用类型 回收时机 用途
强引用 Object o = new Object() 永不回收(除非不再被引用) 普通对象
软引用 SoftReference 内存不足时回收 缓存(如图片缓存)
弱引用 WeakReference 下次 GC 一定回收 WeakHashMap、ThreadLocal
虚引用 PhantomReference 任何时候,仅做通知 对象回收跟踪、NIO 直接内存管理

4.2 分代收集思想

分代 特点 回收频率 GC 类型
新生代(Young) 对象存活短,大批快速死亡 高频 Minor GC
老年代(Old) 存活久的大对象 低频 Major GC

对象分配与晋升流程:

复制代码
1. 新对象 → Eden 区分配
2. Eden 满 → Minor GC,存活对象 → Survivor 区(S0 / S1,轮换使用)
3. 每熬过一次 Minor GC,年龄 +1
4. 年龄达到 15(默认)→ 晋升老年代
5. 大对象 → 直接进入老年代(-XX:PretenureSizeThreshold)

GC 类型对比:

GC 类型 回收区域 特点
Minor GC 仅新生代 频繁、速度快
Major GC 仅老年代 通常伴随 Minor GC
Full GC 整个堆 + 元空间 STW 时间长,应尽量避免

4.3 垃圾收集算法

算法 过程 优点 缺点 适用区域
标记-清除 标记垃圾对象 → 直接清除 简单 产生内存碎片 老年代(CMS 基础)
复制算法 存活对象复制到新区域 → 清空旧区域 无碎片、高效 浪费一半内存 新生代
标记-整理 标记存活 → 向一端移动 → 清理边界外 无碎片 移动成本高、STW 长 老年代

4.4 常见垃圾收集器(⭐ 面试重点)

经典收集器一览
复制代码
       新生代                    老年代
   ┌─────────────┐         ┌─────────────┐
   │ Serial      │ ──────→ │ Serial Old  │   单线程,Client 模式
   │ ParNew      │ ──────→ │ CMS         │   ParNew + CMS 经典组合
   │ Parallel    │ ──────→ │ Parallel Old│   JDK8 默认,吞吐量优先
   │ Scavenge    │         │             │
   └─────────────┘         └─────────────┘

   不分代:
   ┌─────────────────────────────────────┐
   │ G1 (JDK9+ 默认)     --- 分区 Region    │
   │ ZGC (JDK11+)        --- 亚毫秒级停顿   │
   │ Shenandoah (JDK12+) --- 并发回收       │
   └─────────────────────────────────────┘
收集器详细对比
收集器 线程 算法 目标 适用场景
Serial 新生代 单线程 复制 简单高效 Client 模式、单核
ParNew 新生代 多线程 复制 缩短 STW 配合 CMS
Parallel Scavenge 新生代 多线程 复制 吞吐量优先 后台计算任务
Serial Old 老年代 单线程 标记-整理 --- Client 模式
Parallel Old 老年代 多线程 标记-整理 吞吐量 配合 Parallel Scavenge
CMS 老年代 并发 标记-清除 低停顿 互联网 Web 应用
G1 全堆 并发 标记-整理 + 复制 可预测停顿 JDK9+ 默认,大堆
ZGC 全堆 并发 染色指针 亚毫秒停顿 超大堆、低延迟
CMS 详细过程(4 个阶段)
复制代码
1. 初始标记 (STW)    --- 标记 GC Roots 直接关联对象,极短
2. 并发标记           --- 从 GC Roots 遍历对象图,与用户线程并发,耗时长
3. 重新标记 (STW)    --- 修正并发标记期间变动的对象,比初始标记长但可控
4. 并发清除           --- 清除垃圾对象,与用户线程并发

CMS 缺点:

  • 内存碎片(标记-清除算法,可能触发 Full GC)
  • 并发阶段占用 CPU 资源
  • 浮动垃圾(并发标记期间新产生的垃圾,本次 GC 无法回收)
  • Concurrent Mode Failure(老年代在并发阶段被填满,退化到 Serial Old)
G1 核心特点
  • 分区思想 :将堆分成多个大小相等的 Region,不再物理划分新生代/老年代。
  • Garbage-First:优先回收垃圾最多的 Region,最大化收益。
  • 可预测停顿 :可设置期望停顿时间 -XX:MaxGCPauseMillis
  • RSet(Remembered Set):记录跨 Region 引用,避免全堆扫描。
各 JDK 版本默认收集器
JDK 版本 默认收集器
JDK 8 Parallel Scavenge(新生代) + Parallel Old(老年代)
JDK 9 G1
JDK 11+ G1(ZGC 作为实验性功能)
JDK 17 推荐 ZGC(低延迟场景)

五、类加载机制

5.1 类加载的 5 个阶段

复制代码
加载 → 验证 → 准备 → 解析 → 初始化
  │      │      │      │      │
  │      │      │      │      └── 执行静态代码块、静态变量赋代码值
  │      │      │      └── 符号引用 → 直接引用(内存地址)
  │      │      └── 静态变量分配内存并赋默认值(非代码赋值)
  │      └── 校验字节码格式、元数据、字节码、符号引用合法性
  └── 读取 class 字节码,生成 Class 对象放入元空间

各阶段详解:

阶段 做什么 关键点
加载 通过全限定名获取字节流 → 转为方法区数据结构 → 生成 Class 对象 可自定义类加载器实现
验证 文件格式、元数据、字节码、符号引用验证 保证 JVM 安全
准备 为静态变量分配内存,设置类型默认值 static int a = 100; 此阶段 a = 0
解析 符号引用(字面量)转为直接引用(内存地址) 可在初始化之后(动态绑定)
初始化 执行 <clinit>() 方法:静态赋值、静态代码块 a 才变成 100

5.2 双亲委派模型

核心思想:类加载器收到加载请求 → 先委派给父加载器 → 父加载器无法加载 → 自己才加载。

复制代码
    ┌──────────────────────────┐
    │  Bootstrap ClassLoader   │  ← C++ 实现,加载 rt.jar(String/Object 等核心类)
    │  启动类加载器              │     $JAVA_HOME/jre/lib/
    ├──────────────────────────┤
    │  Extension ClassLoader   │  ← 加载 ext 目录下的扩展类
    │  扩展类加载器 (JDK9 废弃)   │     $JAVA_HOME/jre/lib/ext/
    ├──────────────────────────┤
    │  Application ClassLoader │  ← 加载 classpath 下用户写的类
    │  应用程序类加载器          │
    ├──────────────────────────┤
    │  自定义类加载器            │  ← 继承 ClassLoader,实现特殊加载逻辑
    └──────────────────────────┘

为什么用双亲委派?

  • 避免类的重复加载(父加载器加载过的,子加载器不用重复加载)
  • 保护核心类库安全(防止用户自定义 java.lang.String 替换核心类)

破坏双亲委派的场景:

  • JDBC(SPI 机制):核心类调用实现类,使用线程上下文类加载器
  • Tomcat:多个 Web 应用需要隔离各自的类
  • OSGi:模块化热部署

六、常见 JVM 调优参数速查

参数 含义 示例
-Xms 初始堆大小 -Xms512m
-Xmx 最大堆大小 -Xmx4g
-Xmn 新生代大小 -Xmn256m
-Xss 线程栈大小 -Xss1m
-XX:NewRatio 老年代/新生代比例 -XX:NewRatio=2(老年代:新生代=2:1)
-XX:SurvivorRatio Eden/Survivor 比例 -XX:SurvivorRatio=8(Eden:S0=8:1)
-XX:MaxTenuringThreshold 晋升老年代年龄阈值 -XX:MaxTenuringThreshold=15
-XX:MetaspaceSize 元空间初始大小 -XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize 元空间最大大小 -XX:MaxMetaspaceSize=256m
-XX:+UseG1GC 使用 G1 收集器 ---
-XX:+UseZGC 使用 ZGC 收集器 ---
-XX:MaxGCPauseMillis 期望 GC 停顿时间 -XX:MaxGCPauseMillis=200

七、面试高频问题速答

Q1:堆和栈的区别?

维度
存储内容 对象实例、数组 局部变量、方法调用
线程共享 全局共享 线程私有
内存管理 GC 自动回收 方法结束自动出栈
异常 OOM StackOverflowError / OOM
大小 可调(-Xmx) 较小(-Xss,默认 1M)

Q2:Minor GC、Major GC、Full GC 区别?

  • Minor GC:仅新生代回收,频繁、速度快。
  • Major GC:仅老年代回收(CMS 的并发收集算 Major GC)。
  • Full GC :整堆 + 元空间回收,STW 长 ,尽量避免。常见触发条件:System.gc()、老年代空间不足、元空间不足、CMS 并发失败等。

Q3:什么时候对象进入老年代?

  1. 年龄超过 MaxTenuringThreshold(默认 15)
  2. 大对象直接分配在老年代
  3. 动态年龄判断:Survivor 中同年龄对象大小总和超过 Survivor 一半,该年龄及以上直接晋升
  4. 空间分配担保:Minor GC 后 Survivor 放不下,直接进入老年代

Q4:CMS 和 G1 的区别?

维度 CMS G1
内存布局 连续物理分代 分区 Region,逻辑分代
算法 标记-清除 标记-整理 + 复制
碎片 会产生碎片 整理避免碎片
停顿预测 无法预测 可设置停顿目标
适用堆大小 中小堆(< 4G-6G) 大堆(> 4G-6G)
并发阶段 不压缩,可能 Full GC 优先回收收益高的 Region

Q5:什么是 STW(Stop The World)?

GC 执行时,所有用户线程暂停。不同收集器的 STW 时长不同:

  • Serial:全程 STW
  • CMS:仅初始标记和重新标记两个短阶段 STW
  • G1:标记阶段和部分回收阶段
  • ZGC:亚毫秒级,几乎无 STW

Q6:类加载过程,双亲委派为什么这样设计?

  • 避免重复加载:父 ClassLoader 加载过的类,子 ClassLoader 不再加载。
  • 安全隔离 :防止自定义的 java.lang.String 等类篡改核心 API。
  • 加载顺序:Bootstrap → Extension → Application → 自定义。

Q7:OOM 常见原因和排查思路?

类型 原因 排查
Java heap space 堆内存不足,对象太多 jmap -dump + MAT/JProfiler 分析
GC overhead limit exceeded GC 时间 > 98% 但回收 < 2% 调大堆或优化代码
Metaspace 类加载过多(动态代理、大量 JSP) -XX:MaxMetaspaceSize 调大
unable to create new native thread 线程创建过多 ulimit -u,减少线程或调小 -Xss

排查工具jpsjstatjmapjstackjconsoleVisualVM、Arthas(阿里开源)。


📌 记忆口诀:类加内存 GC,堆分两代四区,可达分析定生死,CMS 低停 G1 分区,JDK8 默认 Parallel,JDK9 起用 G1。

相关推荐
Data_Journal1 小时前
掌握网页抓取中的分页:完整指南
java·服务器·前端
莫逸风1 小时前
【AgentScope 2.0】05-文件系统(Filesystem)详解
java·ai·agent·springai·agentscope
mifengxing11 小时前
LeetCode 41.缺失的第一个正数|Hard题O(n)+O(1)最优解法深度解析
java·算法·leetcode·排序算法
markinmarkin13 小时前
Spring 中Bean 的作用域有哪些?
java·后端·spring
山荷枝14 小时前
Java学习第十天
java·学习
matlabgoodboy15 小时前
计算机毕设代做|Java Python Matlab APP 全套开发设计
java·python·课程设计
程序员雷欧16 小时前
环形缓冲区深度解析:从基础原理到Disruptor源码的全面剖析
java
xiaoqiMikko16 小时前
Dependabot 面板全绿,不代表你的 Tomcat 没洞
java·spring boot