JVM概述
JVM体系
JDK , JRE , JVM 是 Java 生态中三个核心概念,它们共同构成了Java程序的运行环境。
1. JDK(Java Development Kit):Java 完整的开发环境,包含了JRE和开发工具(比如javac,将java编译成字节码文件)和扩展类库(javax*,org.*),开发者使用 JDK 编写,编译和调试 Java 程序。
2. JRE(Java Runtime Environment): 运行 Java 程序所需的最低环境,包含 JVM 和 核心类库(java.lang,java.util),它为程序提供运行时支持,但不包含开发工具。
3. JVM(Java Virtual Machine): JVM 是 Java 程序的运行环境,负责执行编译后的字节码文件(class)。提供了跨平台能力,通过将字节码转换为特定操作系统认识的机器码来实现“一次编写,到处运行”。
JDK分支
jdk有两种分支,openjdk vs oraclejdk:
许可证与成本
OpenJDK:完全免费,适合个人开发者和小型企业,无生产环境使用限制。
Oracle JDK:开发/测试免费,但生产环境需付费订阅(如Java SE订阅按处理器数量计费)。
性能与稳定性
OpenJDK:性能与Oracle JDK接近(Java 11+),但缺乏某些专有优化(如特定场景下的GC调优)。
Oracle JDK:提供企业级优化(如ZGC低延迟GC、Java Flight Recorder性能监控),稳定性经过严格测试,适合大型应用。
更新与支持
OpenJDK:每6个月发布新版本,社区支持周期通常为6个月(需升级到最新版本或选择第三方LTS版本,如AdoptOpenJDK 8/11)。
Oracle JDK:提供LTS版本(如Java 8、11、17),支持周期长达8年(含安全更新和补丁)。
功能完整性
OpenJDK:包含Java标准功能,但缺少Oracle专有工具(如Java Mission Control)。
Oracle JDK:集成商业功能(如JFR、JMC),适合需要深度性能监控的场景。
虚拟机分类
1. Sun Classic VM:第一款商用 JVM,但是已经被淘汰了。
2. BEA JRockit:专注于服务端应用,号称世界最快的JVM。
3. IBM公司 J9VM:J9是IBM自己开发的一款虚拟机,市场定位与HotSpot接近,服务器端,桌面应用,嵌入式等多用途VM。
4. HotSpot VM:OpenJDK/Oracle JDK 所用的默认虚拟机,也是目前使用范围最广的虚拟机。
5. 其他公司自研的虚拟机,比如Taobao JVM,阿里巴巴基于 OpenJDK HotSpot 深度定制的高性能服务器版 JVM。

查看java版本,可以看到使用的是HotSpot虚拟机
基本结构
JVM结构
Java运行流程(JVM如何运行Java程序)
类编译:
编译是将我们可读的源代码(如 .java 文件)转换为机器或虚拟机可执行的中间代码或机器码的过程。
在 Java 中,编译指将 .java 源代码文件转换为 .class 二进制字节码文件(我们不能直接阅读),前面的文章用过 javap 反汇编工具 可以将二进制字节码文件 转换成可阅读的指令列表。
类加载:
将 .class 文件加载到 JVM内存。
该阶段包括 加载,链接,初始化(执行静态代码块和为静态变量赋值)
类执行:
将字节码文件转换为操作系统认识的机器码文件执行。
- 解释执行:
- JVM 逐条解释字节码指令。
- 启动快,但性能较低(适合冷启动代码)。
- 即时编译(JIT):
- HotSpot 虚拟机对热点代码(多次执行的代码块)编译为本地机器码(如 x86 指令)。
- 编译后性能接近原生代码(如 C++)。
- 混合模式:
- 默认同时使用解释器和 JIT(如 C1/C2 编译器分层编译)。

JVM模型

由上图可以看出,JVM主要由三大部分构成,即 类加载子系统 / 运行时数据区 / 执行引擎
类加载子系统
类加载子系统是将描述类的数据从 Class文件 加载到内存,并对数据进行校验,转换,解析和初始化,最终形成虚拟机可以直接使用的Java类型。
- 加载:将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 验证:确保被加载的类符合JVM的规范,确保虚拟机的安全。
- 解析:将常量池的符号引用替换成为直接引用。
- 初始化:执行类构造器的 <clinit>() 方法,对类变量进行赋值和执行静态代码块。
运行时数据区
Java虚拟机在运行Java程序的时候会将其管理的内存划分为若干个不同的数据区域,这些区域各自有各自的用途,创建,和销毁的时间。
从图片可以看出,总共划分出了五个区域。分别是方法区,栈区,堆区,PC寄存器,本地方法栈。
- 方法区:存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码缓存等数据。
- 堆:存储对象实例和数组,是垃圾回收的主要区域。
- 虚拟机栈:存储方法调用时的局部变量表,操作数栈,动态链接,方法返回地址等信息。
- 本地方法栈:支持本地方法的执行,存储本地方法的局部变量表,操作数栈等信息。
- 程序计数器(PC寄存器):记录当前线程正在执行的字节码指令地址,确保线程切换后能恢复到正确的执行位置。
执行引擎
用于执行 JVM字节码指令,将字节码转换为机器码或直接执行。
- 解释执行:在执行时逐条翻译字节码指令为机器码执行,启动快但执行效率低。
- 编译执行:在执行前先将字节码编译为机器码,再执行编译后的机器码,启动慢但是执行速度快。
类文件结构
测试用类
public class Cat {
private String name = "JVM";
private int age = 5;
}
用 maven 编译成class文件,然后用 vscode 查看 class 文件(需要下载 Hex 插件)

我们知道 class文件 是一个与平台无关的二进制文件,转换后是16进制表示,依次是
| 偏移量(字节) | 类型 | 名称 | 描述 |
|---|---|---|---|
| 0-3 | u4 | magic | 魔数(固定值 0xCAFEBABE) |
| 4-5 | u2 | minor_version | 次版本号(如Java 8为 0x0000) |
| 6-7 | u2 | major_version | 主版本号(如Java 8为 0x0034) |
| 8-9 | u2 | constant_pool_count | 常量池项数(索引范围 1~constant_pool_count-1) |
| 10-(n-1) | cp_info[] | constant_pool | 常量池项列表(constant_pool_count-1 项,每项长度由 tag 决定) |
| n-(n+1) | u2 | access_flags | 访问标志(如 public、final 等) |
| (n+2)-(n+3) | u2 | this_class | 当前类索引(指向常量池中的 CONSTANT_Class_info) |
| (n+4)-(n+5) | u2 | super_class | 父类索引(若为 0 表示无父类,如 java.lang.Object) |
| (n+6)-(n+7) | u2 | interfaces_count | 接口数量 |
| (n+8)-(m-1) | u2[] | interfaces | 接口索引列表(interfaces_count 项,每项指向常量池中的 CONSTANT_Class_info) |
| m-(m+1) | u2 | fields_count | 字段数量 |
| (m+2)-(p-1) | field_info[] | fields | 字段表(fields_count 项,每项长度固定为 6 + attribute_info_length) |
| p-(p+1) | u2 | methods_count | 方法数量 |
| (p+2)-(q-1) | method_info[] | methods | 方法表(methods_count 项,每项长度固定为 6 + attribute_info_length) |
| q-(q+1) | u2 | attributes_count | 属性数量 |
| (q+2)-end | attribute_info[] | attributes | 属性表(attributes_count 项,每项长度由 attribute_length 决定) |
魔数和版本
魔数是固定的 CAFEBABE ,用于验证文件是否为合法的Java类文件。

版本是3D,转换为十进制就是61 jdk8是52,那么61就是jdk17

常量池
0x001A 即 总共有(26 -1 = 25,也就是上表的 n)个常量,索引范围为 1~25

常量池列表
共 25 项,每项长度由 tag 决定
tag 值(16进制) | tag 值(10进制) | 常量类型 | 大小(字节) | 说明 |
|---|---|---|---|---|
0x01 | 1 | CONSTANT_Utf8_info | 3 + length | UTF-8编码的字符串,length表示字节长度,后面跟着length个字节的字符串数据。 |
0x03 | 3 | CONSTANT_Integer_info | 5 | 32位整型常量(int),包含4个字节的整数值。 |
0x04 | 4 | CONSTANT_Float_info | 5 | 32位浮点型常量(float),包含4个字节的浮点数值。 |
0x05 | 5 | CONSTANT_Long_info | 9 | 64位长整型常量(long),包含8个字节的长整数值。占用两个常量池索引。 |
0x06 | 6 | CONSTANT_Double_info | 9 | 64位双精度浮点型常量(double),包含8个字节的双精度浮点数值。占用两个常量池索引。 |
0x07 | 7 | CONSTANT_Class_info | 3 | 类或接口的符号引用,包含2个字节的索引,指向CONSTANT_Utf8_info类型的全限定名。 |
0x08 | 8 | CONSTANT_String_info | 3 | 字符串的符号引用,包含2个字节的索引,指向CONSTANT_Utf8_info类型的字符串内容。 |
0x09 | 9 | CONSTANT_Fieldref_info | 5 | 字段的符号引用,包含2个字节的类索引和2个字节的名称和描述符索引,分别指向CONSTANT_Class_info和CONSTANT_NameAndType_info。 |
0x0A | 10 | CONSTANT_Methodref_info | 5 | 普通方法的符号引用,包含2个字节的类索引和2个字节的名称和描述符索引,分别指向CONSTANT_Class_info和CONSTANT_NameAndType_info。 |
0x0B | 11 | CONSTANT_InterfaceMethodref_info | 5 | 接口方法的符号引用,包含2个字节的类索引和2个字节的名称和描述符索引,分别指向CONSTANT_Class_info和CONSTANT_NameAndType_info。 |
0x0C | 12 | CONSTANT_NameAndType_info | 5 | 字段或方法的名称和描述符的组合,包含2个字节的名称索引和2个字节的描述符索引,分别指向CONSTANT_Utf8_info。 |
0x0F | 15 | CONSTANT_InvokeDynamic_info | 5 | 动态调用点的符号引用(Java 7+引入,用于invokedynamic指令),包含2个字节的引导方法属性索引和2个字节的名称和类型索引,分别指向引导方法表和CONSTANT_NameAndType_info。 |
0x10 | 16 | CONSTANT_Module_info | 3 | 模块的符号引用(Java 9+引入,用于模块系统),包含2个字节的索引,指向CONSTANT_Utf8_info类型的模块名。 |
0x11 | 17 | CONSTANT_Package_info | 3 | 包的符号引用(Java 9+引入,用于模块系统),包含2个字节的索引,指向CONSTANT_Utf8_info类型的包名。 |

举个例子:例如 0x0A 表明tag是CONSTANT_Methodref_info
后面五个字节指向method的地址 可以看到即父类Object的 init 方法
用jdk自带的javap反汇编工具可以查看当前类详细的常量列表

运行时数据区
1.程序计数器
每个线程都有一个(线程私有),是一块较小的内存空间,表示当前线程执行的字节码指令的地址。字节码解释器工作的时候,通过改变计数器的值来选取下一条需要执行的字节码指令。
如果是native方法,PC寄存器不存储字节码的地址,而是标记为 undefined。
PC寄存器不会内存溢出,因为其内存占用极小,大小固定,无需动态分配,而且JVM规定程序计数器不会出现OOM(OutOfMemoryError)错误。
同样使用javap查看字节码文件,前面的数字就表示计数器所记录的执行编号

2.虚拟机栈

同样也是线程私有的,当一个方法被执行的时候,JVM 就会同步创建一个栈帧,将改方法包装成栈帧入栈,栈帧存储局部变量表,操作数栈,动态连接,方法出口等信息。每一个方法被调用直至执行完毕的整个过程对应着栈帧的入栈和出栈。
当栈帧的深度大于虚拟机允许的深度,则会抛出 java.lang.StackOverflowError 异常(通常递归就会出现这种情况)
在一些特殊的虚拟机下,栈是允许内存扩展的(HotSpot不支持),在线程极多的情况下,系统分配给jvm进程的物理内存被吃光了,这时候就会出现 OOM。每个线程一般默认分配1M空间(64位linux,hotspot环境)
3.本地方法栈
类似于虚拟机栈(两个异常也是一样),但是本地方法栈执行的都是native方法,不是java方法,而是其他任意语言的方法。(hotspot中把它和虚拟机栈合二为一了)
4.堆(GC堆)
堆不是线程私有的,而是所有线程共享同一个堆。该内存区域是用来存放java对象实例的。
由于它存放对象的实例,因此也是垃圾回收器集中管理的区域,也叫GC堆。
jdk1.7
堆内存模型划分为三个区域:年轻代,老年代和永久代
jdk1.8
堆内存模型划分为三个区域:年轻代,老年代和元空间(不再有永久代)
1. Young Generation(年轻代)
Eden区:新创建的对象首先分配在此。
Survivor区:分为两个大小相等的 survivor0 和 survivor1,但同一时间仅一个使用。
Minor GC:当 Eden 满时触发,存活对象被复制到空闲的 Survivor 区。
晋升规则:经过多次 Minor GC(默认15次,通过 -XX:MaxTenuringThreshold 配置)仍存活的对象,会被移至 OldGen 区。
2. Old Generation(老年代)
存放生命周期长的对象(如缓存、大对象等)。
Full GC:当老年代空间不足时触发,可能伴随年轻代的回收(标记-清除或标记-整理算法)。
3. Metaspace(元空间)
替代 PermGen:类的元数据(主要保存类信息,class,method,filed等对象)移至本地内存(Native Memory),而非 JVM 堆内。
优势:避免 PermGen 空间固定导致的 OOM 问题。动态调整大小(默认仅受系统内存限制,可通过
-XX:MaxMetaspaceSize限制)。触发 GC 条件:当元空间内存不足时,会触发 Full GC 回收无用的类元数据。
5. 方法区
是线程共享的区域,主要存储类的信息,类里定义的常量,静态变量等,还用来保存编译器编译后的代码缓存。hotspot1.7中将其放在了永久代里面(由GC管理回收),1.8后单独在本地内存中单独开辟了MetaSpace元空间存放一部分内容。(常量池中的对象在堆中)
方法区只是一个逻辑概念,可以说metaspace是它的具体实现。
示例
例如有个 Bootstrap.class 执行main方法全流程
1. 首先JVM会将 Bootstrap.class 加载到内存中的方法区
2. 接着,主线程开辟一块内存空间,并且准备好了pc寄存器,栈和本地方法栈。(线程私有)
3. 然后再线程共享的堆中创建一个 Bootstrap.class 的实例
4. JVM开始执行main方法,将main方法封装成一个栈帧入栈。
5. 如果main方法在执行过程中,调用了其他方法,例如hello方法,那就将hello方法也入栈,hello方法执行玩后就出栈,继续执行main方法。
类加载
类加载的核心就是将 class文件的二进制数据 或者 符合字节码规范的二进制流 加载到JVM内存中
流程:首先加载,然后链接(Linking 包含验证/准备/解析),最后初始化
1. 加载
将class文件的二进制数据(也可以是符合字节码规范的流)读取到内存,然后转换为方法区运行的数据结构,并且在堆内存中生成一个 java.lang.Class对象作为访问方法区数据结构的入口。
jvm 提供了 3个系统加载器,用于将字节码加载到内存。分别是BootstrapClassLoader,ExtClassLoader,AppClassLoader。

Bootstrap ClassLoader
C++语言写的,它在Java虚拟机启动后初始化,它主要负责加载以下路径的文件
1. %JAVA_HOME%/jre/lib/*.jar
2. %JAVA_HOME%/jre/classes/*
3. -Xbootclasspath参数指定的路径
ExtClassLoader
java语言实现,即sun.misc.Launcher$ExtClassLoader ($是内存类 即ExtClassLoader是Launcher的内部类),主要加载:
1. %JAVA_HOME%/jre/lib/ext/*
2. ext下的所有classes目录
3. java.ext.dirs系统变量指定的路径中类库
AppClassLoader
java语言实现,实现类是sun.misc.Launcher$AppClassLoader,负责加载:
1. -classpath所指定的位置的类或者jar文档
2. 也是java程序默认的类加载器
双亲委派机制
与java中的继承概念不一样,类加载器会优先调用父类的load方法,如果父类不能加载,那么自己加载。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,检测是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
//如果没有加载,开始按如下规则执行:
long t0 = System.nanoTime();
try {
if (parent != null) {
//重点!父加载器不为空则调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则调用Bootstrap
Classloader c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass,自己查找并加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
为什么要有双亲委派机制?
(1) 避免核心类被篡改
场景示例:假设用户自定义了一个 java.lang.String 类,试图替换 JDK 的核心类。
双亲委派的作用:
当加载 java.lang.String 时,请求会先委派给 Bootstrap ClassLoader。
启动类加载器发现核心类库中已存在 String 类,直接返回,不会加载用户自定义的 恶意类。
安全性:确保 Java 核心 API 的不可替代性。
(2) 避免重复加载
如果父加载器已加载某个类,子加载器无需重复加载(通过 findLoadedClass 检查)。
内存优化:同一个类只被加载一次,所有子加载器共享同一份 Class 对象。
(3) 维护类加载的层次性
类加载器的父子关系形成了一个逻辑上的“优先级”:
Bootstrap ClassLoader(最高优先级)→ Extension ClassLoader → Application ClassLoader → 自定义类加载器(最低优先级)。
示例:java.util.List 必然由 Bootstrap ClassLoader 加载,即使应用类加载器也能找到该类,也不会重复加载。
2. 链接
验证
class字节码被加载后,为了jvm的安全,还要进行验证,分为四个阶段。
(1)文件格式验证
确保字节码文件(.class)的二进制结构符合 JVM 规范
魔数校验:文件必须以CAFEBABE开头
版本号校验:主次版本号必须在JVM支持的范围内
常量池类型:检测常量类型是否合法
结构完整性:检查字节码结构是否完整
(2)元数据验证
从语法和继承关系层面验证类的元数据是否合法。
验证类继承关系/抽象方法检查/是否有字段冲突/方法重写是否符合
(3)字节码验证
验证方法体内的指令逻辑是否安全,确保运行时不会破坏JVM内存或控制流
深入分析指令的语义逻辑 主要验证类型安全/控制流安全/数据流安全
(4)符号引用验证
确保符号引用能正确解析到实际目标,并且检查访问权限。
| 阶段 | 验证内容 | 错误示例 |
|---|---|---|
| 文件格式验证 | 魔数、版本号、常量池结构 | 损坏的 .class 文件 |
| 元数据验证 | 继承关系、抽象方法、字段冲突 | 继承 final 类 |
| 字节码验证 | 指令逻辑、类型安全、控制流 | 非法跳转、未初始化变量使用 |
| 符号引用验证 | 目标是否存在、访问权限 | 调用不存在的类或方法 |
准备
为class中定义的各种类变量(就是类定义的静态变量 属于这个类的变量)分配内存空间,并且初始化赋值(是该变量的默认值,即int就是0 而不是你指定的值)。
这些静态类变量存储在方法区中,但如果这个变量是对象的话是存储在堆内存中的
解析
解析类之间的关系,需要关联的类被加载
3. 初始化
进行变量初始化赋值 将 666 赋值给 a,注意与上面链接的准备阶段不同,准备阶段是把a设置为默认值,也就是0
public static int a = 666
该阶段完成后,类的信息就会完全进入 jvm 内存,整个字节码加载到 jvm 内存结束
1019

被折叠的 条评论
为什么被折叠?



