文章可能会比较长,主要解析的非常详解,或涉及一些底层知识,供面试高阶难度用。可以根据自己实际理解情况合理取舍阅读
Java 类加载过程是 Java 虚拟机(JVM)将 .class 文件加载到内存并将其转化为 JVM 可以执行的类对象的过程。类加载的过程涉及到 类加载器(ClassLoader)、类的加载、验证、准备、解析和初始化 等多个阶段。下面是 Java 类加载过程的详细说明:
加载是指从存储介质(如硬盘)中读取 .class 文件,并将其转化为 JVM 中可以使用的类元数据。类加载的第一步是由 类加载器(ClassLoader) 完成的。
步骤:
JVM 找到类的二进制数据(.class 文件),并将它加载到内存中。
将 .class 文件的数据解析成 JVM 可以理解的二进制格式,并生成相应的 Class 对象。
类加载器:负责加载类的组件。Java 中的类加载器主要分为以下几类:
Bootstrap ClassLoader:负责加载 JDK 核心库,如 rt.jar 中的类。
Extension ClassLoader:负责加载 JDK 扩展库(如 ext/ 目录中的类)。
Application ClassLoader:负责加载应用程序的类路径(classpath)中的类,通常是应用的 .class 文件。
验证阶段主要是对加载的 .class 文件进行格式检查,确保类文件是合法的,并且不会破坏 Java 虚拟机的安全性。
步骤:
文件格式验证:检查 .class 文件的结构是否符合规范。
元数据验证:检查常量池、类结构是否符合 JVM 的要求。
字节码验证:验证类文件中的字节码是否符合 JVM 的字节码规范。
通过验证后,JVM 可以确保类文件不会引起安全问题,如篡改字节码导致异常的行为。
准备阶段是为类的静态变量分配内存并设置初始值的过程。类的静态变量会被分配到 方法区(在 Java 8 之后是 Metaspace)中,并为它们设置默认值。
类的静态变量会被分配内存,所有静态变量都会初始化为默认值。例如,int 类型的静态变量会被初始化为 0,boolean 类型为 false,对象类型为 null。
注意:此时静态变量并不会执行类中的初始化代码(如构造函数或静态代码块)。静态变量的初始化和类的初始化阶段是两个不同的概念。
解析阶段是将符号引用转换为直接引用的过程。符号引用是通过类的全限定名表示的,例如:java.lang.String。解析过程是将这些符号引用解析为 JVM 能够使用的实际内存地址或方法引用。
步骤:
类的解析:将类的符号引用转换为类的直接引用。
字段的解析:将类中的字段(成员变量)符号引用转换为实际内存地址。
方法的解析:将方法的符号引用转换为方法的直接引用。
解析过程中,JVM 需要加载与符号引用相关的类、字段或方法,确保它们能够正确地链接到内存中的相应实体。
初始化阶段是类加载过程中的最后一步。此时,JVM 会初始化类的静态变量,并执行静态代码块。这是类首次被使用时才会发生的过程。
步骤:
静态变量赋值:如果静态变量有显式赋值,则会在此阶段完成赋值操作。
静态代码块执行:静态代码块(static {})会在类被加载时执行。这个代码块只会执行一次,即使类被多次加载。
注意:类的初始化是延迟的,只有在类的 第一次使用时 才会初始化。例如,当调用该类的静态方法、访问静态变量、或者通过反射等方式触发时,类才会被初始化。
类加载通常发生在以下几种情况:
显式的类加载:如通过 Class.forName()、ClassLoader.loadClass() 等方法来加载类。
类的首次引用:当类中的静态方法、静态字段、或类的构造函数被访问时,类会被加载并初始化。
反射:通过反射机制,访问类的构造函数、方法、字段等时会触发类的加载。
JVM 启动时加载的类:如 main() 方法所在的类,在 JVM 启动时会被加载。
父类委托模型:首先尝试委托给 Bootstrap ClassLoader,如果没有找到,再委托给 Extension ClassLoader,然后是 Application ClassLoader。如果它们都无法加载该类,当前类加载器才会尝试自己加载。
委托机制的优势:通过这种委托机制,Java 确保了核心类库的安全性和一致性,例如 java.lang.* 中的类始终由 Bootstrap ClassLoader 加载,避免了加载用户自定义类时的潜在冲突。
加载非标准路径下的类。
实现特殊的类加载策略,例如从数据库或网络加载类文件。
自定义类加载器通常会重写 findClass() 方法,并在其中实现类的查找和加载逻辑。
类的多次加载:同一个类可以由不同的类加载器加载多次。如果使用不同的类加载器加载同一个类,它们会被认为是不同的类,可能会导致类冲突或 ClassCastException。
类的双重加载:通过多个类加载器加载同一个类,可能会导致类的状态不同步或资源的重复初始化。
JVM 会根据类的类加载器和类的全限定名判断类是否已经加载过。每个类加载器有自己的 类加载缓存(缓存已加载的类)。如果该类已经被加载并且在缓存中存在,JVM 会直接返回已加载的类,而不会重复加载。
类的初始化阶段包括:
静态变量赋值:静态变量被赋予显式的初始化值。
静态代码块执行:执行类中的 static {} 静态代码块,这个代码块只会在类加载和初始化时执行一次。
如果类有继承关系,父类的静态变量和静态代码块会先被初始化。
类的加载和初始化通常在以下情况下发生:
直接使用类的静态方法:调用类的静态方法时,类会被加载和初始化。
访问类的静态变量:访问静态字段时,类会被加载和初始化。
通过反射访问类:使用反射机制获取类的 Class 对象并访问其构造函数或方法时,类会被加载和初始化。
类的子类被创建:当一个类的子类被创建时,父类会被初始化。
使用 Class.forName() 方法:显式地通过 Class.forName() 方法加载类时,类会被加载并初始化。
可以通过继承 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]; // 示例返回空字节数组
}
}
双重加载指的是同一个类被多个类加载器加载。当使用不同的类加载器加载同一个类时,JVM 会认为它们是不同的类,即使它们是相同的 .class 文件。这可能会导致 ClassCastException 或类状态不同步的情况。
类加载器本身会持有对类的引用。如果类加载器不被正确地回收,类加载的类也不会被回收,从而导致内存泄漏。为了避免类加载的内存泄漏,应该:
显式卸载类加载器:在应用程序不再使用某些类时,可以通过 ClassLoader 的 null 引用或自定义类加载器的销毁方法来卸载类加载器。
避免过多的动态类加载:尽量避免在应用程序中频繁地创建新的类加载器,特别是在长生命周期的应用中。
可以通过 ClassLoader 的 loadClass() 方法或者反射 API 来判断类是否已经被加载。如果 ClassNotFoundException 被抛出,则说明类没有被加载。如果该类已经被加载,则可以通过 Class.forName() 等方法直接返回已加载的类对象。
类的加载指的是将类的字节码从存储介质加载到内存,并创建 Class 对象,而类的初始化是指执行类的静态变量初始化和静态代码块。类加载过程中的初始化是延迟加载的,只有在类第一次被使用时才会执行初始化。类的加载和初始化是两个不同的阶段:
加载阶段:类字节码被加载到 JVM 内存。
初始化阶段:类的静态成员被赋值,执行静态代码块。
类的静态变量初始化是在类被加载后,为所有的静态变量分配内存,并将它们初始化为默认值(如 0、false 或 null)。随后,JVM 会根据类中的静态变量的定义给它们赋予实际的值。如果类有静态代码块,则静态代码块会在静态变量初始化后执行。