JVM类加载机制

在 JVM 中,类加载器(ClassLoader)负责将磁盘上的 .class 文件读取到内存(元空间)中。Java 采用的是一种层级结构,不同的加载器负责加载不同路径下的类库。


1. 四种主要的类加载器

从 JDK 8 到 JDK 9+(模块化之后),类加载器的名称和职责稍有变化,但逻辑核心是一致的:

① 启动类加载器 (Bootstrap ClassLoader)

  • 地位 :它是加载器中的"老祖宗",由 C++ 编写,嵌套在 JVM 内部。
  • 职责 :负责加载 Java 的核心类库 (如 $JAVA_HOME/lib 目录下的 rt.jarresources.jar 等)。
  • 特点 :你在 Java 代码中无法直接获取它,返回通常为 null。它加载的是像 java.lang.Objectjava.lang.String 这样最基础的类。

② 扩展类加载器 (Extension ClassLoader) / 平台类加载器 (Platform ClassLoader)

  • JDK 8 :叫 Extension ClassLoader,负责加载 $JAVA_HOME/lib/ext 目录下的扩展类库。
  • JDK 9+ :由于模块化,改名为 Platform ClassLoader 。它负责加载 Java 平台的一些特定模块(如 java.sqljava.xml 等)。

③ 应用程序类加载器 (Application ClassLoader)

  • 别名:系统类加载器 (System ClassLoader)。
  • 职责 :负责加载用户类路径(ClassPath)上指定的类库。
  • 特点:这是我们在开发中最常用的加载器,如果你没有自定义加载器,你写的代码默认都由它加载。

④ 自定义类加载器 (Custom ClassLoader)

  • 实现 :通过继承 java.lang.ClassLoader 类并重写 findClass 方法实现。
  • 用途
    • 加密保护 :加载加密过的 .class 文件并在内存中解密。
    • 非标准来源:从数据库、网络甚至是云端加载类。
    • 热部署/隔离:像 Tomcat 这种 Web 容器,会为每个 Web 应用创建独立的加载器,实现应用间的类隔离。

2. 双亲委派模型 (Parent Delegation Model)

这是 Java 类加载器的核心工作机制。当一个类加载器收到加载请求时,它不会自己先去加载,而是:

  1. 向上委托 :把请求委派给父类加载器,一直委派到顶层的 Bootstrap ClassLoader
  2. 向下尝试:只有当父加载器反馈自己无法加载(在它的搜索范围没找到)时,子加载器才会尝试自己去加载。

为什么要有这个模型?(核心作用)

  • 安全性 :防止核心 API 被篡改。如果没有这个机制,你写一个 java.lang.String 类并让 ApplicationClassLoader 加载,那系统里就会有两个 String。有了委派机制,系统永远优先使用 Bootstrap 加载的官方版。
  • 避免重复加载:保证了在整个 JVM 范围内,同一个类只会被加载一次。

3.类加载过程

JVM 的类加载过程是将磁盘上的 .class 字节码文件转化成 元空间(Metaspace) 中运行数据结构的全过程。这个过程可以细分为五个阶段:加载、验证、准备、解析、初始化

1. 加载 (Loading)

这是类加载的第一步,主要完成三件事:

  • 获取二进制流 :通过类的全限定名(如 java.lang.String)找到对应的字节码文件。
  • 转化结构:将字节码中的静态存储结构转化为方法区(元空间)的运行时数据结构。
  • 生成 Class 对象 :在堆中生成一个代表该类的 java.lang.Class 对象,作为访问元空间中这些数据的入口。

2. 链接 (Linking)

链接阶段负责将类的二进制数据合并到 JVM 的运行时状态中,它包含三个小阶段:

① 验证 (Verification)
  • 目的:确保加载的类符合 JVM 规范,不会危害虚拟机安全。
  • 内容 :检查文件格式(魔数 CAFEBABE)、元数据验证、字节码验证、符号引用验证等。
② 准备 (Preparation)
  • 目的 :为类变量(static 修饰的变量)分配内存并设置初始零值
  • 关键点 :此时不会执行你的赋值逻辑。
    • 例如:public static int value = 123; 在准备阶段,value 的值是 0 而不是 123
    • 例外 :如果是 static final 修饰的常量,由于是常量,准备阶段就会直接赋予代码中的真实值。
③ 解析 (Resolution)
  • 目的 :将常量池内的符号引用 替换为直接引用
  • 内容 :比如代码中调用了 methodA(),在字节码里只是一个字符串符号,解析阶段会将其指向该方法在内存中的实际地址(偏移量)。

3. 初始化 (Initialization)

这是类加载过程的最后一步,也是真正开始执行 Java 代码的阶段。

  • 核心逻辑 :执行类构造器 <clinit> 方法的过程。
  • 动作
    1. 合并所有静态变量的赋值动作和 static { ... } 静态代码块。
    2. 按语句在源文件中出现的顺序执行。
    3. 父类优先 :JVM 会保证在子类的 <clinit> 执行前,父类的 <clinit> 已经执行完毕。

4.使用

使用类静态方法或创建对象

5.卸载

JVM 卸载类(Class Unloading)是一个比对象回收(Object GC)严苛得多的过程。在元空间(Metaspace)中,一个类只有在同时满足以下三个条件时,才会被垃圾回收器回收。

1. 条件一:该类所有的实例都已被回收
  • 要求:在 Java 堆中,已经不存在该类及其任何派生子类的任何实例对象。
  • 逻辑:只要还有一个活着的实例,类定义的"模板"就必须留在元空间里,否则实例就失去了结构支撑。
2. 条件二:加载该类的 ClassLoader 已被回收
  • 要求 :负责加载这个类的 ClassLoader 实例本身必须已经被 GC 回收。
  • 为什么这一条最难?
    • 对于 启动类加载器 (Bootstrap)平台类加载器 (Platform)应用程序类加载器 (App),它们在 JVM 运行期间几乎永远不会被回收。
    • 因此,我们编写的普通业务类,在正常的程序运行中基本不可能被卸载
  • 场景 :类卸载通常只发生在频繁使用 自定义类加载器 的场景,如:
    • OSGi热部署 框架:动态加载一个插件,卸载插件时销毁对应的加载器。
    • JSP 引擎:每次 JSP 修改后,Tomcat 可能会抛弃旧的加载器,创建新的来重新加载。
3. 条件三:该类对应的 java.lang.Class 对象没有在任何地方被引用
  • 要求:无法在任何地方通过反射(Reflection)访问该类的方法或字段。
  • 细节 :如果你的代码里还存着 Class<?> clazz = User.class; 这种强引用,或者有线程正在执行该类的方法,那么它就不是死透的。
类卸载条件汇总表
维度 检查项 判定标准
实例层 堆内存 (Heap) 实例总数 = 0 且被 GC 清理
加载层 类加载器 (ClassLoader) 加载器实例已被回收
引用层 反射与根搜索 (GC Roots) 无法通过任何路径触达该类的 Class 对象

6. 什么时候会触发"初始化"?

JVM 采用的是 懒加载 机制,只有在"主动使用"一个类时才会触发初始化:

  1. 使用 new 关键字创建实例。
  2. 访问或修改类的静态变量(非 final)。
  3. 调用类的静态方法。
  4. 使用 java.lang.reflect 进行反射调用。
  5. 初始化一个类时,发现其父类还没初始化,则先触发父类初始化。
  6. 虚拟机启动时的执行主类(包含 main 方法的那个类)。

相关推荐
2401_879693872 小时前
自动化与脚本
jvm·数据库·python
小王不爱笑1323 小时前
深入浅出 JVM:从内存结构到性能调优的全维度解析
jvm
xushichao19893 小时前
Python Web爬虫入门:使用Requests和BeautifulSoup
jvm·数据库·python
qq_416018723 小时前
开发一个简单的Python计算器
jvm·数据库·python
干啥啥不行,秃头第一名3 小时前
实战:用Python开发一个简单的区块链
jvm·数据库·python
qwehjk20083 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
m0_560396474 小时前
用Python创建一个Discord聊天机器人
jvm·数据库·python
m0_569881474 小时前
使用Flask快速搭建轻量级Web应用
jvm·数据库·python