JVM类加载器辨析

186 阅读5分钟

类加载器包括

  • 启动类加载器
  • 拓展类加载器(Java9之后已经被模块系统代替)
  • 应用类加载器

加载内容

  • 启动类加载器(Bootstrap ClassLoader)

    负责加载核心 Java 类库,包括 java.lang.*java.util.*java.io.* 等基础类。这些类通常位于JAVA_HOME/lib 目录下的核心 JAR 文件(如 rt.jar,从 Java 9 开始则是模块化的 JRT 文件系统)。

  • 拓展类加载器(Extension ClassLoader)

    负责加载 Java 扩展库中的类,通常位于 JAVA_HOME/lib/ext (新版java中已经没有这个目录了,已经被替代了),有时会将第三方jar包放进去

  • 应用类加载器(Application ClassLoader)

    负责加载应用程序类路径(classpath) 下的类。也就是运行时指定的 -classpath 路径下的所有类,主要是用户代码和第三方库。

类关系

  • 启动类加载器是所有加载器的顶层,没有父类加载器。
  • 扩展类加载器的父类是启动类加载器。
  • 应用类加载器的父加载器是扩展类加载器。

加载顺序

  • 启动类加载器 ,然后是拓展类加载器,然后是应用类加载器(依托于双亲委托机制,后面会解释)

解释

JAVA_HOME

  • JAVA_HOME是你JAVA下载的路径,JAVA_HOME是你配的环境变量

第三方jar包的加载

  • 第三方jar包通常由应用类加载器加载而不是拓展类加载器

    • 灵活性:将第三方库放在应用类加载器的类路径中更灵活,可以根据应用的需求灵活添加或移除依赖。
    • 隔离性:每个应用可以有不同的类路径,放在应用类加载器可以避免多个应用之间的库版本冲突,而拓展类加载器是系统层面的,他会应用于所有项目
    • 管理便利:大多数构建工具(如 Maven、Gradle)会自动将依赖放在应用的类路径中,而不是系统级的扩展路径。

应用类程序路径classpath

  • 默认情况下,Java 的 classpath 是当前目录

    假设在当前目录有如下结构:

    arduin
    oproject/
    ├── com/
    │   └── example/
    │       └── Main.class
    └── other/
        └── Helper.class
    

    project 目录下执行以下命令:

    bash
    cd project
    java com.example.Main
    

    此时,JVM 将会在 project/com/example 路径下查找 Main.class

  • 使用 -classpath-cp 命令行参数指定类路径

    在运行 Java 程序时,可以通过 -classpath-cp 参数指定类路径,例如:

    bash
    java -classpath /path/to/classes:/path/to/libs/library.jar com.example.Main
    

    或者

    bash
    java -cp /path/to/classes:/path/to/libs/library.jar com.example.Main
    

    上述命令中,classpath 被设置为 /path/to/classes/path/to/libs/library.jar,应用类加载器将会在这些路径中查找和加载类。

  • CLASSPATH 环境变量

    在一些情况下,可以设置 CLASSPATH 环境变量,指定 Java 应用程序的类路径。系统将默认使用 CLASSPATH 变量中定义的路径。

    • Linux/macOS

      bash
      export CLASSPATH=/path/to/classes:/path/to/libs/library.jar
      
    • Windows

      cmd
      set CLASSPATH=C:\path\to\classes;C:\path\to\libs\library.jar
      

    注意:使用 -classpath 参数会覆盖 CLASSPATH 环境变量,因此更常用的是使用 -classpath 指定特定程序的类路径,而不是全局设置 CLASSPATH

  • 使用构建工具(如 Maven、Gradle)自动设置类路径

    现代 Java 项目通常使用构建工具(如 Maven 和 Gradle),这些工具在构建和运行时会自动生成并指定类路径。比如,Maven 会将项目的第三方依赖下载到 target/classes 目录下,并在执行时自动生成完整的类路径,无需手动指定。

    在 IDE 中运行 Java 程序时,IDE(如 IntelliJ IDEA、Eclipse)会根据项目结构和依赖配置生成类路径,并传递给 JVM,所以不需要手动设置。

双亲委托机制

  • 假设一个类加载器 A 要加载类 X,双亲委托机制的工作流程如下:

    • 向上委托:类加载器 A 先把加载请求交给它的父加载器 B
    • 逐级上溯B 再把请求往上委托给它的父加载器 C,依此类推,直到请求到达最顶层的启动类加载器。

      • 找到类:如果某一层的父加载器找到了 X,则加载该类并返回。
      • 未找到类:如果所有父加载器都没有找到 X,则加载请求沿着委托链返回到原始加载器 A
    • 自己加载:此时,加载器 A 自行尝试加载 X,如果 A 找到该类,则成功加载。
  • 为什么使用双亲委托机制?
    • 安全性:通过双亲委托机制,Java 核心类(如 java.lang.String 等)会优先由启动类加载器加载,避免被用户自定义的同名类覆盖,从而保证系统的安全性和稳定性。
    • 类唯一性:双亲委托机制保证了同一类在 JVM 中的唯一性,防止不同类加载器加载出多个相同的类,确保类型检查的一致性。
    • 模块化和分离性:双亲委托机制允许用户定义自定义类加载器,但核心类由启动类加载器统一管理,这样在模块化和隔离性方面实现了较好的平衡。

JVM怎么通过classpath找到其所调用的类的

  • 类路径的作用classpath 指定了 JVM 在加载类时要查找的根目录或 JAR 文件路径。例如,classpath 可能是项目的 binout 目录,这里存放编译好的 .class 文件。

  • 包名映射路径:类的完全限定名(如 com.example.Maincom.example.Helper)会映射到文件系统的目录结构中:

    • com.example.Main 类在文件系统中对应路径为 com/example/Main.class
    • com.example.Helper 类对应路径为 com/example/Helper.class
  • 类加载过程

    • 当执行 java com.example.Main 时,应用类加载器会从 classpath 根目录(比如 binout)下查找 com/example/Main.class 文件并加载 Main 类。
    • Main 类中使用 Helper 类时,JVM 根据类路径在 com/example 目录下查找 Helper.class 文件。
    • 如果在 classpath 路径中找到了 com/example/Helper.class,应用类加载器将加载它。