详细介绍:JVM类加载

类加载的过程

1. 什么是类加载?

  • 简单来说,类加载是指 Java 虚拟机(JVM)将类的.class通过文件(二进制数据)读入内存,并将其转换为 JVM 能够使用的 Class 对象的过程。这个 Class 对象是 Java 反射机制的基石,也是我们在程序中利用这个类的模板。

  • 类加载并不仅仅指“加载”这一个动作,它包含了从查找字节码到类完全可用的一整个生命周期


2. 类加载的过程(类的生命周期)
类加载的过程主要分为以下三个核心阶段:加载、链接、初始化。其中链接又细分为三个子阶段。
在这里插入图片描述
阶段一:加载
“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的⼀个阶段,它和类加载Class Loading 是不同的,⼀个是加载 Loading 另⼀个是类加载 Class Loading,所以不要把⼆者搞混了。

此阶段重要做完三件事:

  • 通过类的全限定名获取其定义的二进制字节流。

    • 这不仅仅是从文件系统读取 .class 文件,还可以从 JAR、WAR 包、网络、数据库、运行时计算生成(动态代理)等多种来源获取。
  • 将该字节流所代表的静态存储结构转化为办法区的运行时数据结构。

    • 方法区(在 HotSpot JVM 8+ 中称为 Metaspace)存储了类的结构信息,如常量池、字段描述、技巧描述等。
  • 在堆内存中生成一个代表这个类的 java.lang.Class 对象。

    • 这个 Class 对象作为方法区中该类各种材料的访问入口。我们通过 MyClass.class 或 obj.getClass() 获取的就是这个对象。

注意:要靠类加载器来加载就是数组类本身不由类加载器创建,而是由 JVM 直接在内存中动态构造。但数组的元素类型(Component Type)最终还
阶段二:链接(Linking)
(1)验证(Verification)
验证连接阶段的第⼀步,这⼀阶段的⽬的是确保Class⽂件的字节 流中涵盖的信息符合《Java虚拟机规范》的全部约束要求,保证这些信 息被当作代码运⾏后不会危害虚拟机⾃⾝的安全。

  • 目的:确保被加载的类的字节流符合 JVM 规范,不会危害 JVM 的安全。

  • 主要检查:

    • 文件格式验证:魔数(CAFE BABE)、版本号等。
    • 元内容验证:否实现了抽象方法等。就是类是否有父类(除了 Object)、是否是最终类被继承、
    • 字节码验证:确保方式体中的代码逻辑正确(如类型转换安全)。
    • 符号引用验证:发生在解析阶段,确保符号引用能够被正确解析。

(2)准备(Preparation)

  • 目的:为类的静态变量(static variables) 分配内存并设置初始值。

  • 关键点:

    • 分配内存的仅包括类变量(被 static 修饰的变量),不包括实例变量。

    • 设置的是数据类型的零值,而不是代码中显式赋予的值。
      例如:public static int value = 123; 在准备阶段后,value 的初始值是 0,而不是 123。赋值为 123 的动作将在后面的初始化阶段执行。

    • 对于 static final修饰的常量(ConstantValue),如果它的类型是基础类型或 String,并且在编译期就能确定值,那么准备阶段就会直接将其初始化为指定的值。例如:public static final int value = 123; 在准备阶段后,value 的值就是 123。

(3)解析(Resolution)

  • 目的:将常量池内的符号引用(Symbolic References) 替换为直接引用(Direct References)

    • 符号引用:一组用来描述所引用目标的符号,允许是任何形式的字面量,与虚拟机内存布局无关。

    • 直接引用:可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。与虚拟机内存布局相关。

  • 解析的目标通常包括:类或接口、字段、类方法、接口技巧、技巧类型等。

阶段三:初始化(Initialization)

  • 目的:执行类的构造器< clinit >() 技巧的过程,真正开始执行类中定义的 Java 程序代码。

  • < clinit >() 途径是什么?

    • 它是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块) 中的语句合并产生的。
    • 顺序由语句在源文件中出现的顺序决定。
  • 初始化的时机(主动引用,触发初始化):

    • 遇到 new, getstatic, putstatic, invokestatic 这四条字节码指令时。
      对应代码场景:应用 new 关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。

    • 利用 java.lang.reflect 包的方法对类进行反射调用的时候。

    • 当初始化一个类时,如果其父类还没有被初始化,则得先触发其父类的初始化。

    • 虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。

  • 不会导致初始化的情况(被动引用):

    • 通过子类引用父类的静态字段,不会导致子类初始化。
    • 通过数组定义来引用类,不会触发此类的初始化。如 MyClass[] arr = new MyClass[10];
    • 引用一个类的常量(static final)不会触发初始化,因为常量在编译阶段就存入调用类的常量池了。

3. 类加载器(ClassLoader)
实际执行“加载”阶段动作的组件。就是类加载器

三层类加载器模型( parental delegation model, 双亲委派模型)
Java 保留了三种基本的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):

    • C++ 实现,是 JVM 自身的一部分。
    • 负责加载 <JAVA_HOME>/lib 目录下的核心类库(如 rt.jar, charsets.jar 等),或者被 -Xbootclasspath 参数指定的路径中的类。
    • 无法被 Java 程序直接引用。
  • 扩展类加载器(Extension ClassLoader):

    • Java 搭建,是 sun.misc.Launcher$ExtClassLoader 类。
    • 负责加载 <JAVA_HOME>/lib/ext 目录下的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
    • 开发者许可直接使用。
  • 应用程序类加载器(Application ClassLoader):

    • sun.misc.Launcher$AppClassLoader 类。就是Java 实现,
    • 也叫系统类加载器(System ClassLoader)。
    • 负责加载用户类路径(ClassPath)上所指定的类库。
    • 它。就是是程序中默认的类加载器。要是没有自定义类加载器,ClassLoader.getSystemClassLoader() 返回的就

双亲委派模型

1. 什么是双亲委派模型?
如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆ 法结束这个加载请求(它的搜索范围中没有找到所需的类)时,⼦加载器才会尝试⾃⼰去完成加载。

  • 从下到上检查:Application ClassLoader -> Extension ClassLoader -> Bootstrap ClassLoader。

  • 从上到下尝试加载:只有当父加载器反馈自己无法完成这个加载请求(在自己的搜索范围没找到所需的类)时,子加载器才会尝试自己去加载。
    在这里插入图片描述

  • 启动类加载器:加载 JDK 中 lib ⽬录中 Java 的核⼼类库,即$JAVA_HOME/lib⽬录。 扩展类加载器。加载 lib/ext ⽬录下的类。

  • 应⽤程序类加载器:加载我们写的应⽤程序。

  • ⾃定义类加载器:根据⾃⼰的需求定制类加载器。


2.优点:

  • 避免类的重复加载:确保一个类在 JVM 中全局唯一。⽐如 A 类和 B 类都有⼀个⽗类 C 类,那么当 A 启动时就会将 C 类加载起来,那
    么在 B 类进⾏加载时就不需要在重复加载 C 类了。

  • 安全:防止核心 API 库被随意篡改。比如用户自定义了一个java.lang.Object 类,如果没有双亲委派,它会被加载,从而破坏 Java 体系。但有了它,这个请求会最终委派给启动类加载器,而启动类加载器加载的是核心的 Object 类,用户的这个类就无法被加载。


3. 打破双亲委派模型
在某些场景下,需要打破双亲委派模型,实现自己的类加载逻辑,例如:

  • 从非标准来源加载类(如网络、数据库)。

  • 搭建热部署、热替换(如 Tomcat 为每个 Web 应用提供独立的类加载器)。

  • 对类进行加密,必须在加载时解密。

如何实现:
通常继承 java.lang.ClassLoader 类,然后重写 findClass(String name) 方法。在这个方法中,编写如何获取类的字节码并调用 defineClass 方法来定义类的逻辑。

public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
  // 1. 根据 name 和 classPath,找到 .class 文件,读取为字节数组 byte[]
  byte[] classData = loadClassData(name);
  if (classData == null) {
  throw new ClassNotFoundException();
  } else {
  // 2. 调用 defineClass 方法,将字节数组转换为 Class 对象
  return defineClass(name, classData, 0, classData.length);
  }
  }
  private byte[] loadClassData(String className) {
  // 实现从特定路径(如文件、网络)读取 .class 文件的逻辑
  // 返回字节数组
  // ... (具体实现省略)
  return null;
  }
  }

总结

概念描述
过程 加载(获取字节流 -> 方式区 -> 生成Class对象) -> 链接(验证 -> 准备 -> 解析) -> 初始化(执行 )
类加载器Bootstrap(核心库) -> Extension(扩展库) -> Application(用户ClassPath)
双亲委派子加载器将加载请求委派给父加载器,父加载器无法完成时子加载器才自己加载。保证了安全性和唯一性。
自定义继承 ClassLoader,重写 findClass 方法,用于实现非标准来源的类加载、热部署等高级功能。

理解类加载机制是深入理解 JVM 工作原理、实现高级特性和处理复杂类冲突问题的关键。

posted on 2026-01-25 10:13  ljbguanli  阅读(5)  评论(0)    收藏  举报