类加载
加载、连接、初始化、使用、卸载 。
连接又分为 验证 、准备 和解析。
类的加载阶又相当于类的生命周期。
找到需要 加载的类 并把 类的信息 加载到 jvm的方法区 ,然后在 堆区 中 实例化 一个 java.lang.Class 对象,作为方法区中这个类的信息的入口。
类加载 其实包括加载、连接、初始化三个阶段。
类加载强调一个jvm能够直接使用所需的类,所以类必须完成初始化。
狭义的类加载
1. Loading 阶段在做什么?
加载(Loading)阶段 :JVM 通过各种途径获取类的二进制字节流 ,并由 ClassLoader 将其"定义"为 JVM 可识别的类,最终在 JVM 内部生成并返回对应的 java.lang.Class 对象。
更具体一点:
-
ClassLoader根据**类的全限定名(Fully Qualified Name)**定位并获取该类的二进制字节流(来源可能是
.class、jar、网络、动态生成等) -
然后将字节流交给 JVM 去执行"defineClass"这一类的定义过程
-
JVM 完成类的内部结构创建后,返回对应的
java.lang.Class实例
2. 类的字节流从哪里来?
类的加载方式非常灵活,常见来源包括:
-
从本地
.class文件加载(常用)根据类的全路径名找到对应的
.class文件,读取文件内容得到字节流。 -
从
jar/war等归档包中加载(常用)Java 项目打包后,大部分类都位于
jar包中,类加载器会从包内读取字节流。 -
从网络加载(历史上常见)
例如早期流行的 Applet,可以从远程网络获取字节流并加载。
-
运行时动态生成(常见于框架/设计模式)
按规则即时生成字节码,例如:
-
动态代理(JDK Proxy / CGLIB)
-
字节码增强(ASM / ByteBuddy 等)
-
-
从非
.class的其他格式转换得到(本质相同)例如某些自定义格式或加密/压缩的类数据,最终仍会转换为 JVM 可识别的字节流再加载。
本质:最终目标仍然是"拿到合法的 class 字节码"。
3. 什么时候会触发加载?
不同 JVM 对"加载时机"的实现可能存在差异,具体取决于虚拟机实现。
连接
验证 - 准备 - 解析
-
验证(Verification) :
进行类的合法性校验 。
对比如 字节码格式 、变量/方法的合法性 、数据类型的有效性 、继承与实现的规范性 等进行检查。
确保被加载的类能够正常被 JVM 运行。
-
准备(Preparation) :
分内存、赋初值。-
为类的 静态变量(static) 分配内存,并设为 JVM 默认初始值;
-
对于 非静态变量(实例变量) ,则不会为它们分配内存。
注意 :这里的初始值是 JVM 默认赋值,而不是程序里写的赋值。
规则:
-
基本类型 默认值为
0(int、long、short、char、byte、boolean、float、double) -
引用类型 默认值为
null -
常量的默认值为程序中设定的值,例如:
javafinal static int a = 100;则准备阶段
a的值就是100。 -
-
解析(Resolution) :
把常量池的 符号引用 转换为 直接引用。
类的初始化
初始化是类加载过程的最后一个阶段 (Loading → Linking → Initialization)。
在这个阶段,JVM 会执行类的类初始化方法 <clinit>(),也就是把:
-
静态变量的显式赋值 (
static int a = 1;) -
静态代码块 (
static { ... })
按源码顺序合并到 <clinit>() 里执行。
注意:在 Linking 的 Preparation 阶段,静态变量只是被分配内存并设置默认值(0/null/false),真正的显式赋值与静态块逻辑是在初始化阶段执行。
什么时候会触发「初始化」?(主动使用 / Active Use)
JVM 对"如何加载"没有完全硬性规范,但对"何时初始化"有明确规则:
只有当类型被"主动使用(direct/active use)"时,才会触发初始化。常见触发点:
-
new创建该类实例 -
读/写该类的静态字段 (但编译期常量例外,见下)
-
调用该类的静态方法
-
反射调用上述 1/2/3(如
Class.forName、反射访问静态成员等) -
初始化一个子类时,会先初始化其父类(这是"父类优先"的递归链)
-
作为程序入口执行,首次调用包含
main的那个类时会初始化该类
一个关键例外:编译期常量不会触发初始化
如果是 static final 且是编译期可确定的常量 ,使用它可能被编译器直接内联 ,从而不触发类初始化:
class A { static final int X = 1; // 编译期常量 static { System.out.println("A init"); } } System.out.println(A.X); // 很可能不打印 "A init"
初始化时具体"执行什么"?
初始化阶段只做一件事:执行 <clinit>()。因此:
-
会执行:静态代码块、静态变量的显式赋值
-
不会执行:实例代码块、实例字段赋值、构造器 (这些属于"对象初始化",由
new触发并在对象创建时执行)
你原句里"非静态与非静态的静态语法均不执行"建议改成更标准的这句:
类初始化只执行静态初始化逻辑;所有实例初始化逻辑都不会在类初始化阶段执行。
初始化顺序(父类优先)
-
如果该类有父类:先初始化父类,再初始化子类
-
同一个类内部:按源码顺序执行静态赋值/静态块(它们共同组成
<clinit>())
你原来的"弗雷德初始化"应当是笔误,标准表述是:
初始化子类会触发父类初始化。
Class文件等概念
Class文件(.class)
- (读取成)二进制字节流/字节码(byte[])
- (ClassLoader defineClass) -> Class对象(java.lang.Class 实例)
- (JVM 用它)-》才能new、反射、调用方法等。
1. Class文件 (.class)
- 编译器(javac)把
.java编译后的产物,存在磁盘、jar包中。 - 包含 常量池、字段表、方法表、属性、以及方法的字节码指令等结构。
"存储介质上的标准化类表示"
2. 二进制字节流(binary bytes/ byte stream)
- 把class文件都出来后得到的原始二进制数据
- 常见形态:
byte[]、InputStream
还没解析、只是字节序列
3. 字节码(byteCode)
- class文件里方法体的那段JVM指令序列(比如aload_0, invokevirtual)
- 区分:
- 二进制字节流:外层raw bytes(文件、网络读出来的)
- 字节码:里面方法Code属性中的JVM指令
JVM能执行、解释、编译的指令级表示
4. ClassLoader(类加载器)
- 负责把 **"类名 -》字节数据"**变成JVM里的类定义的组件。
- 关键动作 :
loadClass(name):按规则找到并加载(通常还会走双亲委派)findClass(name):具体去哪找字节(classpath/jar/网络/自定义)defineClass(bytes):把字节数组交给 JVM 定义成类
- 一句话 :"搬运 + 定义类的工人/管道"。
5.Class对象(Class instance)
- 代码里能拿到的
java.lang.Class<?>实例,比如String.class的那个对象。 - 表示什么 :某个类在 JVM 中的运行时类型信息入口(反射、获取方法字段、创建实例等)。
- 同一个类的唯一性 :在一个 ClassLoader 命名空间里,某个类通常对应 唯一的 Class对象;不同 ClassLoader 加载的"同名类"可以是不同的 Class对象(也就是"类隔离")。
- 一句话 :"运行时类型信息的句柄/门把手"。