本文面向初学者,带你深入理解 JVM 的核心概念和工作原理。


一、什么是 JVM?

1.1 从一个问题开始

你有没有想过,为什么 Java 程序可以在 Windows、Linux、Mac 上运行,而不需要重新编译?

这背后的大功臣就是 JVM(Java Virtual Machine),也就是 Java 虚拟机

1.2 JVM 是什么?

简单来说,JVM 就是一个"翻译官"。

┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  Java 代码   │ ──→  │    JVM      │ ──→  │  电脑硬件    │
│  (.java)    │      │  (翻译官)   │      │  (执行)     │
└─────────────┘      └─────────────┘      └─────────────┘

工作流程:

  1. 你写的 Java 代码(.java 文件)

  2. 编译器把它变成字节码(.class 文件)

  3. JVM 把字节码翻译成电脑能懂的指令

  4. 电脑执行这些指令

1.3 JVM 的组成部分

JVM 主要由四部分组成:

组件作用类比
类加载器加载 .class 文件快递员(送货上门)
运行时数据区管理内存仓库(存放货物)
执行引擎执行字节码工人(干活)
本地接口调用本地方法翻译(对外沟通)
┌─────────────────────────────────────────────────────────────┐
│                        JVM 组成结构                          │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐    ┌─────────────────────────────────────┐ │
│  │  类加载器    │    │         运行时数据区                  │ │
│  │ (ClassLoader)│    │  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐   │ │
│  └─────────────┘    │  │堆   │ │栈   │ │方法区│ │程序计│   │ │
│         │           │  │     │ │     │ │     │ │数器  │   │ │
│         ▼           │  └─────┘ └─────┘ └─────┘ └─────┘   │ │
│  ┌─────────────┐    └─────────────────────────────────────┘ │
│  │  执行引擎    │                     │                      │
│  │(Execution   │                     ▼                      │
│  │  Engine)    │    ┌─────────────────────────────────────┐ │
│  └─────────────┘    │         本地接口                      │ │
│         │           │     (Native Interface)               │ │
│         ▼           └─────────────────────────────────────┘ │
│  ┌─────────────┐                                            │
│  │  本地方法库  │                                            │
│  └─────────────┘                                            │
└─────────────────────────────────────────────────────────────┘

1.4 JVM vs JRE vs JDK

很多人分不清这三个概念,其实很简单:

┌─────────────────────────────────────┐
│              JDK                     │
│  ┌───────────────────────────────┐  │
│  │              JRE              │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │           JVM           │  │  │
│  │  └─────────────────────────┘  │  │
│  │  + 核心类库                   │  │
│  └───────────────────────────────┘  │
│  + 开发工具(javac, java, etc.)    │
└─────────────────────────────────────┘
名称全称包含内容用途
JVMJava Virtual Machine虚拟机执行字节码
JREJava Runtime EnvironmentJVM + 核心类库运行 Java 程序
JDKJava Development KitJRE + 开发工具开发 Java 程序

简单记忆:

  • 想运行 Java 程序?装 JRE

  • 想开发 Java 程序?装 JDK

  • JVM 是 JRE 的核心部分


二、JVM 能干什么?

2.1 核心功能

JVM 的核心功能可以概括为四点:

1. 加载代码

  • 将 .class 文件加载到内存

  • 通过类加载器(ClassLoader)实现

2. 验证代码

  • 检查字节码是否合法

  • 防止恶意代码破坏系统

3. 执行代码

  • 将字节码翻译成机器码

  • 通过执行引擎实现

4. 内存管理

  • 自动分配内存

  • 自动回收内存(垃圾回收)

2.2 为什么需要 JVM?

跨平台

Java 代码 ──编译──→ 字节码 ──运行──→ Windows JVM ──执行──→ Windows
                │
                ├──运行──→ Linux JVM   ──执行──→ Linux
                │
                └──运行──→ Mac JVM     ──执行──→ Mac

安全性

  • 沙箱机制:限制代码访问权限

  • 字节码验证:防止恶意代码

  • 访问控制:保护系统资源

自动内存管理

  • 程序员不需要手动释放内存

  • 垃圾回收器自动回收不再使用的对象

  • 避免内存泄漏和内存溢出

性能优化

  • JIT 即时编译:热点代码编译成机器码

  • 自适应优化:根据运行情况优化代码

  • 多线程支持:充分利用多核 CPU

2.3 实际应用场景

应用场景说明示例
Web 应用服务器端开发Spring Boot、Tomcat
Android移动应用开发Android App
大数据数据处理Hadoop、Spark
云计算微服务架构Spring Cloud
游戏开发游戏服务器Minecraft 服务器

三、JVM 内存结构

3.1 运行时数据区

JVM 内存分为五个部分:

┌─────────────────────────────────────────────────────────────┐
│                        JVM 内存结构                          │
├─────────────────────────────────────────────────────────────┤
│  线程私有                        │  线程共享                  │
│  ┌─────────────┐                │  ┌─────────────────────┐  │
│  │ 程序计数器   │                │  │       堆(Heap)     │  │
│  └─────────────┘                │  └─────────────────────┘  │
│  ┌─────────────┐                │  ┌─────────────────────┐  │
│  │  虚拟机栈    │                │  │  方法区(元空间)    │  │
│  └─────────────┘                │  └─────────────────────┘  │
│  ┌─────────────┐                │                           │
│  │  本地方法栈  │                │                           │
│  └─────────────┘                │                           │
└─────────────────────────────────────────────────────────────┘

3.2 各部分详解

程序计数器(Program Counter Register)
  • 线程私有:每个线程都有自己的程序计数器

  • 作用:记录当前线程执行的字节码行号

  • 特点:唯一不会出现 OOM 的区域

// 程序计数器示例
public void test() {
    int a = 1;  // 程序计数器记录这行
    int b = 2;  // 然后记录这行
    int c = a + b;  // 最后记录这行
}
虚拟机栈(VM Stack)
  • 线程私有:每个线程都有自己的虚拟机栈

  • 作用:存储方法调用的信息

  • 组成:由一个个栈帧组成

┌─────────────────────────────────────────────────────────────┐
│                       虚拟机栈                               │
├─────────────────────────────────────────────────────────────┤
│  ┌───────────────────────────────────────────────────────┐  │
│  │                    栈帧(Stack Frame)                 │  │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐   │  │
│  │  │ 局部变量表   │  │ 操作数栈    │  │ 动态链接    │   │  │
│  │  └─────────────┘  └─────────────┘  └─────────────┘   │  │
│  │  ┌─────────────┐                                      │  │
│  │  │ 方法出口    │                                      │  │
│  │  └─────────────┘                                      │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
本地方法栈(Native Method Stack)
  • 线程私有:为 Native 方法服务

  • 作用:存储本地方法调用的状态

  • 示例:Object.hashCode()、Thread.start0()

堆(Heap)
  • 线程共享:所有线程共享同一个堆

  • 作用:存储对象实例

  • 特点:垃圾回收的主要区域

┌─────────────────────────────────────────────────────────────┐
│                          堆(Heap)                          │
├─────────────────────────────────────────────────────────────┤
│  新生代(Young Generation)          │  老年代(Old Gen)    │
│  ┌─────┬─────┬─────┐                │  ┌─────────────────┐  │
│  │ Eden│  S0 │  S1 │                │  │                 │  │
│  │  8  │  1  │  1  │                │  │                 │  │
│  └─────┴─────┴─────┘                │  └─────────────────┘  │
│      80%    10%  10%                │                       │
└─────────────────────────────────────────────────────────────┘

新生代:

  • Eden:新对象分配的区域

  • S0(Survivor 0):幸存者区域

  • S1(Survivor 1):幸存者区域

  • 比例:8:1:1

老年代:

  • 存放长期存活的对象

  • 默认经过 15 次 GC 后进入老年代

方法区(Method Area)
  • 线程共享:存储类信息

  • 作用:存储类元数据、常量、静态变量

  • JDK 8 之后:改为元空间(Metaspace)

┌─────────────────────────────────────────────────────────────┐
│                      方法区(元空间)                         │
├─────────────────────────────────────────────────────────────┤
│  ┌───────────────────────────────────────────────────────┐  │
│  │ 类信息                                                 │  │
│  │ - 类名、父类、接口                                      │  │
│  │ - 字段、方法                                            │  │
│  │ - 访问修饰符                                            │  │
│  └───────────────────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ 常量池                                                  │  │
│  │ - 字面量                                                │  │
│  │ - 符号引用                                              │  │
│  └───────────────────────────────────────────────────────┘  │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ 静态变量                                                │  │
│  │ - static 修饰的变量                                     │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

3.3 堆内存细分

堆内存分为新生代和老年代:

对象创建 ──→ Eden 区
              │
              ├── 存活 ──→ S0 区
              │              │
              │              ├── 存活 ──→ S1 区
              │              │              │
              │              │              ├── 存活 ──→ 老年代
              │              │              │
              │              │              └── 死亡 ──→ 被回收
              │              │
              │              └── 死亡 ──→ 被回收
              │
              └── 死亡 ──→ 被回收

四、垃圾回收机制

4.1 什么是垃圾回收?

垃圾回收(Garbage Collection,GC) 是 JVM 自动管理内存的机制。

为什么需要垃圾回收?

  • 手动管理内存容易出错

  • 内存泄漏会导致程序崩溃

  • 垃圾回收让程序员专注业务逻辑

4.2 如何判断对象是否可回收?

引用计数法

每个对象有一个引用计数器:

  • 有引用时 +1

  • 引用失效时 -1

  • 计数器为 0 时可回收

Object A ──引用──→ Object B
    │                  │
    │                  ▼
    └──────────────→ Object C(计数器:0,可回收)

缺点: 无法解决循环引用

// 循环引用示例
Object a = new Object();
Object b = new Object();
a.ref = b;  // a 引用 b
b.ref = a;  // b 引用 a
// 即使不再使用,引用计数器也不为 0
可达性分析法

从 GC Roots 开始搜索:

  • 能到达的对象是存活的

  • 不能到达的对象是可回收的

GC Roots
    │
    ▼
  Object1 ──→ Object2 ──→ Object3
                              │
                              ▼
                          Object4(可回收)

GC Roots 包括:

  • 虚拟机栈中引用的对象

  • 方法区中静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中引用的对象

4.3 垃圾回收算法

标记-清除算法
步骤:
1. 标记可回收对象
2. 清除标记对象
​
内存变化:
[对象A][垃圾][对象B][垃圾][对象C]
      ↓
[对象A][  ][对象B][  ][对象C]

缺点: 产生内存碎片

复制算法
步骤:
1. 将内存分为两块
2. 只使用其中一块
3. 清空时将存活对象复制到另一块
​
内存变化:
[对象A][对象B][对象C][  ][  ]  ← 使用中
[  ][  ][  ][  ][  ]          ← 空闲
      ↓
[  ][  ][  ][  ][  ]          ← 清空
[对象A][对象B][对象C][  ][  ]  ← 复制过来

优点: 没有内存碎片 缺点: 内存利用率只有 50%

标记-整理算法
步骤:
1. 标记可回收对象
2. 将存活对象向一端移动
3. 清理边界外内存
​
内存变化:
[对象A][垃圾][对象B][垃圾][对象C]
      ↓
[对象A][对象B][对象C][  ][  ]

优点: 没有内存碎片 缺点: 移动对象开销大

分代收集算法

根据对象存活周期,将内存分为不同区域:

区域算法原因
新生代复制算法对象存活率低
老年代标记-整理算法对象存活率高

4.4 常见垃圾回收器

回收器特点适用场景
Serial单线程,简单高效客户端程序
ParNew多线程版本的 Serial服务端程序
Parallel Scavenge关注吞吐量后台计算
CMS并发收集,低停顿互联网应用
G1分区收集,可预测停顿大内存应用
ZGC超低停顿大内存应用

CMS 收集器工作流程:

1. 初始标记(STW)
2. 并发标记
3. 重新标记(STW)
4. 并发清除

G1 收集器特点:

┌─────────────────────────────────────────────────────────────┐
│                        G1 收集器                             │
├─────────────────────────────────────────────────────────────┤
│  ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐         │
│  │  E  │  E  │  S  │  O  │  O  │  H  │  E  │  S  │         │
│  └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘         │
│  E: Eden  S: Survivor  O: Old  H: Humongous                 │
└─────────────────────────────────────────────────────────────┘

五、JVM 调优入门

5.1 为什么要调优?

  • 减少 GC 停顿时间:提高用户体验

  • 提高系统吞吐量:处理更多请求

  • 避免内存溢出:保证系统稳定

5.2 常见 JVM 参数

堆内存设置
# 初始堆大小
-Xms512m
​
# 最大堆大小
-Xmx1024m
​
# 新生代大小
-Xmn256m
​
# 示例:设置初始堆 512M,最大堆 1G
java -Xms512m -Xmx1024m -jar app.jar
元空间设置
# 元空间初始大小
-XX:MetaspaceSize=128m
​
# 元空间最大大小
-XX:MaxMetaspaceSize=256m
GC 设置
# 使用 G1 收集器
-XX:+UseG1GC
​
# 最大 GC 停顿时间
-XX:MaxGCPauseMillis=200
​
# GC 线程数
-XX:ParallelGCThreads=4
日志设置
# 打印 GC 详细信息
-XX:+PrintGCDetails
​
# 打印 GC 时间戳
-XX:+PrintGCDateStamps
​
# 输出 GC 日志到文件
-Xloggc:gc.log

5.3 常见问题排查

内存溢出(OOM)
# 查看堆内存使用情况
jmap -heap <pid>
​
# 查看对象占用情况
jmap -histo <pid>
​
# 导出堆转储文件
jmap -dump:format=b,file=heap.hprof <pid>
CPU 使用率高
# 查看线程 CPU 使用情况
top -Hp <pid>
​
# 导出线程栈
jstack <pid> > thread.txt

5.4 调优案例

案例:Web 应用频繁 Full GC

问题现象:

  • 应用响应变慢

  • 日志显示频繁 Full GC

排查过程:

  1. 查看 GC 日志,发现 Full GC 频率高

  2. 使用 jmap -heap 查看堆内存,发现老年代接近满

  3. 使用 jmap -dump 导出堆转储

  4. 使用 MAT 工具分析,发现内存泄漏

解决方案:

  1. 增加堆内存大小:-Xms2g -Xmx2g

  2. 修复内存泄漏:某个集合不断添加对象,未清理

  3. 调整 GC 参数:使用 G1 收集器

优化效果:

  • Full GC 频率从每分钟 1 次降到每小时 1 次

  • 应用响应时间从 2 秒降到 200 毫秒


六、常见问题与面试题

6.1 常见问题

Q1:Java 为什么能跨平台?

A:因为 JVM 将 Java 代码编译成字节码,然后由不同平台的 JVM 解释执行。字节码是平台无关的,JVM 是平台相关的。

Q2:堆和栈的区别?

对比项
线程共享
存储内容对象实例局部变量
生命周期GC 管理方法结束自动释放
大小较大较小
速度较慢较快

Q3:什么是内存泄漏?

A:对象不再使用,但无法被 GC 回收。常见原因:

  • 集合类持有对象引用

  • 未关闭的资源(数据库连接、文件流)

  • 静态集合类

Q4:什么是内存溢出?

A:内存不足,无法分配新对象。常见原因:

  • 堆内存设置过小

  • 内存泄漏导致

  • 对象过大

6.2 面试高频题

1. JVM 内存结构有哪些?

答:JVM 内存分为五部分:

  • 程序计数器(线程私有)

  • 虚拟机栈(线程私有)

  • 本地方法栈(线程私有)

  • 堆(线程共享)

  • 方法区(线程共享)

2. 垃圾回收算法有哪些?

答:主要有四种:

  • 标记-清除算法

  • 复制算法

  • 标记-整理算法

  • 分代收集算法

3. 如何判断对象是否可回收?

答:两种方法:

  • 引用计数法(有循环引用问题)

  • 可达性分析法(主流方法)

4. 常见的垃圾回收器有哪些?

答:

  • Serial、ParNew、Parallel Scavenge

  • CMS、G1、ZGC

5. 如何进行 JVM 调优?

答:

  1. 监控 GC 日志

  2. 分析堆内存使用

  3. 调整 JVM 参数

  4. 修复内存泄漏


七、总结

核心知识点回顾

  1. JVM 是什么:Java 虚拟机,实现跨平台

  2. JVM 组成:类加载器、运行时数据区、执行引擎、本地接口

  3. 内存结构:堆、栈、方法区、程序计数器、本地方法栈

  4. 垃圾回收:自动内存管理,四种算法

  5. JVM 调优:监控、分析、调参、修复

学习建议

  1. 先理解概念:不要急于深入细节

  2. 多动手实践:使用工具分析 JVM

  3. 关注面试题:巩固知识

  4. 循序渐进:先掌握基础,再学习高级特性

推荐资源


恭喜! 你已经掌握了 JVM 的核心知识。接下来,尝试在你的项目中实践吧!

如果你觉得这篇文章有帮助,欢迎点赞、收藏、关注!


附录:常用 JVM 命令

# 查看 JVM 版本
java -version
​
# 查看 JVM 参数
java -XX:+PrintFlagsFinal -version
​
# 查看进程 ID
jps
​
# 查看堆内存
jmap -heap <pid>
​
# 查看线程栈
jstack <pid>
​
# 查看 GC 日志
jstat -gcutil <pid> 1000
​
# 导出堆转储
jmap -dump:format=b,file=heap.hprof <pid>

更多推荐