JVM架构与执行流程详解
JVM架构与执行流程详解
概述
Java虚拟机(JVM)是Java平台的核心组件,负责执行Java字节码,提供平台无关性、自动内存管理和安全性等特性。本文详细解析JVM的构成和各部分功能,以及Java程序的完整执行流程。
一、JVM的构成和各部分功能
1. 类加载器子系统(ClassLoader Subsystem)
功能职责
- 加载(Loading):读取.class文件到内存中
- 链接(Linking):验证、准备、解析字节码
- 初始化(Initialization):执行类构造器方法
三个核心加载器
1. 启动类加载器(Bootstrap ClassLoader)
- 负责加载核心Java类库(rt.jar等)
- 使用本地代码实现,是JVM的一部分
- 是所有类加载器的父加载器
2. 扩展类加载器(Extension ClassLoader)
- 负责加载扩展目录(jre/lib/ext)中的类
- 是启动类加载器的子加载器
- 使用Java语言实现
3. 应用程序类加载器(Application ClassLoader)
- 负责加载用户类路径(classpath)上的类
- 是扩展类加载器的子加载器
- 也称为系统类加载器
双亲委派模型
- 类加载请求首先委派给父加载器
- 父加载器无法加载时,子加载器才尝试加载
- 确保类的唯一性和安全性
2. 运行时数据区(Runtime Data Areas)
2.1 方法区(Method Area)
功能职责
- 存储类结构信息(类名、父类、接口、字段、方法等)
- 存储运行时常量池(字符串常量、数字常量等)
- 存储静态变量和类变量
- 存储方法字节码和即时编译器编译后的代码
内存特性
- 所有线程共享的内存区域
- 在JVM启动时创建
- 逻辑上是堆的一部分,但物理上可以独立
- 可选择垃圾回收
2.2 堆(Heap)
功能职责
- 存储所有对象实例和数组
- 是垃圾回收的主要区域
内存分区
新生代(Young Generation)
- Eden区:新创建的对象首先分配在此区域
- Survivor区:分为From Survivor和To Survivor两个区域
- 对象晋升:经过多次GC后存活的对象晋升到老年代
老年代(Old Generation)
- 存储长期存活的对象
- 垃圾回收频率较低
- 使用标记-整理或标记-清除算法
永久代/元空间(PermGen/Metaspace)
- Java 8之前:永久代,存储类元数据
- Java 8之后:元空间,使用本地内存
2.3 Java栈(Java Stack)
功能职责
- 存储方法调用的栈帧(Stack Frame)
- 每个方法调用对应一个栈帧
栈帧结构
局部变量表(Local Variable Table)
- 存储方法参数和局部变量
- 以变量槽(Slot)为单位
- 基本类型和引用类型占用不同槽数
操作数栈(Operand Stack)
- 用于字节码指令执行时的操作数存储
- 后进先出(LIFO)结构
- 指令从栈顶获取操作数,结果压入栈顶
动态链接(Dynamic Linking)
- 指向运行时常量池中该栈帧所属方法的引用
- 支持方法调用时的动态绑定
方法返回地址(Return Address)
- 存储方法正常或异常返回后的执行地址
2.4 程序计数器(Program Counter Register)
功能职责
- 记录当前线程执行的字节码指令地址
- 线程私有,每个线程独立维护
- 在线程切换时保存和恢复执行状态
特性
- 如果执行的是Java方法,记录的是正在执行的虚拟机字节码指令地址
- 如果执行的是本地方法,计数器值为空(Undefined)
- 不会发生内存溢出异常
2.5 本地方法栈(Native Method Stack)
功能职责
- 支持本地方法(Native Method)的执行
- 与Java栈类似,但服务于本地方法
特性
- 线程私有
- 可能使用传统的栈(如C栈)来支持本地方法调用
- 具体实现取决于JVM设计
3. 执行引擎(Execution Engine)
3.1 解释器(Interpreter)
功能职责
- 逐条读取、解释和执行字节码指令
- 实现简单,启动速度快
工作流程
- 读取字节码指令
- 解码指令含义
- 执行相应操作
- 移动到下一条指令
3.2 即时编译器(JIT Compiler)
功能职责
- 将热点代码(HotSpot)编译成本地机器码
- 提高程序执行效率
编译策略
客户端编译器(C1)
- 快速启动,优化较少
- 适用于桌面应用程序
服务器端编译器(C2)
- 深度优化,启动较慢
- 适用于服务器端应用程序
分层编译(Tiered Compilation)
- Java 7引入的混合编译策略
- 结合解释执行和不同级别的编译优化
3.3 垃圾回收器(Garbage Collector)
功能职责
- 自动回收不再使用的对象内存
- 防止内存泄漏和内存溢出
垃圾回收算法
标记-清除算法(Mark-Sweep)
- 标记所有存活对象
- 清除未标记的对象
- 产生内存碎片
复制算法(Copying)
- 将内存分为两个区域
- 将存活对象复制到另一个区域
- 清理原区域
- 适用于新生代
标记-整理算法(Mark-Compact)
- 标记存活对象
- 将对象向一端移动
- 清理边界外的内存
- 适用于老年代
分代收集算法(Generational Collection)
- 根据对象生命周期采用不同策略
- 新生代使用复制算法
- 老年代使用标记-清除或标记-整理算法
4. 本地方法接口(JNI)
功能职责
- 提供Java代码调用本地方法的能力
- 实现Java与本地代码的互操作
主要功能
- 加载本地库
- 注册本地方法
- 数据类型转换
- 异常处理
二、Java程序的完整执行流程
阶段1:编写和编译
1.1 编写Java源代码
// HelloWorld.java
public class HelloWorld {public static void main(String[] args) {System.out.println("Hello, World!");}
}
1.2 编译成字节码
javac HelloWorld.java
编译过程
- 词法分析:将源代码分解为标记(Token)
- 语法分析:构建抽象语法树(AST)
- 语义分析:类型检查和方法解析
- 字节码生成:生成.class文件
阶段2:类加载过程
2.1 加载(Loading)
加载步骤
- 通过类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象
类加载器工作流程
- 检查类是否已加载
- 委派父加载器加载
- 父加载器无法加载时,自己尝试加载
- 读取.class文件到内存
2.2 链接(Linking)
验证(Verification)
- 文件格式验证:确保.class文件格式正确
- 元数据验证:对类的元数据进行语义校验
- 字节码验证:确保方法体不会危害虚拟机安全
- 符号引用验证:确保解析动作能正确执行
准备(Preparation)
- 为类变量分配内存并设置初始值
- 静态变量分配在方法区
- 初始值为零值(0、false、null等)
解析(Resolution)
- 将常量池内的符号引用替换为直接引用
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
2.3 初始化(Initialization)
初始化时机
- 创建类的实例(new关键字)
- 调用类的静态方法
- 使用类的静态字段(final常量除外)
- 使用反射方法创建实例
- 初始化子类时,父类必须先初始化
- JVM启动时指定的主类
初始化过程
- 执行类构造器()方法
- ()方法由编译器自动收集类中所有类变量的赋值动作和静态语句块合并产生
- 父类的()方法先于子类执行
- 接口的()方法不需要先执行父接口的()方法
阶段3:运行时执行
3.1 创建主线程
JVM启动流程
- 创建引导类加载器(Bootstrap ClassLoader)
- 加载核心Java类库
- 创建Launcher实例
- 创建应用程序类加载器
- 加载主类
- 调用主类的main()方法
3.2 方法调用执行
栈帧创建和压栈
public class MethodDemo {public static void main(String[] args) {int result = calculate(10, 20);System.out.println("Result: " + result);}public static int calculate(int a, int b) {int sum = a + b;return sum * 2;}
}
执行流程
- main方法栈帧压入Java栈
- 局部变量表存储args参数
- 调用calculate方法,创建新的栈帧
- calculate方法栈帧压栈
- 执行字节码指令
- 方法返回,栈帧弹出
3.3 字节码指令执行
常见字节码指令
- 加载和存储指令:iload, istore, aload, astore
- 运算指令:iadd, isub, imul, idiv
- 类型转换指令:i2l, i2f, i2d
- 对象操作指令:new, getfield, putfield
- 方法调用指令:invokevirtual, invokestatic, invokeinterface
- 控制转移指令:ifeq, ifne, goto
3.4 对象创建和内存分配
对象创建流程
- 类加载检查:检查new指令的参数是否能在常量池中定位到类的符号引用
- 分配内存:在堆中为对象分配内存空间
- 内存空间初始化:将分配的内存空间都初始化为零值
- 设置对象头:设置对象的类元数据信息、哈希码、GC分代年龄等
- 执行方法:按照程序员的意愿进行初始化
内存分配策略
- 指针碰撞(Bump the Pointer):适用于内存规整的情况
- 空闲列表(Free List):适用于内存不规整的情况
- 本地线程分配缓冲(TLAB):为每个线程预先分配内存,避免竞争
阶段4:内存管理和垃圾回收
4.1 垃圾识别算法
引用计数法(Reference Counting)
- 每个对象维护一个引用计数器
- 引用增加时计数器加1,减少时减1
- 计数器为0时对象可被回收
- 无法解决循环引用问题
可达性分析算法(Reachability Analysis)
- 通过GC Roots对象作为起点
- 从这些节点开始向下搜索,搜索路径称为引用链
- 当一个对象到GC Roots没有任何引用链相连时,证明此对象不可用
GC Roots对象包括
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- Java虚拟机内部的引用
- 被同步锁持有的对象
4.2 垃圾回收算法
标记-清除算法(Mark-Sweep)
标记阶段:遍历所有对象,标记存活对象
清除阶段:回收未标记的对象内存
优点:实现简单
缺点:产生内存碎片,效率不稳定
复制算法(Copying)
将内存分为大小相等的两块
每次只使用其中一块
垃圾回收时,将存活对象复制到另一块
清理已使用的内存块
优点:没有内存碎片
缺点:内存利用率只有50%
标记-整理算法(Mark-Compact)
标记阶段:标记所有存活对象
整理阶段:将存活对象向一端移动
清理阶段:清理边界外的内存
优点:没有内存碎片
缺点:移动对象成本较高
分代收集理论
- 弱分代假说:绝大多数对象都是朝生夕死的
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数
4.3 垃圾回收器
Serial收集器
- 单线程收集器
- 新生代使用复制算法
- 老年代使用标记-整理算法
- 适用于客户端应用
ParNew收集器
- Serial收集器的多线程版本
- 新生代收集器
- 与CMS收集器配合使用
Parallel Scavenge收集器
- 吞吐量优先的收集器
- 新生代使用复制算法
- 老年代使用标记-整理算法
- 适用于后台运算任务
CMS收集器(Concurrent Mark Sweep)
- 以获取最短回收停顿时间为目标
- 基于标记-清除算法
- 并发收集,低停顿
- 会产生内存碎片
G1收集器(Garbage First)
- 面向服务端应用的垃圾收集器
- 将堆划分为多个Region
- 可预测的停顿时间模型
- 整体基于标记-整理,局部基于复制算法
阶段5:程序终止
5.1 正常终止
终止条件
- 所有非守护线程执行完毕
- 调用System.exit()方法
- 最后一个非守护线程结束
终止流程
- 执行所有已注册的关闭钩子(Shutdown Hook)
- 执行finalize()方法(如果对象重写了此方法)
- JVM停止运行
5.2 异常终止
终止条件
- 发生未捕获的异常
- 调用Runtime.getRuntime().halt()
- 操作系统强制终止
终止特点
- 不会执行关闭钩子
- 立即终止JVM运行
- 可能造成资源泄漏
三、JVM的关键特性
1. 平台无关性
实现原理
- 字节码作为中间表示
- 不同平台有对应的JVM实现
- 一次编译,到处运行
优势
- 开发效率高
- 部署简单
- 维护成本低
2. 自动内存管理
管理范围
- 对象内存分配
- 垃圾回收
- 内存碎片整理
优势
- 避免内存泄漏
- 提高开发效率
- 减少程序崩溃
3. 安全性
安全机制
- 字节码验证
- 类加载安全
- 访问控制
- 安全沙箱
优势
- 防止恶意代码
- 保护系统资源
- 确保程序稳定
4. 高性能
性能优化
- 即时编译(JIT)
- 热点代码优化
- 内联缓存
- 逃逸分析
优势
- 接近本地代码性能
- 自适应优化
- 低延迟运行
5. 多线程支持
线程管理
- 线程创建和销毁
- 线程同步
- 线程调度
优势
- 充分利用多核CPU
- 提高程序响应性
- 支持并发编程
四、JVM调优参数
内存相关参数
# 堆内存设置
-Xms512m # 初始堆大小
-Xmx1024m # 最大堆大小
-Xmn256m # 新生代大小# 方法区设置
-XX:PermSize=64m # 永久代初始大小(Java 8之前)
-XX:MaxPermSize=128m
-XX:MetaspaceSize=64m # 元空间初始大小(Java 8之后)
-XX:MaxMetaspaceSize=128m# 栈内存设置
-Xss1m # 每个线程栈大小
垃圾回收相关参数
# 垃圾回收器选择
-XX:+UseSerialGC # 使用Serial收集器
-XX:+UseParallelGC # 使用Parallel收集器
-XX:+UseConcMarkSweepGC # 使用CMS收集器
-XX:+UseG1GC # 使用G1收集器# GC日志参数
-XX:+PrintGCDetails # 打印GC详细信息
-XX:+PrintGCTimeStamps # 打印GC时间戳
-Xloggc:gc.log # GC日志文件
性能优化参数
# JIT编译参数
-XX:+TieredCompilation # 启用分层编译
-XX:CompileThreshold=1000 # 方法调用阈值# 内存分配参数
-XX:+UseTLAB # 启用线程本地分配缓冲
-XX:TLABSize=64k # TLAB大小
五、总结
JVM作为Java技术的核心,通过精妙的架构设计实现了平台无关性、自动内存管理和高性能运行。理解JVM的构成和执行流程对于Java开发者至关重要,不仅有助于编写高质量的代码,还能在遇到性能问题时进行有效的调优。
随着Java技术的不断发展,JVM也在持续演进,新的特性和优化不断被引入,为Java应用程序提供更好的性能和更丰富的功能。掌握JVM原理是每个Java开发者必备的技能。
