🏭 JVM 是什么?—— 一个超级工厂
想象你要运行一个 Java 程序(比如一个 .jar
文件)。你的电脑(Windows/Mac/Linux)是物理世界,它不懂 Java 程序说什么。JVM 就是建在这个物理世界上的一个虚拟工厂,专门负责执行 Java 程序。
✅ 核心作用:
让同一份 Java 程序(字节码)能在不同操作系统(Windows/Mac/Linux)上运行!
(一次编译,到处运行 —— Write Once, Run Anywhere)
🧩 JVM 的核心工作流程(工厂运作图)
🔹 1. 进货 & 质检(类加载器 - ClassLoader)
- 任务: 把编译好的
.class
文件(字节码,相当于“零件图纸”)搬进工厂。 - 过程:
- 加载: 找到
.class
文件,读入二进制数据。 - 链接:
- 验证: 检查“图纸”是否安全、符合规范(防止恶意代码)。
- 准备: 给类变量(
static
变量)在内存里划好地方,设个初始值(比如 0 或null
)。 - 解析: 把符号引用(名字)替换成直接的内存地址引用(门牌号)。
- 初始化: 正式执行类的静态代码块(
static{}
)和给静态变量赋真正的值(工厂正式开始组装)。
- 加载: 找到
🔹 2. 车间(运行时数据区 - Runtime Data Areas)
工厂内部有几个核心车间,存放程序运行时的东西:
- 📚 方法区(Method Area): 存放“永久性”的东西。
- 放什么: 加载的类信息(类名、父类、方法签名、常量池等)、
static
变量、常量(final
)、编译后的代码(机器指令)。像工厂的“永久仓库”和“设计图纸库”。 - 注意: JDK 8 后,
static
变量和字符串常量池移到了 堆(Heap) 里。
- 放什么: 加载的类信息(类名、父类、方法签名、常量池等)、
- 🗑️ 堆(Heap): 最大的车间,大家共享。
- 放什么: 所有对象实例(
new
出来的东西)和数组。这是垃圾回收(GC)的主战场。想象成堆满各种产品的“大仓库”。 - 特点: 线程共享。需要垃圾回收器不断清理没用的对象腾地方。
- 放什么: 所有对象实例(
- 📝 栈(Stack): 每个工人(线程)有自己的小工作台。
- 放什么: 方法执行时的信息。每调用一个方法,就压入一个栈帧(Stack Frame)。栈帧里包含:
- 局部变量表: 存放方法里的局部变量(基本类型值、对象引用地址)。
- 操作数栈: 进行计算的临时空间(像计算器)。
- 动态链接: 指向方法区里该方法的实际信息。
- 方法返回地址: 方法结束后该回到哪里。
- 特点: 线程私有。后进先出(LIFO)。方法结束,栈帧弹出销毁。非常高效。
- 放什么: 方法执行时的信息。每调用一个方法,就压入一个栈帧(Stack Frame)。栈帧里包含:
- 🧮 程序计数器(Program Counter Register): 每个工人的“进度条”。
- 作用: 记录当前线程正在执行的字节码指令的地址(如果执行的是本地方法则为空)。线程切换后回来,靠它知道上次干到哪了。
- 特点: 线程私有。不会发生内存溢出(
OutOfMemoryError
)。
- 🔧 本地方法栈(Native Method Stack): 调用“本地方法”的专用区。
- 作用: 为用其他语言(如 C/C++)写的、通过 JNI(Java Native Interface)调用的本地方法(Native Method) 服务。
- 特点: 线程私有。作用和结构类似 Java 栈。
🔹 3. 工人(执行引擎 - Execution Engine)
负责真正干活的:
- 解释器(Interpreter): 拿到字节码指令,逐条解释执行(读一条图纸,做一步操作)。启动快,执行慢。
- 即时编译器(JIT Compiler - Just-In-Time Compiler): 工厂运行一段时间后,它会把热点代码(频繁执行的代码段)整个编译成本地机器码,下次直接执行高效的机器码。执行快,但编译需要时间。HotSpot VM 的名字就源于它能找到“Hot Spots”(热点)进行编译优化。
- 垃圾回收器(Garbage Collector - GC): 工厂里最重要的清洁工 👷♂️。自动回收堆里不再被任何引用指向的对象,释放内存空间。这是 Java 自动内存管理的核心,避免了 C/C++ 需要手动
free/delete
的麻烦和风险。
🔹 4. 本地接口(JNI - Java Native Interface)
工厂和外部世界(本地操作系统、其他语言库)沟通的桥梁。让 Java 程序可以调用 C/C++ 等写的库,或者被其他程序调用。
🗑️ 垃圾回收(GC)—— 清洁工的工作(重点!)
这是 JVM 最核心的特性之一:自动内存管理。
- 目标: 回收堆(Heap)里已经“死掉”的对象(没有任何引用指向它)。
- 核心思想:分代收集: 根据对象“年龄”(存活时间)把堆分成不同区域,不同区域用不同回收策略,提高效率。
- 新生代(Young Generation): 刚
new
出来的对象大部分很快死去。- Eden 区: 对象出生地。新对象都放这里。
- Survivor 区(S0, S1): 经历过一次 GC 还活着的对象会移到 Survivor 区(两个区轮流使用)。对象在 Survivor 区之间“熬过”几次 GC,年龄就增加。
- 老年代(Old Generation/Tenured): 在新生代“熬”过一定次数(年龄阈值,默认15)GC 还活着的对象,晋升到这里。这里对象寿命长,GC 频率低。
- 永久代(PermGen) / 元空间(Metaspace)(JDK8+): 存放类元信息等。不属于堆。JDK8 后用元空间(使用本地内存)替代永久代,减少 OOM 风险。
- 新生代(Young Generation): 刚
- GC 过程(简化版):
- Minor GC / Young GC: 只清理新生代(Eden + Survivor)。
- 新对象进 Eden。
- Eden 满 -> 触发 Minor GC。
- 活着的对象复制到空的 Survivor 区(如 S0),年龄+1,清空 Eden 和另一个 Survivor(S1)。
- 下次 Minor GC,活着的对象(包括 S0 里的)复制到 S1,年龄再+1。如此反复。
- 年龄达到阈值 -> 晋升到老年代。
- Major GC / Full GC: 清理整个堆(新生代 + 老年代 + 方法区/元空间)。通常比 Minor GC 慢很多倍!要尽量避免频繁 Full GC。
- Minor GC / Young GC: 只清理新生代(Eden + Survivor)。
- 常用 GC 算法: 标记-清除、标记-整理、复制算法。不同区域的 GC 器组合使用不同算法(如新生代用复制算法)。
🔄 一、GC 算法详解(垃圾回收的核心手段)
垃圾回收的目标是高效清理堆内存中的无效对象,主流算法分三类:
1. 标记-清除(Mark-Sweep)
- 过程:
- 标记:从 GC Roots(如静态变量、栈中局部变量)出发,标记所有可达对象。
- 清除:遍历堆内存,回收未被标记的对象。
- 缺点:
- 内存碎片:产生大量不连续碎片,导致大对象分配失败。
- 效率问题:标记和清除过程效率较低。
2. 复制算法(Copying)
- 过程:
- 将内存分为 Eden区 + 2个Survivor区(S0, S1)。
- 对象首先分配在 Eden 区,Minor GC 时将 存活对象复制到空的 Survivor 区,清空原区域。
- 对象在 Survivor 区间多次复制后(默认15次),晋升老年代。
- 优点:
- 无内存碎片,分配高效。
- 缺点:
- 浪费空间:一半内存(Survivor)始终闲置。
- 适用场景:新生代(对象存活率低)。
3. 标记-整理(Mark-Compact)
- 过程:
- 标记:同标记-清除。
- 整理:将所有存活对象向内存一端移动,清理边界外内存。
- 优点:
- 无内存碎片。
- 缺点:
- 移动对象成本高。
- 适用场景:老年代(对象存活率高)。
⚙️ 二、JVM 调优实战指南
调优核心目标:减少 Full GC 频率与耗时,避免 OOM。
1. 关键参数解析
参数 | 作用 | 示例值 |
---|---|---|
-Xms / -Xmx |
堆初始大小 / 最大大小 | -Xms4g -Xmx4g |
-Xmn |
新生代大小 | -Xmn2g |
-XX:MetaspaceSize |
元空间初始大小(JDK8+) | -XX:MetaspaceSize=256m |
-XX:MaxMetaspaceSize |
元空间最大大小 | -XX:MaxMetaspaceSize=512m |
-XX:SurvivorRatio |
Eden 与 Survivor 区的比例 | -XX:SurvivorRatio=8 (Eden:S0:S1=8:1:1) |
-XX:MaxTenuringThreshold |
对象晋升老年代的年龄阈值 | -XX:MaxTenuringThreshold=15 |
2. GC 日志分析(调优第一步!)
启用日志参数:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
关键指标:
- Young GC 时间:通常应 < 50ms
- Full GC 频率:一天超过1次需警惕
- Full GC 时间:超过1秒是严重问题
3. 调优场景与策略
问题现象 | 可能原因 | 解决方案 |
---|---|---|
频繁 Full GC | 老年代空间不足 | 增大 -Xmx ,减少大对象分配 |
Young GC 时间长 | Survivor 区过小 | 增大 -Xmn 或调整 -XX:SurvivorRatio |
Metaspace OOM | 类加载过多(如动态代理) | 增大 -XX:MaxMetaspaceSize |
GC 停顿时间过长 | 老年代使用 CMS/G1 失败 | 切换为 G1 并优化 -XX:MaxGCPauseMillis |
4. 选择垃圾回收器
回收器 | 特点 | 适用场景 |
---|---|---|
Serial | 单线程,简单高效 | 客户端小应用 |
Parallel | 多线程,吞吐量优先 | 后台计算型应用 |
CMS | 并发标记清除,低停顿 | Web 服务(JDK8 主流) |
G1 | 分Region收集,可预测停顿 | 大内存、低延迟(JDK9+默认) |
ZGC | 亚毫秒级停顿(JDK15+) | 超低延迟金融系统 |
G1 调优示例:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标停顿时间
-XX:G1HeapRegionSize=4m # Region大小(建议不设)
🧩 三、类加载机制深度解析
1. 类加载过程
加载(Loading)
- 通过类名获取二进制字节流。
- 生成
Class
对象(方法区)。
链接(Linking)
- 验证:检查字节码合法性(魔数、语法等)。
- 准备:为静态变量分配内存并赋零值(如
int=0
,boolean=false
)。 - 解析:将符号引用转为直接引用(延迟到初始化前)。
初始化(Initialization)
- 执行静态代码块(
static{}
)和静态变量赋值。 - 触发条件:
new
、访问静态变量/方法、反射等。
- 执行静态代码块(
2. 类加载器(ClassLoader)层次
- Bootstrap ClassLoader(C++实现)
- 加载
JAVA_HOME/lib
的核心类(如java.lang.*
)。
- 加载
- Extension ClassLoader
- 加载
JAVA_HOME/lib/ext
的扩展类。
- 加载
- Application ClassLoader
- 加载
classpath
下的应用类。
- 加载
- Custom ClassLoader
- 用户自定义(如 Tomcat 的
WebappClassLoader
)。
- 用户自定义(如 Tomcat 的
3. 双亲委派模型(核心安全机制)
- 工作流程:
- 收到加载请求时,先委托父加载器处理。
- 父加载器无法完成时,才自己加载。
- 目的:
- 避免核心类被篡改(如自定义
java.lang.String
无效)。 - 保证类全局唯一性。
- 避免核心类被篡改(如自定义
4. 打破双亲委派的场景
- SPI 机制(如 JDBC 驱动加载)
ServiceLoader
用线程上下文类加载器(ThreadContextClassLoader
)加载实现类。
- 热部署(如 Tomcat)
- 每个 Web 应用用独立的
WebappClassLoader
优先加载自身类。
- 每个 Web 应用用独立的
🔧 四、实战工具链
- 监控工具:
jps
:查看 Java 进程jstat -gc <pid>
:实时 GC 统计jmap -heap <pid>
:堆内存分析
- 故障诊断:
jstack <pid>
:抓取线程栈(查死锁)MAT
/VisualVM
:分析堆转储(-XX:+HeapDumpOnOutOfMemoryError
)
📌 总结关键点(记住这些就懂了!)
- 跨平台核心: JVM 让 Java 字节码 (.class) 能在任何装了 JVM 的机器上运行。
- 类加载: 加载、验证、准备、解析、初始化 .class 文件。
- 内存管理(核心!):
- 方法区: 类信息、常量池(JDK8 后部分在堆)。
- 堆: 所有对象和数组,GC 主战场(分新生代、老年代)。
- 栈: 线程私有,放方法调用的栈帧(局部变量、操作数栈等)。
- 程序计数器: 线程私有,指向当前执行的指令地址。
- 本地方法栈: 服务于本地方法。
- 执行引擎: 解释器(启动快)、JIT 编译器(运行快)、垃圾回收器(自动清理堆内存)。
- 自动垃圾回收(GC): JVM 最大福利之一! 基于分代收集(新生代、老年代),自动回收无用对象内存。程序员基本不用操心内存释放(但也需注意避免内存泄漏)。
💡 简单来说
JVM 就是一个建在你电脑操作系统上的虚拟工厂。它接收 Java 编译好的“通用图纸”(字节码),在自己的车间(内存区域)里,由工人(执行引擎)按照图纸生产(创建对象)、计算(执行指令),并有专门的清洁工(GC)自动打扫废弃产品(回收内存),最终让 Java 程序在你的电脑上顺利运行起来,并且不管电脑是 Windows、Mac 还是 Linux!