本文面向初学者,带你深入理解 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.)    │
└─────────────────────────────────────┘
名称 全称 包含内容 用途
JVM Java Virtual Machine 虚拟机 执行字节码
JRE Java Runtime Environment JVM + 核心类库 运行 Java 程序
JDK Java Development Kit JRE + 开发工具 开发 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>
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐