Java类加载过程详解

你好,我是吴计可师
点击下方👇关注公众号,带你一起复习后端技术,看看面试考点,补充积累技术知识,每天都为面试准备积累

文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读


Java 类加载过程是 Java 虚拟机(JVM)将 .class 文件加载到内存并将其转化为 JVM 可以执行的类对象的过程。类加载的过程涉及到 类加载器(ClassLoader)、类的加载、验证、准备、解析和初始化 等多个阶段。下面是 Java 类加载过程的详细说明:

01
类加载的基本流程


类的加载过程分为 加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization) 五个阶段。


1.1 加载(Loading)

加载是指从存储介质(如硬盘)中读取 .class 文件,并将其转化为 JVM 中可以使用的类元数据。类加载的第一步是由 类加载器(ClassLoader) 完成的。

  • 步骤:

    • JVM 找到类的二进制数据(.class 文件),并将它加载到内存中。

    • 将 .class 文件的数据解析成 JVM 可以理解的二进制格式,并生成相应的 Class 对象。

  • 类加载器:负责加载类的组件。Java 中的类加载器主要分为以下几类:

    • Bootstrap ClassLoader:负责加载 JDK 核心库,如 rt.jar 中的类。

    • Extension ClassLoader:负责加载 JDK 扩展库(如 ext/ 目录中的类)。

    • Application ClassLoader:负责加载应用程序的类路径(classpath)中的类,通常是应用的 .class 文件。

1.2 验证(Verification)

验证阶段主要是对加载的 .class 文件进行格式检查,确保类文件是合法的,并且不会破坏 Java 虚拟机的安全性。

  • 步骤:

    • 文件格式验证:检查 .class 文件的结构是否符合规范。

    • 元数据验证:检查常量池、类结构是否符合 JVM 的要求。

    • 字节码验证:验证类文件中的字节码是否符合 JVM 的字节码规范。

通过验证后,JVM 可以确保类文件不会引起安全问题,如篡改字节码导致异常的行为。

1.3 准备(Preparation)

准备阶段是为类的静态变量分配内存并设置初始值的过程。类的静态变量会被分配到 方法区(在 Java 8 之后是 Metaspace)中,并为它们设置默认值。

  • 类的静态变量会被分配内存,所有静态变量都会初始化为默认值。例如,int 类型的静态变量会被初始化为 0,boolean 类型为 false,对象类型为 null。

  • 注意:此时静态变量并不会执行类中的初始化代码(如构造函数或静态代码块)。静态变量的初始化和类的初始化阶段是两个不同的概念。

1.4 解析(Resolution)

解析阶段是将符号引用转换为直接引用的过程。符号引用是通过类的全限定名表示的,例如:java.lang.String。解析过程是将这些符号引用解析为 JVM 能够使用的实际内存地址或方法引用。

  • 步骤:

    • 类的解析:将类的符号引用转换为类的直接引用。

    • 字段的解析:将类中的字段(成员变量)符号引用转换为实际内存地址。

    • 方法的解析:将方法的符号引用转换为方法的直接引用。

解析过程中,JVM 需要加载与符号引用相关的类、字段或方法,确保它们能够正确地链接到内存中的相应实体。

1.5 初始化(Initialization)

初始化阶段是类加载过程中的最后一步。此时,JVM 会初始化类的静态变量,并执行静态代码块。这是类首次被使用时才会发生的过程。

  • 步骤:

    • 静态变量赋值:如果静态变量有显式赋值,则会在此阶段完成赋值操作。

    • 静态代码块执行:静态代码块(static {})会在类被加载时执行。这个代码块只会执行一次,即使类被多次加载。

    • 注意:类的初始化是延迟的,只有在类的 第一次使用时 才会初始化。例如,当调用该类的静态方法、访问静态变量、或者通过反射等方式触发时,类才会被初始化。

02
类加载的触发条件


类加载通常发生在以下几种情况:

  • 显式的类加载:如通过 Class.forName()、ClassLoader.loadClass() 等方法来加载类。

  • 类的首次引用:当类中的静态方法、静态字段、或类的构造函数被访问时,类会被加载并初始化。

  • 反射:通过反射机制,访问类的构造函数、方法、字段等时会触发类的加载。

  • JVM 启动时加载的类:如 main() 方法所在的类,在 JVM 启动时会被加载。


03
类加载器的委托机制


Java 的类加载器使用了 委托模型,即每个类加载器在加载类时,会先委托父类加载器去加载,只有父类加载器无法加载时,子类加载器才会加载。

  • 父类委托模型:首先尝试委托给 Bootstrap ClassLoader,如果没有找到,再委托给 Extension ClassLoader,然后是 Application ClassLoader。如果它们都无法加载该类,当前类加载器才会尝试自己加载。

  • 委托机制的优势:通过这种委托机制,Java 确保了核心类库的安全性和一致性,例如 java.lang.* 中的类始终由 Bootstrap ClassLoader 加载,避免了加载用户自定义类时的潜在冲突。


04
自定义类加载器


Java 允许开发者自定义类加载器,通过继承 ClassLoader 类来创建自定义加载器。自定义类加载器可以在特定场景下用于:

  • 加载非标准路径下的类。

  • 实现特殊的类加载策略,例如从数据库或网络加载类文件。

自定义类加载器通常会重写 findClass() 方法,并在其中实现类的查找和加载逻辑。


05
类加载过程中的常见问题


类的多次加载:同一个类可以由不同的类加载器加载多次。如果使用不同的类加载器加载同一个类,它们会被认为是不同的类,可能会导致类冲突或 ClassCastException。

类的双重加载:通过多个类加载器加载同一个类,可能会导致类的状态不同步或资源的重复初始化。


06
直击面试


6.1 JVM 在加载类时,如何判断是否已经加载过该类?

JVM 会根据类的类加载器和类的全限定名判断类是否已经加载过。每个类加载器有自己的 类加载缓存(缓存已加载的类)。如果该类已经被加载并且在缓存中存在,JVM 会直接返回已加载的类,而不会重复加载。


6.2 类的初始化阶段发生了什么?

类的初始化阶段包括:

  • 静态变量赋值:静态变量被赋予显式的初始化值。

  • 静态代码块执行:执行类中的 static {} 静态代码块,这个代码块只会在类加载和初始化时执行一次。

  • 如果类有继承关系,父类的静态变量和静态代码块会先被初始化。


6.3 什么时候会触发类的加载和初始化?

类的加载和初始化通常在以下情况下发生:

  • 直接使用类的静态方法:调用类的静态方法时,类会被加载和初始化。

  • 访问类的静态变量:访问静态字段时,类会被加载和初始化。

  • 通过反射访问类:使用反射机制获取类的 Class 对象并访问其构造函数或方法时,类会被加载和初始化。

  • 类的子类被创建:当一个类的子类被创建时,父类会被初始化。

  • 使用 Class.forName() 方法:显式地通过 Class.forName() 方法加载类时,类会被加载并初始化。


6.4 如何实现自定义类加载器?

可以通过继承 ClassLoader 类并重写 findClass() 方法来实现自定义类加载器。在 findClass() 方法中,您可以通过自定义逻辑(如从网络或数据库加载类文件)来加载类字节码。示例如下:

public class MyClassLoader extends ClassLoader {    @Override    public Class<?> findClass(String name) throws ClassNotFoundException {        byte[] data = loadClassData(name); // 自定义加载逻辑        return defineClass(name, data, 0, data.length); // 将字节码定义为类    }    private byte[] loadClassData(String name) {        // 实现加载类文件的逻辑        return new byte[0]; // 示例返回空字节数组    }}


6.5 什么是类加载的双重加载(Double Loading)?

双重加载指的是同一个类被多个类加载器加载。当使用不同的类加载器加载同一个类时,JVM 会认为它们是不同的类,即使它们是相同的 .class 文件。这可能会导致 ClassCastException 或类状态不同步的情况。


6.6 如何防止类加载过程中的内存泄漏?

类加载器本身会持有对类的引用。如果类加载器不被正确地回收,类加载的类也不会被回收,从而导致内存泄漏。为了避免类加载的内存泄漏,应该:

  • 显式卸载类加载器:在应用程序不再使用某些类时,可以通过 ClassLoader 的 null 引用或自定义类加载器的销毁方法来卸载类加载器。

  • 避免过多的动态类加载:尽量避免在应用程序中频繁地创建新的类加载器,特别是在长生命周期的应用中。

6.7 如何判断一个类是否已加载?

可以通过 ClassLoader 的 loadClass() 方法或者反射 API 来判断类是否已经被加载。如果 ClassNotFoundException 被抛出,则说明类没有被加载。如果该类已经被加载,则可以通过 Class.forName() 等方法直接返回已加载的类对象。


6.8 解释一下 Java 类加载过程中的“类的初始化”与“类的加载”有什么不同?

类的加载指的是将类的字节码从存储介质加载到内存,并创建 Class 对象,而类的初始化是指执行类的静态变量初始化和静态代码块。类加载过程中的初始化是延迟加载的,只有在类第一次被使用时才会执行初始化。类的加载和初始化是两个不同的阶段:

  • 加载阶段:类字节码被加载到 JVM 内存。

  • 初始化阶段:类的静态成员被赋值,执行静态代码块。

6.9 什么是类的 “静态变量初始化”?

类的静态变量初始化是在类被加载后,为所有的静态变量分配内存,并将它们初始化为默认值(如 0、false 或 null)。随后,JVM 会根据类中的静态变量的定义给它们赋予实际的值。如果类有静态代码块,则静态代码块会在静态变量初始化后执行。


END


扫码关注

一起积累后端知识
不积跬步,无以至千里
不积小流,无以成江海



喜欢此内容的人还喜欢

谈谈id那些事(五)——美团的 Leaf 的ID生成

谈谈id那些事(四)——雪花 ID(Snowflake ID)


谈谈id那些事(三)——阿里巴巴的 TDDL的ID生成


谈谈id那些事(二)——Redis 自增 ID


谈谈id那些事(一)——数据库的自增ID