news 2026/2/3 10:01:37

JVM类加载过程:从字节码到运行时对象的诞生

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM类加载过程:从字节码到运行时对象的诞生

字节码的"变身记":从.class文件到运行时对象

一、类加载阶段

.class文件 -> 加载(Loading) -> 链接(Linking) -> 初始化 -> 使用 -> 卸载 ^ 验证>准备>解析

前两篇我们完成了:

解码:拆解了.class文件的二进制结构(CAFEBABE、常量池、方法表...)

定位:追踪了ClassLoader如何找到目标字节码(双亲委派、SPI机制)

那么问题来了:找到的字节码如何"活"起来,变成我们能new出来的对象?

让我们通过一个具体例子,追踪类的完整加载过程:

// Main.java public class Main { public static void main(String[] args) { System.out.println("开始加载Student"); Student student = new Student("张三", 20); System.out.println(student); } } // Person.java class Person { static { System.out.println("Person静态块执行"); } static final String SPECIES = "人类"; // 准备阶段赋值 static int population = 0; // 准备阶段:0,初始化:0 String name; Person(String name) { this.name = name; population++; } } // Student.java class Student extends Person { static { System.out.println("Student静态块执行"); } static final int MAX_AGE = 25; // 准备阶段赋值 static int studentCount = 0; // 准备阶段:0 int score; Student(String name, int score) { super(name); this.score = score; studentCount++; } }

执行流程

1. 执行"java Main" ↓ 2. 加载Main类(触发原因:主类) ↓ 3. 初始化Main类,执行main()方法 ↓ 4. main()第一行:打印"开始加载Student" ↓ 5. main()第二行:new Student("张三", 20) ↓ 6. 检查Student类是否加载 → 未加载 ↓ 7. 加载Student类(Loading阶段) ↓ 【关键步骤】在加载Student时,JVM发现其父类Person未加载 ↓ 8. 先加载父类Person - 读取Person.class二进制 - 创建Person的Klass(元空间) - 递归检查Person的父类(Object)并加载 ↓ 9. 然后加载Student类 - 读取Student.class二进制 - 创建Student的Klass(元空间) - 建立继承关系:Student.klass.super = Person.klass ↓ 10. 链接Person类(Linking阶段) - 验证Person字节码 - 准备Person的静态变量: SPECIES = "人类"(final,直接赋值) population = 0(默认值) - 解析:延迟进行 ↓ 11. 链接Student类(Linking阶段) - 验证Student字节码 - 准备Student的静态变量: MAX_AGE = 25(final,直接赋值) studentCount = 0(默认值) - 解析:延迟进行 ↓ 12. 【注意】此时两个类都已加载和链接,但都未初始化 ↓ 13. 初始化Student类前,发现父类Person未初始化(JVM检查) ↓ 14. 初始化Person类(父类优先) - 执行Person.<clinit>() - 打印"Person静态块执行" - population初始化为0(其实还是0,因为默认值就是0) ↓ 15. 初始化Student类 - 执行Student.<clinit>() - 打印"Student静态块执行" - studentCount初始化为0 ↓ 16. 创建Student实例 - 调用Student.<init>构造器 - 先调用super() → Person.<init> - population++ (1) - studentCount++ (1)

Loading阶段的核心问题

放哪里?JVM加载后的Klass、Class分别数据存放在JVM规定内存的哪块区域?

首先需要对JVM内存区域划分有个大致的概念,参考以下草绘图

图中两处⭐️标记出了class在内存中的存放位置。

1. Meta Space(元空间)⭐️

存放内容:Klass信息

  • Klass:HotSpot虚拟机内部用来表示Java类的C++对象

  • 特点

    • JVM内部使用的数据结构
    • 包含完整的类元信息(vtable、方法代码、布局信息等)
    • 分配在本地内存(Native Memory),不在Java堆中
    • Loading阶段创建
2. Java Heap(堆)⭐️

存放内容:Class对象

  • java.lang.Class:Java程序运行时使用的类对象

  • 特点

    • Java标准的反射API入口
    • 作为锁对象(synchronized(MyClass.class)
    • 分配在Java堆中,参与GC
    • 注意:不是在Loading阶段创建,而是在Initialization阶段创建

当Klass对象在本地内存中被创建完成即代表Loading阶段完成,下一个阶段——Linking(验证、准备、解析)——将为类的"活动"做好最后的安全检查和资源准备。

Linking阶段的核心问题

Verification检查文件内容各项是否符合JVM规范

1. 格式验证:魔数、版本、常量池格式 2. 元数据验证:继承规则、final约束 3. 字节码验证:类型安全、控制流 4. 符号验证:引用存在性

Preparation为静态变量分配内存(元数据区),设置默认值

//比如各类型赋默认值 static int x = 5; // 准备阶段:x = 0 static boolean a = true; //a = false static Object o = new Object(); //o = null //特殊处理:final常量准备阶段即赋值 //准备阶段会直接赋值通常需要符合:1.final修饰,2.基本类型,3.编译阶段能确定的值(常量/常量表达式) static final int y = 10; // 准备阶段:y = 10(常量)

Resolution解析引用

将符号引用转换为直接引用的过程,HotSpot默认采用延迟解析策略——只有在第一次实际使用时才进行解析和绑定,解析结果会缓存起来供后续使用,这显著提升了启动性能和减少了内存占用。

字节码中的符号引用如前文中提到的 Constant Pool: #1. Methodref #... #2 Fieldref #... #3. NameAndType #... ...

直接引用是"可以直接被CPU使用的内存地址信息",它可能是指针、偏移量或索引,但最终目的都是避免运行时的查找过程。

Initialzation阶段的核心问题

  • 加载的最终阶段,让类可以正式投入使用:
    • 创建Class对象(堆内)
    • 执行<clinit>()方法
    • 建立Klass与Class对象的双向引用

<clinit>()方法是由 Java 编译器自动生成的类初始化方法,它负责处理所有静态成员的初始化逻辑。(如果类不存在静态变量和静态代码块,则JVM不会处理)

  • <clinit>()具体做了些什么?

    • 静态变量的最终赋值
    在准备阶段已经为静态变量赋了默认值比如 static int a = 5; 此时a = 0 -> a = 5;
    • 静态代码块执行
    static { // do smt }

相当于是Class存在于JVM内部的构造函数,只负责静态部分,保证一次且仅一次执行,这也是为什么静态代码中不能访问实例成员--因为此时可能还没有任何实例被创建。

二、运行时对象实例

对象创建位置与内存分配

大多数情况下,新创建的对象都会在Young Gen - Eden区分配,针对大对象,基于不同的收集器,会有不同的分配策略。

CMS收集器 (Concurrent Mark-Sweep)

· 大对象直接进入Old Gen参数:-XX:PretenureSizeThreshold=3M;任何大小超过这个参数阈值的对象,将直接在老年代分配,避免大对象在eden区创建导致空间不足以及Eden -> Survivor之间的复制。

CMS老年代是使用并发标记-清除算法,不涉及对象复制,直接将大对象分配在老年代可以避免昂贵的复制开销和触发YoungGC对年轻代造成影响。

·风险点如果老年代空间不足,或因为大对象生命周期很短,会引发频繁且不必要的fgc。

G1收集器(Garbage-First)

·大对象有专门区域:Humongous Region如果一个对象大小超过单个Region大小的50%, 就会被定义为大对象。

大对象不在Eden Region分配,分配时,G1会尝试找到连续的、空闲的Region来存放,这些被大对象占用的Region即Humongous Region

因为G1是将堆划分成N个固定大小的Region进行管理,没有物理上的Young/Old Gen之分,所以为大对象专门设计了管理区域,避免碎片化。

-XX:G1HeapRegionSize决定了Region的大小,也决定了大对象的定义阈值,比如(-XX:G1HeapRegionSize=2M)即超过1M的对象就会进入Humongous Region。

对象的组成结构

运行时对象在堆中的结构由三部分组成:

  • 对象头:Mark Word/Klass指针/数组长度(如果是数组对象)
  • 实例数据
  • 缓存行填充(确保缓存行对齐,大小保持8的倍数)
主要关注对象头⭐️
Mark Word

Mark Word 是 Java 对象头的一部分,存储了对象的运行时状态信息。其结构在不同状态下会复用相同的 32/64 位空间,以节省内存,以64位为例:

  • 无锁状态

  1. Lock(2bits): 01
  2. biased_lock(1bit): 0
  3. age(4bits):对象分代年龄0~15
  4. identity_hashcode(31bits):仅调用hashcode()后会生成,对象哈希值
  5. cms_free(1bit):CMS垃圾收集标记
  6. unused(25bits):未使用空间

仅无锁状态下才会在Mark Word中存储哈希值,线程获取锁时默认切换偏向锁状态

  • 偏向锁状态

  1. Lock(2bits):01
  2. biased_lock(1bit):1(标记为偏向状态)
  3. age(4bits):0~15
  4. thread ID(54bits):持有偏向锁的线程ID
  5. epoch(2bits):偏向版本号(线程访问时优先检查biased标记,若biased_lock ==1,则检查epoch,若对象epoch==Klass.epoch代表偏向有效,才会去检查threadID是否相等,若epoch != Klass.epoch,则当前线程会尝试CAS重偏向替换threadID,若CAS失败则会升级为轻量锁)
  6. unused(2bits): 未使用空间

通过无锁和偏向锁对比可以看出,无锁状态有31bits空间存储hashcode,而偏向锁优化时,需要54bits空间来存放持有的线程ID,此时hashcode与threadID互斥,先获取hashcode则无法进入偏向锁状态,先进入偏向锁状态再获取hashcode则会强行撤销偏向锁状态进入无锁状态(后续再发生竞争则会直接进入重量锁状态)

  • 轻量级锁状态

  1. Lock(2bits):00(代表轻量锁状态)
  2. prt_to_lock_record(62bits):指向栈帧中锁记录的指针

轻量级锁的加锁过程便是通过CAS尝试将无锁/偏向锁状态的对象头替换为指向锁记录的指针无锁状态可直接获得锁,偏向锁状态会先尝试偏向锁撤销,退回无锁状态再尝试替换对象头,成功则获取轻量级锁,失败则膨胀为重量级锁在轻量锁状态获取hashcode同样会直接膨胀为重量锁

  • 重量级锁

  1. Lock(2bits):10
  2. prt_to_monitor(62bits):指向ObjectMonitor的指针

重量级锁状态会创建ObjectMonitor实例,其中存储了锁关键信息,如:owner,原始对象头,竞争队列,等待队列,重入次数,等待线程数,以及hashcode等信息到这一步才算真正到达了系统层面的线程竞争,通过操作系统原语实现线程调度/上下文切换/CAS自旋等复杂机制

总结

从.class文件到运行时对象,JVM通过类加载机制(加载-链接-初始化)在元空间创建Klass结构、在堆中创建Class对象,最终实例化时在堆中分配对象内存,其对象头(Mark Word)会根据使用情况动态变化锁状态(无锁/偏向锁/轻量锁/重量锁),整个过程体现了延迟加载、分层初始化、空间复用等优化设计,让字节码“活”成真正的Java对象。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/1/26 22:57:05

光储(VSG)并网系统:超级电容储能的魅力

光储&#xff08;虚拟同步发电机&#xff09;VSG并网系统&#xff0c;储能为超级电容。 波形好。在当今追求清洁能源高效利用的时代&#xff0c;光储&#xff08;虚拟同步发电机&#xff09;VSG并网系统逐渐成为研究和应用的热点。今天咱们就来唠唠这其中以超级电容作为储能装置…

作者头像 李华
网站建设 2026/1/26 22:15:20

MATLAB 探索湍流对螺旋谱模式纯度的影响

MATLAB计算湍流对螺旋谱模式纯度影响 拉盖尔高斯光束经过湍流介质后的螺旋谱分布&#xff0c;探测概率&#xff0c;模式纯度在光学领域&#xff0c;拉盖尔高斯光束&#xff08;Laguerre - Gaussian beam, LG beam&#xff09;因其独特的螺旋相位结构而备受关注。然而&#xff0…

作者头像 李华
网站建设 2026/2/3 4:07:56

27.PXE高效批量网络装机

1.挂载镜像文件&#xff1a; mkdir -p /mnt/cdrom mount /dev/sr0 /mnt/cdrom 2.安装必要组件&#xff1a; sudo yum install -y httpd dhcp tftp-server syslinux xinetd 配置 TFTP 服务&#xff08;传输引导文件&#xff09; TFTP 用于传输 pxelinux.0、vmlinuz、initr…

作者头像 李华
网站建设 2026/1/30 17:16:54

对比接口测试工具在自动化测试优缺点:Jmeter、Python、Postman

一、JMeter总结&#xff1a;适合对代码不敏感的使用人员&#xff0c;不会代码也可以完成接口自动化&#xff0c;设计框架。适合紧急迭代的项目。JMeter接口测试的优势小巧轻量级&#xff0c;并且开源免费&#xff0c;社区接受度高&#xff0c;比较容易入门支持多协议&#xff0…

作者头像 李华
网站建设 2026/2/3 8:22:08

数据里的“平行宇宙”:用分支管理实现数据的版本控制

适用版本提示&#xff1a;本文提及的 Data Branch 功能适用于 MatrixOne v3.0 及以上版本。 我们想解决的不是“怎么再备份一份”&#xff0c;而是这三件事&#xff1a;随时落一个可靠锚点、开出互不打扰的试验台、把变更做成可审阅/可回放的补丁。 序幕&#xff1a;双线并行的…

作者头像 李华
网站建设 2026/1/29 2:13:17

SMT贴片加工生产车间主要设备有哪些

SMT的全称是SuRFace mount technology&#xff0c;中文意思为表面贴装技术&#xff0c;SMT设备是指用于SMT加工过程需使用的机器或设备&#xff0c;不同厂家根据自身实力规模以及客户要求&#xff0c;配置不同的SMT生产线&#xff0c;可分为半自动SMT生产线和全自动SMT生产线&a…

作者头像 李华