JVM的类加载机制

JVM的类加载器

类加载器的作用

Java 的类加载器(ClassLoader)是 JVM 中用于动态加载类文件的组件。它将 .class 文件中的字节码加载到内存中,并将其转换为 Class 对象,以供 JVM 执行。JVM 的操作对象是 Class 文件,JVM 把 Class 文件中描述类的数据结构加载到内存中,并对数据进行校验、解析和初始化,最终转化成可以被 JVM 直接使用的类型,这个过程被称为类加载机制。

  • 动态加载类 :在运行时根据需要加载类,而不是在编译时加载所有类。
  • 隔离不同的类命名空间 :通过不同的类加载器,可以隔离同名类,使得它们不会相互冲突

类加载器加载原则

JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

类加载器的种类

  • 启动类加载器:它是属于虚拟机自身的一部分,用 C++ 实现的(JDK9 后用 java 实现),主要负责加载<JAVA_HOME>\lib目录中或被 -Xbootclasspath 指定的路径中的并且文件名是被虚拟机识别的文件,它是所有类加载器的父亲。
  • 扩展类加载器:它是 Java 实现的,独立于虚拟机,主要负责加载<JAVA_HOME>\lib\ext目录中或被 java.ext.dirs 系统变量所指定的路径的类库。
  • 应用程序类加载器:它是 Java 实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那这个加载器就是我们程序中的默认加载器。

提示

除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。

类加载过程

类从被加载到 JVM 开始,到卸载出内存,整个生命周期分为七个阶段,分别是加载、验证、准备、解析、初始化、使用和卸载。其中验证、准备和解析这三个阶段统称为连接。

加载

JVM 在该阶段的目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,将静态数据结构转化成方法区中运行时的数据结构,并生成一个代表该类的 java.lang.Class 对象

链接

验证

JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,保证被校验类的方法在运行时不会做出危害虚拟机的事件。

准备

JVM 会在该阶段对static 关键字修饰的静态变量,分配内存并初始化,对应数据类型的默认初始值。

解析

该阶段将常量池中的符号引用转化为直接引用。

提示

符号引用和直接引用的区别

  • 符号引用 :以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。
    • 定义:包含了类、字段、方法、接口等多种符号的全限定名。
    • 特点:在编译时生成,存储在编译后的字节码文件的常量池中。
    • 独立性:不依赖于具体的内存地址,提供了更好的灵活性。
  • 直接引用 :通过对符号引用进行解析,找到引用的实际内存地址。
    • 定义:直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄。
    • 特点:在运行时生成,依赖于具体的内存布局。
    • 效率:由于直接指向了内存地址或者偏移量,所以通过直接引用访问对象的效率较高。

初始化

初始化阶段是执行初始化方法 <clinit> () 方法的过程,是类加载的最后一步,这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

初始化的时机

  • 创建类的实例时。
  • 访问类的静态方法或静态字段时(除了 final 常量,它们在编译期就已经放入常量池)。
  • 使用 java.lang.reflect 包的方法对类进行反射调用时。
  • 初始化一个类的子类(首先会初始化父类)。
  • JVM 启动时,用户指定的主类(包含 main 方法的类)将被初始化。

卸载

卸载类即该类的 Class 对象被 GC。

  • 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
  • 该类没有在其他任何地方被引用
  • 该类的类加载器的实例已被 GC

双亲委派模型

双亲委派模型是 Java 类加载机制的设计模式之一。它的核心思想是:类加载器在加载某个类时,会先委派给父类加载器去加载,父类加载器无法加载时,才由当前类加载器自行加载。

工作流程

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

提示

双亲委派机制先自下而上委托,再自上而下加载,那为什么不直接自上而下加载?

因为本来类加载器是组合关系,也就是子加载器只记录了父加载器,父加载器没记录子加载器(找不到子加载器)。

其次如果先父加载器接活再传给子加载器,假设有 5 个子加载器 (比如 5 个平级的自定义加载器)传给哪个加载呢?每个试过去嘛?效率就不高了。

好处

  • 保证类的唯一性 :通过委托机制,确保了所有加载请求都会传递到启动类加载器,避免了不同类加载器重复加载相同类的情况,保证了Java核心类库的统一性,也防止了用户自定义类覆盖核心类库的可能。
  • 保证安全性 :由于Java核心库被启动类加载器加载,而启动类加载器只加载信任的类路径中的类,这样可以防止不可信的类假冒核心类,增强了系统的安全性。例如,恶意代码无法自定义一个Java.lang.System类并加载到JVM中,因为这个请求会被委托给启动类加载器,而启动类加载器只会加载标准的Java库中的类。
  • 支持隔离和层次划分 :双亲委派模型支持不同层次的类加载器服务于不同的类加载需求,如应用程序类加载器加载用户代码,扩展类加载器加载扩展框架,启动类加载器加载核心库。这种层次化的划分有助于实现沙箱安全机制,保证了各个层级类加载器的职责清晰,也便于维护和扩展。
  • 简化了加载流程 :通过委派,大部分类能够被正确的类加载器加载,减少了每个加载器需要处理的类的数量,简化了类的加载过程,提高了加载效率。

提示

JVM 判定两个 Java 类是否相同的具体规则 :JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。

破环双亲委派模型的方法

重写 ClassLoader 的 loadClass() 方法。

JDBC 的接口是类库定义的,但实现是在各大数据库厂商提供的 jar 包中,那通过顶层的启动类加载器是找不到这个实现类的,所以就需要通过底层的应用程序加载器去完成这个任务

解决方案:使用线程上下文类加载器,通过 setContextClassLoader() 默认设置了应用程序类加载器,然后通过 Thread.current.currentThread().getContextClassLoader() 获得类加载器来加载。

最后更新于 2025-04-16 14:45 UTC
그 경기 끝나고 좀 멍하기 있었는데 여러분 이제 살면서 여러가
使用 Hugo 构建
主题 StackJimmy 设计