JVM(一)----- 类加载过程

一、什么是类加载(Class Loading)?

在 Java 程序运行时,类(.class 文件)并不是一次性全部加载进 JVM 的,而是 按需动态加载 的。这个过程由 类加载器(ClassLoader) 完成(我们应该知道类加载器的结构,是各自完成自己负责的类的加载任务),它的任务是:

把类的字节码文件加载到 JVM 内存中,并在方法区(元空间)中创建对应的类元数据结构。

二、类加载的七个阶段(从加载到使用)

JVM 规范定义了类的完整生命周期:

加载(Loading)
→ 连接(Linking)
├ 验证(Verification)
├ 准备(Preparation)
└ 解析(Resolution)
→ 初始化(Initialization)
→ 使用(Using)
→ 卸载(Unloading)

我们主要分析前四步,因为 它们决定类是如何进入内存、被准备好使用的

① 加载(Loading)

目标:

  • 通过类的全限定名(如 java.lang.String)获取其字节码文件;

  • 将字节码读入内存;

  • 元空间(Metaspace) 中为该类生成对应的 类元数据结构(Class对象的内部描述)

  • 生成一个 java.lang.Class 实例(代表该类的"镜像")。

加载的来源可能是:

  • 本地文件系统(.class.jar

  • 网络(如 Web Start)

  • 动态生成(如反射、动态代理、ASM 字节码)

类加载器(ClassLoader)机制:

  • 启动类加载器(BootstrapClassLoader) :加载 JDK 核心类库(rt.jar / java.base)。

  • 扩展类加载器(ExtClassLoader) :加载 jre/lib/ext 目录下的类。

  • 应用类加载器(AppClassLoader) :加载 classpath 下的应用类。

  • 自定义类加载器:由开发者自定义加载路径或加密逻辑。

双亲委派模型:

加载请求会先向上委派,父加载器若无法加载,才由子加载器尝试,防止核心类被篡改。

此时开始涉及元空间(Metaspace)!

类加载器加载类的字节码文件的过程中,将这个类的相关信息放入元空间

  • 类的元数据(字段表、方法表、常量池等)被加载到 元空间(方法区实现)

  • 但类的静态变量等数据还未初始化。(在准备阶段赋值)

② 连接(Linking)

连接阶段将类加载后的字节码转为可执行的 JVM 运行时结构。

(1)验证(Verification)(先保证安全)

确保字节码合法、安全、不破坏 JVM。

  • 格式验证:魔数、版本号是否正确;

  • 元数据验证:父类存在吗?接口合法吗?

  • 字节码验证:方法体的控制流、操作数栈是否合法;

  • 符号引用验证:常量池中的引用类型正确。

如果验证失败,会抛出 VerifyError


(2)准备(Preparation)

为类的 静态变量分配内存并设为默认值(不执行赋值语句,但是final修饰的变量会在这里就进行赋值,特例:final x = new object() 这样的语句执行情况不会因为final修饰而提前赋值,因为要new 创建一个对象,所以不能现在赋值而是出现在类的构造方法中)

这些静态变量存放在 Java 堆(Heap)元空间的静态区域中 (取决于实现),

但变量的"定义信息"仍在元空间。

(3)解析(Resolution)

把常量池中的 符号引用 转换为 直接引用(真正与虚拟机内存关联起来,作为符号引用时,读取的时候并不知道是位于内存的哪个位置,但是解析之后就能够通过引用真正找到他在内存中的位置)

③ 初始化(Initialization)

真正执行类的 <clinit>() 方法(类初始化块 + 静态变量初始化)。

前面准备的时候为静态变量赋默认值,但是现在就是真正执行java代码了:

复制代码
public static int a = 10;
static {
    a = 20;
}

这个阶段执行的就是:

  • 静态变量赋值语句;

  • static {} 静态代码块;

  • 初始化顺序:父类先于子类。

只有初始化阶段才真正执行 Java 代码(前面阶段都不执行代码)。

那么类的初始化会在什么时候发生呢?

概括得说,类初始化是【懒情的】

  • main方法所在的类,总会被首先初始化
  • 首次访问这个类的静态变量或静态方法时(初始化之前的阶段只对静态变量赋默认值,如果要访问,那么就应该赋真正值,所以应该快速完成初始化(初始化的时候真正执行代码赋值))
  • 子类初始化,如果父类还没初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new会导致初始化

不会触发初始化的情况:

  • 访问类的staticfinal静态常量(基本类型和字符串)不会触发初始化(因为他们在链接的准备阶段就赋值好了)
  • 类对象.class不会触发初始化(因为加载阶段就已经生成好miror.class了)
  • 创建该类的数组不会触发初始化
  • 类加载器的 loadClass方法(classLoader发生在类加载阶段)
  • Class.forName的参数2(initialize)为 false时

这里有一个小细节:对于下面三种情况,真正用到了三个final修饰的值,会不会导致类执行初始化?

①不会,因为准备阶段直接赋值了;

②会不会? 不会,因为虽然final修饰引用类型,但是用的是字面量,所以"hello"存放在串池中,没有实际意义上进行new一个对象,所以不会导致类初始化;

③会,因为Integer是包装类,引用类型

④ 使用(Using)

类初始化完成后,就可以:

  • 创建实例;

  • 调用静态方法;

  • 访问静态变量;

  • 进行反射操作;

  • 被其他类继承。

⑤ 卸载(Unloading)

当:

  • 该类对应的 ClassLoader 被 GC 回收;

  • 且没有该类的实例对象存在;

  • 且没有该类的 Class 对象引用;

则该类的元数据会从 元空间(Metaspace) 中释放。

最后,类的相关信息(类结构、方法表、常量池等)都被存储在元空间中,但是类的对象存储在java堆中。

细节如下:

  • 加载阶段 将类的元数据加载进 元空间(Metaspace)

  • 验证-准备-解析;

  • 初始化阶段 才真正执行静态代码;

  • 类的元信息(字段表、方法表等)存放在 Metaspace

  • 之后就可以使用这个类了,创建类的实例,通过实例调用方法,但是要知道类对象在堆中;

  • 类的实例对象存放在 Heap

  • 类被卸载时,对应的 元空间内存 也会被释放。

相关推荐
大G的笔记本2 小时前
Java JVM 篇常见面试题
java·开发语言·jvm
ZHE|张恒3 小时前
深入理解 Java 双亲委派机制:JVM 类加载体系全解析
java·开发语言·jvm
她说彩礼65万11 小时前
C# AutoResetEvent和ManualResetEvent
java·jvm·c#
Bug退退退12318 小时前
JVM 内存结构
jvm
那我掉的头发算什么1 天前
【javaEE】多线程--认识线程、多线程
java·jvm·redis·性能优化·java-ee·intellij-idea
堕落年代2 天前
JVM新生代转老年代机制详解
jvm
235162 天前
【JVM】Java为啥能跨平台?JDK/JRE/JVM的关系?
java·开发语言·jvm·spring boot·后端·spring·职场和发展
AAA卷不动了2 天前
JVM(二)------ 类加载、初始化与单例模式的联系
java·jvm·单例模式
程序员卷卷狗2 天前
JVM 内存结构与 GC 调优全景图
java·开发语言·jvm