从一个简单的HelloWorld来完整介绍Java的类加载过程

        我将以一个典型 Java 程序(HelloWorld)的执行过程为基础,逐步分析类加载的每一步,明确涉及的类、方法、输入数据格式、中间数据处理流程,以及最终输出数据格式。

        本文从程序启动开始,涵盖类加载的所有阶段(加载、链接、初始化),并具体到每个阶段调用的类和方法。


1. 概述:类加载的背景和流程

        类加载是 Java 虚拟机(JVM)将类文件(.class 文件或字节码)加载到内存,并将其转换为可执行的 java.lang.Class 对象的过程。它是 Java 程序运行的基础,确保程序所需的类在需要时被正确加载、验证和初始化。

类加载的三大阶段

  1. 加载(Loading):读取类文件的字节码到内存,生成 Class 对象。
  2. 链接(Linking)
    • 验证(Verification):检查字节码的合法性。
    • 准备(Preparation):为静态变量分配内存并设置默认值。
    • 解析(Resolution):将符号引用转换为直接引用。
  3. 初始化(Initialization):执行类的静态初始化代码(如静态块和静态变量赋值)。

类比:图书馆借书

  • 加载:从书库找到一本书(类文件)并放到借阅桌上(内存)。
  • 验证:检查书是否完整、合法(比如没有缺页)。
  • 准备:为书的笔记页(静态变量)分配空白空间。
  • 解析:把书中的参考文献(符号引用)替换为实际地址。
  • 初始化:填写书的笔记页(执行静态代码)。

示例程序

假设我们运行一个简单的 Java 程序 Main.java

public class Main {
    static int staticVar = 42;
    static {
        System.out.println("主程序中的静态块");
    }

    public static void main(String[] args) {
        System.out.println("Hello, World!");
        StaticTest test= new StaticTest();
    }
}

class StaticTest {
    static {
        System.out.println("在StaticTest类中的静态块");
    }
}

我们将以这个程序的执行过程,详细描述类加载的完整流程。


2. 完整类加载流程

以下是类加载的详细步骤,涵盖从程序启动到类加载完成的每一步,明确涉及的类、方法、输入输出数据格式,以及中间处理流程。

2.1 程序启动:JVM 初始化

  • 触发点:运行 java Main 命令。
  • 涉及类
    • JVM 内部实现(非 Java 类,由 C++ 实现)。
    • java.lang.BootClassLoader(引导类加载器,JVM 内部)。
  • 操作
    • JVM 启动,初始化引导类加载器(BootClassLoader)。
    • BootClassLoader 加载核心类库(如 java.lang.Objectjava.lang.Stringjava.lang.System)。
    • JVM 调用 java.lang.System 的 initializeSystemClass() 方法,设置系统属性和环境。
  • 输入数据格式
    • 命令行参数:java Main
    • 核心类库路径(JAVA_HOME/lib 或模块系统中的 java.base)。
  • 中间处理
    • JVM 解析命令行,找到主类 Main
    • BootClassLoader 读取核心类文件的字节码(.class 文件或模块化镜像)。
    • 核心类的字节码被加载到 JVM 的方法区(Method Area),生成 java.lang.Class 对象。
  • 输出数据格式
    • 核心类的 Class 对象,存储在方法区。
    • JVM 环境初始化完成,准备加载用户类。

类比:图书馆开门,主管理员(JVM)先把核心书籍(如字典、工具书)放到桌上(内存),准备好借阅系统。


2.2 加载主类:Main

  • 触发点:JVM 需要执行 Main 类的 main 方法。
  • 涉及类
    • java.lang.AppClassLoader(应用类加载器)。
    • java.lang.ClassLoader(抽象类,提供加载逻辑)。
    • jdk.internal.loader.BuiltinClassLoaderAppClassLoader 的父类)。
    • jdk.internal.loader.URLClassPath(辅助类,管理类路径)。
  • 操作
    1. 查找主类
      • JVM 通过 AppClassLoader 查找 Main 类。
      • AppClassLoader 调用父类 ClassLoader 的 loadClass(String name) 方法。
      • loadClass 实现双亲委派模型:
        • 先调用 getParent() 获取父加载器(PlatformClassLoader)。
        • PlatformClassLoader 再委托其父加载器(BootClassLoader)。
        • 如果父加载器找不到类,AppClassLoader 调用自己的 findClass(String name) 方法。
    2. 读取字节码
      • AppClassLoader 通过 BuiltinClassLoader 的 findClassInModuleOrClassPath 方法查找 Main.class
      • URLClassPath 从类路径(-cp 或默认路径)读取 Main.class 的字节码。
    3. 创建 Class 对象
      • AppClassLoader 调用 ClassLoader 的 defineClass(String name, byte[] b, int off, int len) 方法。
      • JVM 将字节码转换为 java.lang.Class 对象,存储在方法区。
  • 输入数据格式
    • 类名:Main(全限定名 Main)。
    • 类路径:文件系统路径或 JAR 文件。
    • 字节码:Main.class 文件的二进制数据(字节数组)。
  • 中间处理
    • 双亲委派:确保类只加载一次,避免冲突。
      • BootClassLoader 检查是否为核心类(不是,失败)。
      • PlatformClassLoader 检查是否为平台类(不是,失败)。
      • AppClassLoader 从类路径找到 Main.class
    • 字节码读取URLClassPath 打开文件流,读取 Main.class 的二进制数据。
    • Class 对象生成:JVM 解析字节码,创建 Class 实例,记录类的元信息(如方法表、字段表)。
  • 输出数据格式
    • java.lang.Class 对象,表示 Main 类。
    • 存储在方法区,包含类的元信息(方法、字段、常量池等)。

类比:管理员(AppClassLoader)在图书馆(类路径)找到 Main 这本书(.class 文件),检查是否已在核心书库或分馆(父加载器),然后把书的内容(字节码)整理成一本可用的书(Class 对象)。


2.3 链接主类:Main

链接阶段将 Main 类的 Class 对象准备好,分为验证、准备和解析三个子阶段。

2.3.1 验证(Verification)
  • 触发点Main 类加载后,JVM 自动验证字节码。
  • 涉及类
    • JVM 内部验证器(非 Java 类)。
    • java.lang.ClassLoader(提供上下文)。
  • 操作
    • JVM 调用内部验证器,检查 Main 类的字节码是否合法。
    • 验证内容:
      • 文件格式:是否符合 JVM 规范(魔数 CAFEBABE、版本号等)。
      • 语义:方法和字段的访问权限是否合法。
      • 字节码:指令序列是否安全(无非法跳转)。
      • 符号引用:常量池中的引用是否有效。
  • 输入数据格式
    • Main 类的字节码(存储在方法区)。
  • 中间处理
    • JVM 解析字节码的常量池、方法表、字段表。
    • 检查字节码的结构和语义,确保不会导致 JVM 崩溃。
    • 如果验证失败,抛出 java.lang.VerifyError
  • 输出数据格式
    • 验证通过的 Class 对象,标记为“可继续链接”。

类比:管理员检查 Main 这本书是否完整(格式正确)、内容合法(没有危险指令),确保它可以安全借阅。

2.3.2 准备(Preparation)
  • 触发点:验证通过后,JVM 准备静态变量。
  • 涉及类
    • JVM 内部实现。
    • java.lang.Class(存储静态变量信息)。
  • 操作
    • JVM 为 Main 类的静态变量分配内存,并设置默认值。
    • 示例:static int staticVar = 42; 的 staticVar 被分配内存,默认值为 0(int 的默认值)。
  • 输入数据格式
    • Main 类的 Class 对象,包含静态变量的元信息。
  • 中间处理
    • JVM 在方法区为 staticVar 分配 4 字节(int 类型)。
    • 设置初始值 0,暂不执行赋值语句(42 在初始化阶段赋值)。
  • 输出数据格式
    • Class 对象,静态变量内存分配完成,默认值设置。

类比:管理员为 Main 书的笔记页(静态变量)分配空白空间,暂时填上默认内容(0)。

2.3.3 解析(Resolution)
  • 触发点:JVM 解析 Main 类的符号引用(可选,延迟解析可能在初始化或运行时进行)。
  • 涉及类
    • java.lang.ClassLoader(解析引用时可能调用父加载器)。
    • java.lang.Class(存储常量池)。
  • 操作
    • JVM 解析 Main 类常量池中的符号引用,转换为直接引用。
    • 示例:Main 类的 main 方法调用 System.out.println,涉及 java.lang.System 和 java.io.PrintStream
    • JVM 通过 AppClassLoader 加载 System 和 PrintStream 类(如果尚未加载)。
  • 输入数据格式
    • Main 类的常量池,包含符号引用(如 CONSTANT_Class_info 指向 java.lang.System)。
  • 中间处理
    • JVM 查找符号引用的类(如 System),调用 ClassLoader.loadClass("java.lang.System")
    • BootClassLoader 加载 System 类,生成 Class 对象。
    • 常量池中的符号引用(如 #2)被替换为 Class 对象的内存地址。
  • 输出数据格式
    • Class 对象,常量池中的符号引用更新为直接引用。

类比:管理员把 Main 书中的参考书目(符号引用)替换为具体书的地址(直接引用),确保可以快速找到其他书。


2.4 初始化主类:Main

  • 触发点:JVM 准备执行 Main.main 方法前,初始化 Main 类。
  • 涉及类
    • java.lang.Class(存储静态代码)。
    • JVM 内部实现(执行初始化)。
  • 操作
    • JVM 执行 Main 类的静态初始化代码:
      • 静态变量赋值:staticVar = 42;
      • 静态块:System.out.println("主程序中的静态块");
    • JVM 调用  方法(类初始化方法,由编译器生成)。
  • 输入数据格式
    • Main 类的 Class 对象,包含  方法。
  • 中间处理
    • JVM 执行  方法:
      • 设置 staticVar = 42
      • 执行静态块,调用 System.out.println,输出 "主程序中的静态块"
    • 如果涉及其他类(如 System),可能触发它们的加载和初始化。
  • 输出数据格式
    • Main 类的静态变量初始化完成(staticVar = 42)。
    • 静态块执行,输出到控制台。
    • Class 对象标记为“已初始化”。

类比:管理员正式填写 Main 书的笔记页(静态变量和静态块),完成书的准备工作。


2.5 执行 main 方法:触发其他类加载

  • 触发点:JVM 调用 Main.main(String[] args)
  • 涉及类
    • java.lang.AppClassLoader(加载 StaticTest类)。
    • java.lang.ClassLoader(提供加载逻辑)。
    • jdk.internal.loader.BuiltinClassLoader
    • jdk.internal.loader.URLClassPath
    • java.lang.Class(表示 StaticTest类)。
  • 操作
    1. 执行 main 方法
      • 输出 "Hello, World!"(调用 System.out.println)。
      • 创建 StaticTest实例:new StaticTest()
    2. 加载 StaticTest
      • new StaticTest() 触发 StaticTest类的加载。
      • AppClassLoader 调用 loadClass("StaticTest")
      • 类似 Main 类,URLClassPath 读取 StaticTest.class 的字节码。
      • defineClass 创建 StaticTest的 Class 对象。
    3. 链接 StaticTest 类
      • 验证:检查 StaticTest字节码的合法性。
      • 准备:为 StaticTest的静态变量分配内存。
      • 解析:解析 StaticTest的符号引用(如 System)。
    4. 初始化 StaticTest
      • 执行  方法,运行静态块:System.out.println("在StaticTest类中的静态块");
    5. 创建 StaticTest实例
      • JVM 调用 StaticTest的构造方法,分配实例内存,生成对象。
  • 输入数据格式
    • 类名:StaticTest
    • 字节码:StaticTest.class 文件的二进制数据。
  • 中间处理
    • 加载AppClassLoader 通过双亲委派查找 StaticTest,从类路径加载字节码。
    • 链接:验证字节码,分配静态变量内存,解析引用。
    • 初始化:执行静态块,输出 "在StaticTest类中的静态块"
    • 实例化:分配堆内存,调用构造方法。
  • Output data format:
    • StaticTest类的 Class 对象(方法区)。
    • StaticTest实例(堆内存)。
    • 控制台输出:"在StaticTest类中的静态块"

类比:程序运行时,管理员发现需要 StaticTest这本书,重复“找书 -> 检查 -> 准备 -> 初始化”的过程,最终借出书并创建一本新副本(实例)。


2.6 程序输出

运行 java Main 的完整输出:

主程序中的静态块
Hello, World!
在StaticTest类中的静态块
  • 解释
    • Main 类的静态块在初始化时执行。
    • main 方法输出 "Hello, World!"
    • StaticTest类的静态块在 new StaticTest() 时执行。

3. 涉及的类和方法总结

以下是类加载流程中涉及的所有类和关键方法:

阶段 方法 作用
启动 JVM 内部实现, BootClassLoader (无 Java 方法,由 JVM 调用) 初始化 JVM,加载核心类库(如 java.lang.System)。
加载 AppClassLoaderClassLoaderBuiltinClassLoaderURLClassPath loadClass(String)findClass(String)defineClass(String, byte[], int, int)URLClassPath.getResource() 查找并读取 .class 文件,生成 Class 对象。
验证 JVM 内部, ClassLoader (无直接 Java 方法,JVM 验证器) 检查字节码合法性。
准备 JVM 内部, Class (无直接 Java 方法,JVM 分配内存) 为静态变量分配内存,设置默认值。
解析 ClassLoaderClass loadClass(String)(间接调用) 将符号引用转换为直接引用。
初始化 Class, JVM 内部 (由 JVM 执行) 执行静态变量赋值和静态块。
运行 AppClassLoaderClassLoaderClass loadClass(String)defineClass(构造方法) 加载依赖类(如 StaticTest),创建实例。

4. 数据格式总结

阶段 输入数据格式 中间处理 输出数据格式
加载 类名(String),.class 文件(字节数组) 双亲委派查找,读取字节码,解析元信息,生成 Class 对象 java.lang.Class 对象(方法区)
验证 字节码(Class 对象的元信息) 检查文件格式、语义、字节码指令、符号引用 验证通过的 Class 对象
准备 Class 对象(静态变量元信息) 分配内存,设置默认值(如 int 为 0 Class 对象(静态变量内存分配完成)
解析 常量池(符号引用) 加载依赖类,替换符号引用为直接引用(内存地址) Class 对象(常量池更新)
初始化 Class 对象( 方法) 执行静态变量赋值和静态块,可能触发其他类加载 Class 对象(静态变量初始化完成),控制台输出
运行 类名,字节码,构造参数 加载依赖类,执行构造方法,分配堆内存 Class 对象,实例对象(堆),控制台输出

5. 补充说明

5.1 双亲委派模型

  • 确保类加载的唯一性和安全性。
  • 流程:子加载器(如 AppClassLoader)先委托父加载器(如 PlatformClassLoaderBootClassLoader),只有父加载器失败时才自己加载。
  • 涉及方法:ClassLoader.loadClass 和 findClass

5.2 模块化支持(Java 9+)

  • PlatformClassLoader 和 AppClassLoader 支持模块系统。
  • java.lang.Module 定义模块边界,影响类加载的可见性。
  • 示例中,Main 和 StaticTest可能属于同一模块(如 unnamed module)。

6. 总结

类加载的完整流程从 JVM 启动到程序运行,涉及以下关键步骤:

  1. 启动:JVM 初始化,BootClassLoader 加载核心类。
  2. 加载AppClassLoader 通过 loadClass 和 defineClass 加载 Main 类,生成 Class 对象。
  3. 链接
    • 验证:JVM 检查字节码合法性。
    • 准备:分配静态变量内存,设置默认值。
    • 解析:将符号引用替换为直接引用。
  4. 初始化:执行 Main 的  方法,初始化静态变量和块。
  5. 运行:执行 main 方法,触发 StaticTest类的加载、链接、初始化和实例化。

涉及的核心类ClassLoaderAppClassLoaderPlatformClassLoaderBootClassLoaderBuiltinClassLoaderURLClassPathClassModule

数据流:从类名和字节码文件开始,经过加载、验证、准备、解析、初始化,最终生成 Class 对象和实例,输出到控制台。

你可能感兴趣的:(Java,JVM,java,开发语言)