为你系统地介绍Java虚拟机的核心知识。JVM是Java生态的基石,理解它的工作原理不仅有助于编写更高效的代码,还能帮助你在遇到性能问题时快速定位原因。本文将从JVM的发展历史开始,逐步深入到内存模型、类加载机制、垃圾回收等核心概念。
1. JVM简介与发展史
什么是JVM?
JVM(Java Virtual Machine,Java虚拟机)是通过软件模拟的、运行在完全隔离环境中的完整计算机系统。与VMware、VirtualBox等通过软件模拟物理CPU指令集的虚拟机不同,JVM通过软件模拟Java字节码的指令集,可以说是"一台被定制过的现实当中不存在的计算机"。
JVM的发展历程
1. Sun Classic VM(1996年)
-
世界上第一款商用Java虚拟机
-
只提供解释器,JIT编译器需要外挂
-
解释器和编译器不能协同工作
-
JDK 1.4后被完全淘汰
2. Exact VM(1998年)
-
具备现代高性能虚拟机雏形
-
引入热点探测技术和混合工作模式
-
只在Solaris平台短暂使用
-
后被HotSpot VM替代
3. HotSpot VM(主流)
-
最初由"Longview Technologies"公司设计,1997年被Sun收购
-
2009年Sun被甲骨文收购
-
从JDK 1.3起成为默认虚拟机
-
名称源于其"热点代码探测技术"
-
目前市场占有率最高,是Oracle JDK和OpenJDK的默认虚拟机
4. JRockit(专注于服务器端)
-
专注于服务器端应用,不包含解析器
-
全部代码靠即时编译器编译执行
-
号称"世界上最快的JVM"
-
2008年被Oracle收购,优秀特性被整合到HotSpot
5. J9 JVM(IBM)
-
IBM开发的商用虚拟机
-
用于IBM各种Java产品
-
2017年开源为OpenJ9
6. Taobao JVM(国产骄傲)
-
阿里基于OpenJDK深度定制
-
创新的GCIH技术实现off-heap
-
针对大数据场景优化
-
已在淘宝、天猫全线替换Oracle官方JVM
重要概念:所有这些JVM实现都必须符合Oracle发布的《Java虚拟机规范》,这是一本完整描述JVM组成部分的技术规范。
2. JVM的运行流程
JVM的执行流程可以概括为以下四个核心部分:
-
类加载器(ClassLoader):将.class文件加载到内存
-
运行时数据区(Runtime Data Area):Java程序运行时的内存空间
-
执行引擎(Execution Engine):将字节码翻译成系统指令
-
本地库接口(Native Interface):调用其他语言实现的功能
执行流程:Java代码 → 字节码文件 → 类加载器加载 → 运行时数据区 → 执行引擎翻译 → 本地库接口 → CPU执行
3. JVM运行时数据区(内存布局)
JVM运行时数据区是Java程序运行时的内存空间,与Java内存模型(JMM)是不同的概念。它由以下五大部分组成:
3.1 堆(Heap)- 线程共享
-
作用:存储程序中创建的所有对象实例
-
参数 :
-Xms(最小启动内存)、-Xmx(最大运行内存) -
结构:
-
新生代(Young Generation):存放新建对象
-
Eden区:对象出生地
-
Survivor区(S0/S1):幸存者区
-
-
老年代(Old Generation):存放长期存活对象
-
垃圾回收时,Eden区存活对象会移到未使用的Survivor区,并清理当前Eden和正在使用的Survivor。
3.2 Java虚拟机栈(Stack)- 线程私有
-
作用:描述Java方法执行的内存模型
-
栈帧(Stack Frame)结构:
-
局部变量表:存放方法参数和局部变量
-
操作数栈:方法执行的操作栈
-
动态链接:指向运行时常量池的方法引用
-
方法返回地址:PC寄存器的地址
-
线程私有的概念:由于线程切换需要恢复到正确位置,每个线程需要独立的程序计数器等资源。
3.3 本地方法栈 - 线程私有
- 与虚拟机栈类似,但为本地方法(Native Method)服务
3.4 程序计数器 - 线程私有
-
作用:记录当前线程执行的行号
-
执行Java方法时:记录字节码指令地址
-
执行Native方法时:值为空
-
唯一不会发生OutOfMemoryError的区域
3.5 方法区 - 线程共享
-
作用:存储类信息、常量、静态变量、即时编译器编译后的代码
-
实现演变:
-
JDK7及之前:永久代(PermGen)
-
JDK8及之后:元空间(Metaspace)
-
-
重要变化:
-
元空间使用本地内存,不受JVM最大内存参数限制
-
JDK8将字符串常量池移动到堆中
-
比喻理解:方法区好比汽车定义的"动能提供装置",永久代和元空间则是具体的实现技术(汽油发动机 vs 电动发动机)。
运行时常量池
-
方法区的一部分
-
存放字面量(字符串、final常量、基本数据类型值)和符号引用(类、字段、方法的名称和描述符)
4. 内存溢出问题分析
Java堆溢出
-
现象 :
java.lang.OutOfMemoryError: Java heap space -
原因:对象数量达到堆容量上限
-
调试参数 :
-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError -
分析工具:MAT(Memory Analyzer Tool)
-
区分概念:
-
内存泄漏(Memory Leak):泄漏对象无法被GC回收
-
内存溢出(Memory Overflow):内存对象确实应该存活
-
栈溢出
-
参数设置 :
-Xss设置栈容量 -
两种异常:
-
StackOverflowError:请求深度大于允许的最大深度
-
OutOfMemoryError:扩展栈时无法申请足够内存
-
-
多线程场景:减少最大堆和栈容量来换取更多线程
5. JVM类加载机制
类的生命周期
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
类加载详细过程
-
加载(Loading)
-
通过全限定名获取二进制字节流
-
转换为方法区的运行时数据结构
-
生成Class对象作为访问入口
-
-
验证(Verification)
-
确保字节流符合规范
-
包括文件格式、字节码、符号引用验证
-
-
准备(Preparation)
-
为静态变量分配内存
-
设置默认初始值(如int为0)
-
-
解析(Resolution)
- 将符号引用替换为直接引用
-
初始化(Initialization)
-
执行类构造器方法
-
Java程序代码真正开始执行
-
双亲委派模型
模型结构:
-
启动类加载器(Bootstrap ClassLoader):C++实现,加载核心类库
-
扩展类加载器(Extension ClassLoader):加载ext目录
-
应用程序类加载器(Application ClassLoader):加载应用程序
-
自定义类加载器:根据需要定制
工作流程:子加载器收到请求 → 委派给父加载器 → 最终到达启动类加载器 → 父加载器无法完成时子加载器尝试
优点:
-
避免重复加载类
-
保证Java核心API安全(防止篡改)
破坏双亲委派模型 - JDBC案例
问题背景:JDBC的Driver接口在rt.jar中(由启动类加载器加载),但具体实现在数据库厂商的jar包中。
解决方案:使用线程上下文类加载器(Thread Context ClassLoader)
// DriverManager中的关键代码
ClassLoader callerCL = Thread.currentThread().getContextClassLoader();
交互流程:应用程序 → 线程上下文类加载器 → 加载数据库驱动 → 破坏标准的双亲委派模型
6. 垃圾回收机制
死亡对象判断算法
1. 引用计数算法(Java未采用)
-
每个对象维护引用计数器
-
无法解决循环引用问题
-
Python等语言使用此算法
2. 可达性分析算法(Java采用)
-
从GC Roots对象开始向下搜索
-
形成引用链
-
不可达对象判定为可回收
GC Roots包括:
-
虚拟机栈中引用的对象
-
方法区中静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中JNI引用的对象
引用类型(强度依次递减):
-
强引用(Strong Reference):
Object obj = new Object() -
软引用(Soft Reference):内存不足时回收
-
弱引用(Weak Reference):下次GC时回收
-
虚引用(Phantom Reference):对象回收时收到通知
垃圾回收算法
1. 标记-清除算法
-
标记所有需要回收的对象
-
统一回收被标记对象
-
缺点:效率低、产生内存碎片
2. 复制算法
-
内存分为两块,每次使用一块
-
存活对象复制到另一块
-
HotSpot实现:Eden:Survivor From:Survivor To = 8:1:1
-
适合新生代(对象存活率低)
3. 标记-整理算法
-
标记存活对象
-
向一端移动存活对象
-
清理边界外内存
-
适合老年代
4. 分代收集算法
-
新生代:复制算法
-
老年代:标记-清除或标记-整理
-
对象晋升:大对象、经历15次GC的对象进入老年代
GC类型:
-
Minor GC:新生代GC,频繁快速
-
Full GC/Major GC:老年代GC,较慢(可能慢10倍以上)
垃圾收集器
收集器类型矩阵:
| 收集器 | 新生代/老年代 | 算法 | 特点 | 应用场景 |
|---|---|---|---|---|
| Serial | 新生代 | 复制 | 单线程,STW | Client模式默认 |
| ParNew | 新生代 | 复制 | Serial的多线程版 | 与CMS配合 |
| Parallel Scavenge | 新生代 | 复制 | 吞吐量优先 | 后台计算 |
| Serial Old | 老年代 | 标记-整理 | Serial的老年代版 | Client模式 |
| Parallel Old | 老年代 | 标记-整理 | Parallel Scavenge的老年代版 | 吞吐量优先 |
| CMS | 老年代 | 标记-清除 | 并发收集,低停顿 | B/S系统 |
| G1 | 全区域 | 标记-整理+复制 | 区域划分,可预测停顿 | 替换CMS |
重要概念:
-
并行(Parallel):多GC线程并行,用户线程等待
-
并发(Concurrent):GC与用户线程交替执行
-
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间)
CMS收集器详解:
-
初始标记(STW):标记GC Roots直接关联对象
-
并发标记:GC Roots Tracing
-
重新标记(STW):修正并发标记期间的变化
-
并发清除
G1收集器特点:
-
将堆划分为多个Region
-
可预测停顿时间
-
不产生内存碎片
-
适合大内存应用
一个对象的一生
"我是一个普通的Java对象,出生在Eden区,在Survivor区来回迁移,18岁后进入老年代,经历多次GC后最终被回收。"
7. Java内存模型(JMM)
主内存与工作内存
-
主内存:所有变量存储位置
-
工作内存:线程私有的变量副本
-
线程间变量传递必须通过主内存
内存交互操作(8种原子操作)
-
lock(锁定)
-
unlock(解锁)
-
read(读取)
-
load(载入)
-
use(使用)
-
assign(赋值)
-
store(存储)
-
write(写入)
Java内存模型三大特性
-
原子性:基本数据类型访问具备原子性,复杂操作需要synchronized
-
可见性:volatile、synchronized、final保证
-
有序性:happens-before原则
happens-before原则
-
程序次序规则
-
锁定规则
-
volatile变量规则
-
传递规则
-
线程启动规则
-
线程中断规则
-
线程终结规则
-
对象终结规则
volatile关键字
特性:
-
保证可见性:修改立即对其他线程可见
-
禁止指令重排序
注意:volatile不保证原子性
// 错误示例:volatile不保证自增原子性
private volatile int num = 0;
num++; // 非原子操作,可能线程不安全
适用场景:
-
运算结果不依赖当前值
-
变量不需要与其他状态变量共同约束
单例模式双重检查锁定:
class Singleton {
private volatile static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
volatile必要性:防止指令重排序导致返回未完全初始化的对象
8. 总结
JVM是Java生态的核心,理解其工作原理对于编写高性能、稳定的Java应用至关重要。本文从JVM的发展历史、内存模型、类加载机制、垃圾回收到Java内存模型进行了全面介绍。关键点包括:
-
内存管理:理解堆、栈、方法区的区别和作用
-
类加载:掌握双亲委派模型及其破坏场景
-
垃圾回收:熟悉不同算法和收集器的适用场景
-
线程安全:理解JMM和volatile的内存语义
JVM的优化是一个持续的过程,需要根据具体应用场景选择合适的垃圾收集器和调优参数。希望这篇博客能帮助你建立起对JVM的系统性理解,为深入Java性能优化打下坚实基础。