Java虚拟机的执行流程是一条清晰的链路:加载 → 链接(验证、准备、解析)→ 初始化 → 使用 → 卸载。 字节码在运行时数据区里,由解释器和JIT编译器配合执行,最后由垃圾收集器决定哪些类和对象能退场。
Java虚拟机执行流程是怎样的?先把全景图搭起来
把JVM想象成一座工厂:类加载器是负责收料的门卫,运行时数据区是车间和仓库,执行引擎是流水线工人,GC是定时清场的保洁,货物(.class文件)从进门到报废,走的路线是固定的。
| 阶段 | 主要执行者 | 关键动作 |
|---|---|---|
| 加载 | 类加载器 | 读取二进制字节流,在堆中生成Class对象 |
| 链接-验证 | 校验模块 | 文件格式、元数据、字节码、符号引用四类校验 |
| 链接-准备 | 方法区 | 为静态变量分配内存并设零值 |
| 链接-解析 | 常量池 | 符号引用替换为直接引用 |
| 初始化 | 类构造器<clinit> |
执行静态变量赋值与静态代码块 |
| 使用 | 执行引擎 | 解释执行与即时编译并行 |
| 卸载 | 垃圾收集器 | 回收Class对象与对应的元空间 |
据OpenJDK官方文档的描述,整个链路中只有"加载、验证、准备、初始化、卸载"这五个阶段有严格的顺序约束,解析阶段为了支持动态绑定,可以推迟到初始化之后才发生。
JVM类加载到卸载全过程:七个节点逐个拆
加载:把.class文件请进门
加载阶段干三件事,一件都不能少:
- 通过类的全限定名获取二进制字节流,来源不一定是磁盘,jar包、网络、动态代理在运行时生成都算。
- 把字节流代表的静态存储结构转成方法区(JDK 8之后是元空间)的运行时数据结构。
- 在堆中生成一个
java.lang.Class对象,作为方法区数据的访问入口。
谁去取货,由双亲委派模型决定:Bootstrap → Platform(老版本叫Extension)→ Application → 自定义加载器,请求先向上抛,父加载器加载不了才轮到自己,想观察这个过程,启动时加-XX:+TraceClassLoading,或者用java -verbose:class,日志里类名后面跟着的[Loaded ... from ...]就是门卫的收料记录。
链接三兄弟:验证、准备、解析
- 验证:确认字节码不会把虚拟机搞崩,包括魔数
0xCAFEBABE、版本号、语义合法性、栈映射帧等检查。 - 准备:给静态变量分配内存并设零值,注意,
static int a = 1在这一步之后a是0,要等初始化才变成1,被static final修饰的编译期常量是例外,准备阶段就通过ConstantValue属性直接赋值。 - 解析:把常量池里的符号引用转成直接引用,比如把"java/lang/Object"这种字符串换成真正指向类数据的内存地址。
初始化:<clinit>真正跑起来
初始化阶段执行类构造器<clinit>是静态变量赋值语句和静态代码块的合并,触发时机有严格的六种,包括new、访问静态字段或调用静态方法、反射调用、初始化子类时父类先初始化、虚拟机启动时的主类、以及MethodHandle解析结果,反过来,通过子类引用父类静态字段、定义类数组、引用编译期常量,都不会触发初始化,这是面试题的高频陷阱。
使用与卸载:Class对象的最后一程
卸载条件相当苛刻,必须同时满足:该类的所有实例都已被回收、加载它的ClassLoader已被回收、对应的Class对象没有任何引用,生产环境里能触发卸载的,基本只有自定义类加载器场景,比如Tomcat为每个Web应用准备独立加载器、OSGi的模块热插拔、SPI实现类的隔离,普通业务代码用Application ClassLoader加载的类,从启动到进程结束都别指望被卸载。
JVM内存区域划分对比:执行引擎到底在哪干活
线程私有和线程共享的边界,直接决定了并发问题的形态:
| 区域 | 线程归属 | 主要存放 | 典型异常 |
|---|---|---|---|
| 堆 | 共享 | 对象实例、数组 | OutOfMemoryError: Java heap space |
| 元空间 | 共享 | 类元数据、常量池、方法字节码 | OutOfMemoryError: Metaspace |
| 虚拟机栈 | 私有 | 栈帧、局部变量、操作数栈 | StackOverflowError |
| 本地方法栈 | 私有 | native方法调用现场 | StackOverflowError |
| 程序计数器 | 私有 | 当前字节码行号 | 无 |
栈帧:一次方法调用的完整现场
每个方法调用对应一个栈帧,里面装着局部变量表、操作数栈、动态链接和返回地址,局部变量表第0个槽位默认放this(静态方法除外),所以long和double会占用两个槽位,操作数栈是字节码的计算草稿纸,iadd这类指令就是从这里弹出两个数、压回一个结果。
解释器与JIT:两条流水线怎么配合
执行引擎启动时先老老实实解释执行,边跑边用方法调用计数器和回边计数器统计热点,某个方法或循环体足够热,就交给JIT编译成本地机器码,下次直接跑编译后的版本,C1编译器偏重启动速度,C2偏重峰值性能,分层编译默认开启,由-XX:+TieredCompilation控制,编译期还会做逃逸分析,能栈上分配就不放堆里,能标量替换就不建对象,能锁消除就去掉同步。
一个对象的完整旅程:从new到被回收
对象先在TLAB里快速分配,TLAB用完再走Eden区的指针碰撞或空闲列表,经历Minor GC后,存活对象在Survivor区搬两次家,年龄达到阈值(默认15,-XX:MaxTenuringThreshold可调)就晋升老年代,大对象直接进老年代,动态年龄判定还会让一批同龄对象提前晋升,判定生死靠可达性分析,GC Roots包括栈帧中的局部变量、静态变量、常量引用、JNI引用等。
可以动手验证的几条命令
光看理论容易飘,下面这些都能当场跑出结果:
jps -l列出本机Java进程和主类名。jstat -gcutil <pid> 1000每秒刷新各分区使用率和GC次数。jmap -histo:live <pid>触发一次Full GC后看存活对象直方图。jcmd <pid> VM.class_hierarchy <类名>查看类继承链。javap -verbose Xxx.class反编译出常量池、方法表和字节码指令。java -XX:+PrintFlagsFinal -version | grep MetaspaceSize查看参数最终生效值。
如果在排查频繁Full GC时团队内部搞不定,市面上JVM性能调优服务的报价从几千到几万元不等,通常按天或按问题打包计费,北京、上海、杭州这类技术岗位密集的城市服务商更多一些,但在花钱之前,先把GC日志用-Xlog:gc:file=gc.log落盘分析,很多问题自己就能定位。
Java面试JVM类加载机制怎么回答才不失分
业内专家指出,回答这类问题最忌讳背八股,按"流程机制故障"三层展开更稳:
- 先一句话讲清链路:加载、链接、初始化、使用、卸载。
- 再展开双亲委派,说明它解决了类重复加载和核心类被篡改的问题,并补充被打破的典型场景:Tomcat的WebAppClassLoader、JDBC的SPI线程上下文加载器、OSGi、热部署。
- 最后落到线上:
ClassNotFoundException是加载阶段找不到字节流,NoClassDefFoundError是链接或初始化阶段出了问题,Metaspace溢出多半是动态生成类太多或者类加载器泄漏。
Q&A:Java虚拟机执行流程的几个高频疑问
Q1:类加载是整个流程一次性做完的吗?
不是,加载、验证、准备、初始化、卸载的顺序固定,但解析可以推迟到首次使用符号引用时才做,这就是所谓的动态绑定,所以一个类被"加载"了,不代表它已经被"初始化"。
Q2:解释执行和JIT编译,哪个更快?
单次执行解释器占优,因为省掉了编译开销;跑得越久,JIT的本地机器码优势越明显,JVM的策略是先用解释器顶住启动阶段,再让热点代码升级到编译执行,两者是接力关系而非竞争关系。
Q3:类卸载在生产环境常见吗?
不常见,行业共识认为,只有自定义类加载器被回收,且类实例、Class对象都不可达时,卸载才会发生,常规Spring Boot应用里的业务类在整个进程生命周期内都不会卸载,元空间只增不减往往就是类加载器泄漏的信号。
整条链路说到底就一句话:类从字节流进来,经过校验和准备,在初始化时点亮静态状态,靠解释器和JIT跑起来,最后连同对象一起等GC收尾,把这条线在脑子里跑通,读GC日志、看类加载异常、分析元空间溢出,都会顺很多。