news 2026/9/2 21:03:51

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/9/2 12:35:12

JavaScript 性能优化:7 个 V8 引擎偏爱的编码模式让你提速 40%

JavaScript 性能优化&#xff1a;7 个 V8 引擎偏爱的编码模式让你提速 40% 引言 在现代 Web 开发中&#xff0c;JavaScript 性能优化是一个永恒的话题。随着应用的复杂度不断提升&#xff0c;开发者越来越依赖底层引擎的优化能力。V8 作为 Chrome 和 Node.js 的核心引擎&#x…

作者头像 李华
网站建设 2026/8/30 23:38:11

华为OD机考双机位B卷 - 面试叫号系统 (Java Python JS C/C++ GO )

最新华为上机考试 真题目录:点击查看目录 华为OD面试真题精选:点击立即查看 2025华为od机试双机位B卷 题目描述 某公司举行招聘会,面试官通过叫号系统,按照应聘者的预约先后次序依次呼叫应聘者面试。如果被叫到的应聘者没有及时到场,面试官叫不到人就会过号处理。第一…

作者头像 李华
网站建设 2026/9/1 17:09:45

语音信号降噪之旅:MATLAB实战

语音信号降噪(语音转.mat—滤波—.mat转降噪后语音&#xff0c;MATLAB)&#xff0c;程序已调通可直接运行。最近在研究语音信号处理&#xff0c;其中降噪是一个关键环节。今天就来跟大家分享一下我利用MATLAB实现语音信号降噪的全过程&#xff0c;从语音转.mat文件&#xff0c;…

作者头像 李华
网站建设 2026/9/2 3:29:58

python+pytest+request 接口自动化测试

一、环境配置 1.安装python3 brew update brew install pyenv 然后在 .bash_profile 文件中添加 eval “$(pyenv init -)” pyenv install 3.5.3 -v pyenv rehash 安装完成后&#xff0c;更新数据库 pyenv versions 查看目前系统已安装的 Python 版本 pyenv global 3.5…

作者头像 李华
网站建设 2026/8/30 0:31:42

Kafka 与 Spark 在大数据实时分析中的集成

Kafka 与 Spark 在大数据实时分析中的集成关键词&#xff1a;Kafka、Spark、实时分析、流处理、数据集成、分布式系统、结构化流 摘要&#xff1a;在大数据时代&#xff0c;实时分析技术成为企业决策的核心驱动力。Apache Kafka 作为高性能消息中间件&#xff0c;与 Apache Spa…

作者头像 李华
网站建设 2026/8/29 4:55:28

K6 性能测试教程:常用功能 - HTTP 请求,指标和检查

这篇文章详细介绍了 K6 中的 HTTP 请求&#xff08;http request&#xff09;功能&#xff0c;解析了常用的性能指标和检查功能。通过 HTTP 请求模拟用户行为&#xff0c;了解性能指标以评估系统响应。文章还深入讲解了如何配置和执行检查&#xff0c;确保性能符合预期标准。无…

作者头像 李华