类加载机制

类加载过程

类加载过程:加载->连接->初始化。
连接过程又可分为三步:验证->准备->解析。

加载

类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流。

  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。

  3. 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口。

验证

验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

验证阶段也不是必须要执行的阶段。验证阶段主要由四个检验阶段组成:

  • 文件格式验证(Class 文件格式检查)
  • 元数据验证(字节码语义检查)
  • 字节码验证(程序语义检查)
  • 符号引用验证(类的正确性检查)

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

初始化

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

类加载器

类加载器的主要作用就是加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。

类加载器的种类

  1. 启动类加载器,负责加载 JVM 的核心类库,如 rt.jar 和其他核心库位于JAVA_HOME/jre/lib目录下的类。

  2. 扩展类加载器,负责加载JAVA_HOME/jre/lib/ext目录下,或者由系统属性java.ext.dirs指定位置的类库,由sun.misc.Launcher$ExtClassLoader 实现。

  3. 应用程序类加载器,负责加载
    classpath的类库,由sun.misc.Launcher$AppClassLoader实现。
    我们编写的任何类都是由应用程序类加载器加载的,除非显式使用自定义
    类加载器。

  4. 用户自定义类加载器,通常用于加载网络上的类、执行热部署(动态
    加载和替换应用程序的组件),或者为了安全考虑,从不同的源加载类。

类的生命周期

一个类从被加载到虚拟机内存中开始,到从内存中卸载,整个生命周期需要经过七个阶段:加载 、验证、准备、解析、初始化、使用和卸载。相比于类加载机制多了使用和卸载。

双亲委派机制

双亲委派模型要求类加载器在加载类时,先委托父加载器尝试加载,只有父加载器无法加载时,子加载器才会加载。

这个过程会一直向上递归,也就是说,从子加载器到父加载器,再到更上层的加载器,一直到最顶层的启动类加载器。

启动类加载器会尝试加载这个类。如果它能够加载这个类,就直接返回;如果它不能加载这个类,就会将加载任务返回给委托它的子加载器。

子加载器尝试加载这个类。如果子加载器也无法加载这个类,它就会继续向下传递这个加载任务,依此类推。

直到某个加载器能够加载这个类,或者所有加载器都无法加载这个类,最终抛出ClassNotFoundException

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

双亲委派模型的优点

双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载,也保证了 Java 的核心 API 不被篡改。如java.lang.*只能由 Bootstrap ClassLoader 加载,防止被篡改。

如何破坏双亲委派

重写 ClassLoader 的 loadClass() 方法。
如果不想打破双亲委派模型,就重写ClassLoader 类中的 findClass()方法,那些无法被父类加载器加载的类最终会通过这个方法被加载。

破坏双亲委派模型的典型例子

  • 第一种:SPI 机制加载 JDBC 驱动。
  • 第二种:热部署框架。

SPI机制

SPI 是 Java 的一种扩展机制,用于加载和注册第三方类库,常见于 JDBC、JNDI 等框架。

双亲委派模型会优先让父类加载器加载类,而 SPI 需要动态加载子类加载器中的实现。
根据双亲委派模型,java.sql.Driver类应该由父加载器加载,但父类加载器无法加载由子类加载器定义的驱动类,如 MySQL 的com.mysql.cj.jdbc.Driver。那么只能使用 SPI 机制通过 META-INF/services 文件指定服务提供者的实现类。

Tomcat 的类加载机制

Tomcat 基于双亲委派模型进行了一些扩展,主要的类加载器有:

  • Bootstrap ClassLoader:加载 Java 的核心类库;

  • Catalina ClassLoader:加载 Tomcat 的核心类库;

  • Shared ClassLoader:加载共享类库,允许多个 Web 应用共享某些类库;

  • WebApp ClassLoader:加载 Web 应用程序的类库,支持多应用隔离和优先加载应用自定义的类库(破坏了双亲委派模型)。

解释执行与编译执行的区别?

  • 解释:将源代码逐行转换为机器码。
  • 编译:将源代码一次性转换为机器码。

一个是逐行,一个是一次性,再来说说解释执行和编译执行的区别:

  • 解释执行:程序运行时,将源代码逐行转换为机器码,然后执行。
  • 编译执行:程序运行前,将源代码一次性转换为机器码,然后执行。

Java 一般被称为“解释型语言”,因为 Java 代码在执行前,需要先将源代码编译成字节码,然后在运行时,再由 JVM 的解释器“逐行”将字节码转换为机器码,然后执行。这也是 Java 被诟病“慢”的主要原因。

但 JIT 的出现打破了这种刻板印象,JVM 会将热点代码(即运行频率高的代码)编译后放入CodeCache,当下次执行再遇到这段代码时,会从
CodeCache 中直接读取机器码,然后执行。


类加载机制
http://bloomivy.github.io/2025/02/05/类加载机制/
作者
Bloom
发布于
2025年2月5日
许可协议