类加载过程整理

Java类加载与初始化

各种调用初始化顺序

我们用一个小demo来验证一下各种调用的初始化顺序:

demo1
public class Test {

    public static void main(String[] args) {
        new C1();
    }

}

class C1 {

    public static C2 c2 = new C2();

    public C3 c3 = new C3();

    static {
        System.out.println("C1 static block execute");
    }

    public C1() {
        System.out.println("C1 constructor execute");
    }

    {
        System.out.println("C1 free block execute");
    }
}

class C2 {

    static {
        System.out.println("C2 static block execute");
    }

    {
        System.out.println("C2 free block execute");
    }

    public C2() {
        System.out.println("C2 constructor execute");
    }
}

class C3 {

    static {
        System.out.println("C3 static block execute");
    }

    {
        System.out.println("C3 free block execute");
    }

    public C3() {
        System.out.println("C3 constructor execute");
    }
}
结果输出1
C1 static block execute
C2 static block execute
C2 free block execute
C2 constructor execute
C3 static block execute
C3 free block execute
C3 constructor execute
C1 free block execute
C1 constructor execute
demo2

我们交换类C1中C2类型的静态变量,放到静态块下面:

class C1 {

    public C3 c3 = new C3();

    static {
        System.out.println("C1 static block execute");
    }

    public static C2 c2 = new C2();

    public C1() {
        System.out.println("C1 constructor execute");
    }

    {
        System.out.println("C1 free block execute");
    }
}
结果输出2
C1 static block execute
C2 static block execute
C2 free block execute
C2 constructor execute
C3 static block execute
C3 free block execute
C3 constructor execute
C1 free block execute
C1 constructor execute
demo3

接下来我们交换类C1中C3类型的实例属性,放到整个类的最下面

class C1 {

    static {
        System.out.println("C1 static block execute");
    }

    public static C2 c2 = new C2();

    public C1() {
        System.out.println("C1 constructor execute");
    }

    {
        System.out.println("C1 free block execute");
    }

    public C3 c3 = new C3();

}
结果输出3
C1 static block execute
C2 static block execute
C2 free block execute
C2 constructor execute
C1 free block execute
C3 static block execute
C3 free block execute
C3 constructor execute
C1 constructor execute
demo4

在C1中添加静态方法,main方法中的调用换成调用C1的静态方法:

public class Test {

    public static void main(String[] args) {
        C1.staticInvoke();
    }

}

class C1 {

    static {
        System.out.println("C1 static block execute");
    }

    public static C2 c2 = new C2();

    public C1() {
        System.out.println("C1 constructor execute");
    }

    {
        System.out.println("C1 free block execute");
    }

    public C3 c3 = new C3();

    public static void staticInvoke() {
        System.out.println("C1 static method invoke");
    }

}
结果输出4
C1 static block execute
C2 static block execute
C2 free block execute
C2 constructor execute
C1 static method invoke
结论

通过以上几个例子我们可以总结出以下结论(未完待续):

  • 静态属性(包括静态变量、静态块)的初始化优先于实例属性
  • 游离块的初始化优先于构造方法
  • 同为静态属性,代码中定义的顺序决定了初始化顺序
  • 同为实例属性,代码中定义的顺序决定了初始化顺序
  • 只调用类的静态方法,不会初始化类的实例属性

类加载过程

下图为类从被加载到到虚拟机内存开始,到卸载出内存为止,生命周期的七个阶段:
在这里插入图片描述
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定,它在某些情况下可以再初始化阶段之后再“开始”,这是为了支持java语言的运行时绑定,这里强调按部就班地“开始”,而不是按部就班地“进行”或者“完成”,是因为这些阶段通常都是互相交叉混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。

什么情况下需要开始类加载过程的第一个阶段:“加载”。java虚拟机规范中并没强行约束,这点可以交给虚拟机的的具体实现自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5种必须对类进行“初始化”(则加载、验证、准备自然需要在此之前开始):

  1. 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令最常见的java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上5种触发类的初始化的场景称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义类应用类,不会触发此类的初始化。
  3. 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

接下来我们来看一下类加载的每个过程都做了那些事:

加载:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

验证:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备:

  • 为类变量分配内存并设置类的变量的初始值

这里说的初始值也就是变量的默认值(零值),如int型的默认值是0,boolean型的默认值是false,引用类型的默认值为null等。

解析:

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

初始化:

  • 执行类构造器<clinit>()方法的过程

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

<clinit>()方法

<clinit>()方法是在类加载的初始化阶段执行的类构造器,我们要把<clinit>()方法和构造函数(实例的构造器<init>()方法)区分开来,<clinit>()方法并不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。

<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

接口中不能使用静态语句块,但仍然有变量的初始化操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

<think>嗯,用户想了解Java的类加载过程。我记得类加载是Java虚拟机加载的重要步骤,但具体细节可能需要回忆一下。首先,类加载过程应该有几个阶段吧?比如加载、验证、准备、解析、初始化这些。但我不太确定每个阶段的具体内容,需要仔细理清楚。 首先,加载阶段,是不是把.class文件加载到内存里,生成Class对象?这时候类加载器起作用,可能有不同的类加载器,比如启动类加载器、扩展类加载器、应用类加载器,还有自定义的。用户可能需要知道双亲委派机制,但问题可能集中在过程本身,而不是加载器结构。不过可能需要简要提及。 接下来是验证,这部分是确保字节码符合规范,防止恶意代码。比如文件格式验证,元数据验证,字节码验证,符号引用验证。这些步骤可能属于验证阶段,但具体顺序是否正确呢? 然后是准备阶段,这里为变量分配内存并设置初始值。比如static变量会被赋予默认值,而不是代码中的赋值。比如int初始化为0,而不是用户定义的123。这时候需要注意初始化和准备阶段的区别,初始化阶段才会执行代码中的赋值。 解析阶段是把符号引用转为直接引用。符号引用可能指的是、方法、字段的名称,而直接引用是具体的内存地址。这个过程可能在初始化之前或之后进行,取决于虚拟机的实现,比如有些解析可能延迟到使用的时候。 初始化阶段是执行构造器<clinit>方法,包括静态变量赋值和静态代码块。这里要确保多线程环境下的正确执行,比如加锁。这时候才会把static变量设置为用户指定的值。需要强调初始化类加载的最后一步,这时候才完全可用。 可能用户还关心触发初始化的条件,比如new实例、访问静态变量或方法、反射调用等。这些情况会触发初始化,但要注意被动引用不会触发,比如子引用父的静态字段,不会导致子初始化。 另外,类加载过程中的各个阶段顺序是否严格?比如加载、验证、准备、初始化顺序是固定的,但解析可能在初始化之后,这是否正确?比如在Java虚拟机规范中,解析阶段可以在初始化之前或之后,具体视情况而定。 需要检查是否有遗漏的阶段,或者是否有错误的地方。比如卸载阶段属于生命周期的结束,但通常类加载过程指的是加载到初始化,所以用户的问题可能不涉及卸载。 总结一下,回答应该分阶段介绍加载、验证、准备、解析、初始化,每个阶段的作用,可能涉及类加载器的角色,双亲委派机制,以及各阶段的例子,比如准备阶段的默认值,初始化的赋值。同时需要提到触发初始化的条件和被动引用的情况,避免混淆。确保术语正确,比如<clinit>和<init>的区别,前者是构造器,后者是实例构造器。 可能用户是Java初学者,需要简明扼要的解释,所以结构要清晰,步骤明确,用例子辅助说明。比如准备阶段的例子,说明初始值和用户赋值的区别。或者解析阶段的符号引用转为直接引用的例子,比如方法调用时的解析。 另外,可能需要注意类加载过程的异常处理,比如ClassNotFoundException,NoClassDefFoundError,这些异常出现的位置和原因,但可能不属于过程本身,所以可以简单提及或者略过。 最后,确认是否要提到自定义类加载器的可能性,但问题主要是关于过程,所以可能不需要深入,但可以提到类加载器的作用。比如双亲委派模型如何影响类加载过程,确保安全性,避免重复加载等。 总之,整理出各个阶段,每个阶段的功能,顺序,示例,以及相关的注意事项,如触发条件、被动引用等。确保回答准确且易于理解,避免过于技术性的术语,但需要正确使用术语。</think>Java 的类加载过程是 JVM 将 `.class` 文件加载到内存,并转换为 `Class` 对象的过程。这一过程分为以下阶段,按严格顺序执行: --- ### 1. **加载(Loading)** - **作用**:查找并加载的二进制字节流(如 `.class` 文件)。 - **关键点**: - 通过全限定名获取二进制数据(可从文件、网络、JAR 包等来源)。 - 生成对应的 `java.lang.Class` 对象,作为方法区的访问入口。 - **类加载器**负责此过程(如启动类加载器、应用类加载器等)。 --- ### 2. **验证(Verification)** - **作用**:确保字节码合法且符合 JVM 规范,防止安全漏洞。 - **步骤**: 1. **文件格式验证**:检查魔数、版本号等是否符合规范。 2. **元数据验证**:校验语义(如是否继承 final )。 3. **字节码验证**:确保方法逻辑合法(如型转换正确)。 4. **符号引用验证**:确认引用的、方法、字段是否存在。 --- ### 3. **准备(Preparation)** - **作用**:为变量(static 变量)分配内存并设置初始值。 - **示例**: ```java public static int value = 123; ``` - 准备阶段会为 `value` 分配内存并赋默认值 `0`,而非 `123`。 - **例外**:若字段被 `final` 修饰(如 `static final`),则直接赋代码中的值。 --- ### 4. **解析(Resolution)** - **作用**:将常量池中的**符号引用**替换为**直接引用**(内存地址)。 - **符号引用**:以文本形式描述引用的目标(如 `java/lang/Object`)。 - **直接引用**:指向目标在内存中的指针或偏移量。 - **延迟解析**:某些解析可能发生在代码首次执行时(如方法调用)。 --- ### 5. **初始化(Initialization)** - **作用**:执行构造器 `<clinit>()` 方法,完成静态变量赋值和静态代码块。 - **关键点**: - `<clinit>` 由编译器自动生成,合并所有 `static` 变量赋值和静态代码块。 - JVM 保证初始化过程在多线程下被正确加锁(线程安全)。 - **触发条件**: - `new` 实例化对象、访问静态变量/方法。 - 反射调用(如 `Class.forName()`)。 - 主(包含 `main()` 的)会自动初始化。 --- ### 6. **使用与卸载(Usage & Unloading)** - **使用**:完成加载后,可通过 `Class` 对象创建实例、调用方法等。 - **卸载**:当不再被引用且满足条件时,JVM 会回收其内存(由垃圾回收机制管理)。 --- ### 🌰 示例与注意事项 - **被动引用**:某些情况不会触发初始化: ```java class Parent { static int value = 1; } class Child extends Parent {} // 访问 Child.value 只会初始化 Parent,不会初始化 Child ``` - **类加载器双亲委派**:加载时优先委派父加载器,确保核心库的安全性。 --- ### 总结流程 ``` 加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载 ``` 每个阶段紧密衔接,确保能被正确加载并安全执行。理解这一过程对解决 `ClassNotFoundException`、`NoClassDefFoundError` 等问题至关重要。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值