JVM——类加载机制

一、什么是类的加载?

类的加载过程及其最终产品

JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。

  • 类加载子系统是 JVM 的一个具体运行时模块(规范中明确存在)

  • 类的加载系统是对类加载机制整体的描述(更像是一个概念性说法,不是 JVM 内部的独立模块名称)

  • 你可以理解为:

    • 类加载子系统 = JVM 内部真正干活的组件

    • 类的加载系统 = 类加载器体系 + 双亲委派 + 加载流程 这些机制的总称

二、类的生命周期

JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。

类的生命周期并非从"使用"开始,而是远早于此。它始于JVM"发现"需要一个类,并终结于这个类被从内存中彻底清理。整个过程由JVM的类加载子系统 主导,可分为七个明确的阶段:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

其中,加载、验证、准备、解析、初始化 这五个阶段,构成了广义上的 "类加载"过程 ,是类加载子系统的核心职责。


第一阶段:加载

这是类加载过程的起点。此阶段的任务是找到类的二进制字节流(通常是 .class 文件),并将其静态存储结构转化为方法区中的运行时数据结构,最后在堆内存中生成一个代表该类的 java.lang.Class 对象

关键动作与拓展知识:

  1. "寻找"字节流 :JVM并未规定字节流必须来自一个 .class 文件。这体现了Java强大的可扩展性。它可以来自:

    • 本地文件系统(最常见)。

    • 网络(Applet技术的基础)。

    • 运行时计算生成(动态代理技术,如 Proxy.newProxyInstance 生成的类)。

    • 由其他文件生成(如从JAR、WAR包中读取)。

    • 从加密文件中读取并解密。

      这种多样性正是通过 "类加载器" 实现的。

  2. 类加载器的层次与双亲委派模型

    • 启动类加载器 :最顶层,由C++实现,负责加载Java核心库(JAVA_HOME/lib目录下的类)。它是所有类加载器的"始祖"。

    • 平台类加载器 / 扩展类加载器 :负责加载一些扩展功能的库(JAVA_HOME/lib/ext目录)。

    • 应用程序类加载器:负责加载用户类路径(ClassPath)上的所有类。我们写的代码通常由它加载。

    • 自定义类加载器:用户可以根据特殊需求(如热部署、代码加密、模块化隔离)自己实现类加载器。

    双亲委派模型 是类加载的"工作流程":当一个类加载器收到加载请求时,它首先不会自己尝试加载,而是将这个请求向上委托给父加载器去完成。每一层都如此,只有当所有父加载器都无法完成(在自己的搜索范围内找不到该类)时,子加载器才会尝试自己加载。

    • 核心好处

      • 安全性 :防止用户自定义一个恶意的核心类(如 java.lang.String)来替代JVM核心实现。

      • 稳定性:确保了同一个类(由同一个加载器加载)在程序的各种模块中都是唯一的,避免了类的重复加载和冲突。

  3. 成果物 :在方法区(Metaspace)中创建了该类的结构模板(字段、方法、接口、常量池等信息),并在堆中创建了唯一的 Class 对象,作为访问方法区数据的入口和镜像


第二阶段:验证

这是一个至关重要的安全检查站。因为字节流来源广泛,甚至可能是被恶意篡改的,JVM必须确保其不会危害虚拟机的安全与稳定。

验证的四个层次:

  1. 文件格式验证 :验证字节流是否符合 .class 文件格式规范(魔数、版本号、常量池类型等)。这是基于二进制字节流的验证。

  2. 元数据验证 :对方法区中的数据结构进行语义分析,确保符合Java语言规范(例如:这个类是否有父类?是否继承了不允许被继承的 final 类?字段/方法是否与父类冲突?)。

  3. 字节码验证:最复杂的阶段。通过数据流和控制流分析,确定程序语义是合法、符合逻辑的(例如:方法体中的类型转换是否有效?跳转指令是否会跳到方法体以外的字节码?)。

  4. 符号引用验证 :发生在后续的解析阶段。当将符号引用转换为直接引用时,验证该引用是否能够被正确访问(例如:通过符号引用能否找到对应的类、字段、方法?访问权限是否允许?)。

拓展意义 :验证阶段确保了所有被加载的类在语言规范层面是"合法公民",为JVM的稳定运行筑起了第一道防火墙。大量的JVM参数(如 -Xverify:none)可以控制验证的严格程度,但生产环境绝不建议关闭。


第三阶段:准备

此阶段正式为类的静态变量分配内存 (在方法区中),并设置这些变量的默认初始值,而非程序代码中显式赋予的值。

核心要点与拓展:

  • 分配的内存是"类变量"(static变量),不包括实例变量(实例变量将在对象实例化时随对象一起分配在堆中)。

  • 设置的是"零值"

    • int -> 0

    • long -> 0L

    • boolean -> false

    • 引用类型 -> null

  • 特殊情况:常量(final static) :如果静态字段被 final 修饰(即常量),且在编译期就能确定其值,那么在准备阶段就会被直接初始化为程序代码中指定的值。这是因为常量在编译后就被明确,不需要变动。

此阶段可以看作是为类的静态数据"分配宿舍并铺好空床铺"的过程。


第四阶段:解析

这是将符号引用替换为直接引用的过程。

  • 符号引用 :一组用来描述所引用目标的符号,可以是任何形式的字面量,与虚拟机内存布局无关。在 .class 文件的常量池中,类、方法、字段的引用都以符号引用的形式存在(例如 java/lang/Object)。

  • 直接引用:可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。它与虚拟机实现的内存布局直接相关。同一个符号引用,在不同虚拟机实例上翻译出来的直接引用通常不同。

解析的目标 :主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号引用进行解析。
拓展知识 :解析并不一定在准备之后、初始化之前完成。JVM规范允许在"首次使用"某个符号引用时才去解析它,这被称为 "晚期绑定"或"动态链接"。这为Java的动态扩展能力(如反射)提供了基础。相对的,在类加载阶段完成解析称为"早期绑定"。


第五阶段:初始化

这是类加载过程的最后一步,也是真正开始执行用户编写的Java程序代码(字节码)的阶段

在此阶段,JVM会执行类的 <clinit>() 方法(由编译器自动收集类中所有类变量的赋值动作静态代码块中的语句合并而成)。

关键特性与拓展:

  1. 触发时机:"主动使用"一个类时才会初始化。JVM规范严格规定了六种"主动使用"场景,包括:

    • 创建类的实例(new)。

    • 访问或赋值类的静态字段(getstatic, putstatic),但被 final 修饰的常量除外(已在准备阶段解决)。

    • 调用类的静态方法(invokestatic)。

    • 使用反射(Class.forName("..."))强制加载类。

    • 初始化一个类的子类(会触发其父类的初始化)。

    • 被指定为JVM启动时的主类(包含 main 方法的类)。

  2. 线程安全<clinit>() 方法由JVM保证其在多线程环境下的正确加锁和同步。这意味着类的初始化在全局范围内是线程安全的。如果一个类正在被初始化,其他线程必须等待。

  3. 父类优先<clinit>() 方法执行顺序上,保证父类的初始化先于子类。这也意味着父类的静态代码块和静态变量赋值优先于子类。

  4. 非必需 :如果一个类没有静态代码块,也没有对静态变量的赋值操作,编译器可以不为这个类生成 <clinit>() 方法。

初始化标志着类加载过程的完成。此时,类的所有静态变量都已按照开发者的意图被正确赋值,静态代码块也已执行完毕,类已经完全"就绪",可以被用来创建对象、调用方法了。


后续阶段:使用与卸载

  • 使用 :在初始化完成后,类就进入了漫长的使用期。程序可以通过 Class 对象创建实例、访问方法、读写字段。

  • 卸载:当满足以下所有苛刻条件时,类可以被卸载,并从方法区(Metaspace)中回收:

    1. 该类的所有实例都已被垃圾回收。

    2. 加载该类的 ClassLoader 实例已被垃圾回收。

    3. 该类对应的 java.lang.Class 对象没有任何地方被引用,无法通过任何方式(包括反射)访问到该类的方法。

      在由系统类加载器加载的常规应用中,类的卸载几乎很少发生。但在频繁创建和销毁自定义类加载器的场景(如应用服务器、OSGi框架、热部署)下,类的卸载是管理元空间内存、防止内存溢出的关键机制。

三、类加载器

1、核心设计哲学:解耦与开放

类加载子系统最精妙的设计之一,便是将"通过一个类的全限定名来获取描述此类的二进制字节流 "这个关键动作,从JVM内部剥离 出来,交给外部的、独立的"类加载器"去完成。

我们可以这样理解:

  • JVM核心引擎:它只负责定义"类"在内存中应该是什么样子(运行时数据结构),以及如何验证、准备、初始化它。它制定了一套严格的"产品标准"(Class文件格式和生命周期)。

  • 类加载器 :它则是一个独立的"供应商 "或"资源获取模块"。它的唯一核心职责,就是按照JVM给出的"类名"(全限定名),去各种各样的"仓库"里,找到符合要求的"原材料"(二进制字节流),然后交给JVM核心引擎去加工。

这种设计的巨大优势:

  1. 技术解耦:JVM不必关心字节流从何而来。无论来自本地硬盘、网络远端、内存数据库、加密文件,还是运行时动态生成,对于JVM引擎来说,它收到的都是一个待处理的、统一的字节流。这极大地简化了JVM核心的设计。

  2. 极致的可扩展性 :这是最革命性的好处。因为获取字节流的动作被抽象成了一个接口(ClassLoader类),这意味着任何开发者都可以通过继承和重写这个接口的关键方法,来实现自己的"类获取逻辑"

    • 你可以实现一个从网络服务器下载类的加载器(历史上Applet的基础)。

    • 你可以实现一个对加密的.class文件进行解密的加载器,保护知识产权。

    • 你可以实现一个在内存中动态生成类字节码的加载器(如CGLib、动态代理技术)。

    • 你可以实现一个从非标准文件(如脚本文件)编译并生成字节码的加载器。

    • 结论 :只要最终能提供符合格式的二进制字节流,类可以来源于任何地方、以任何形式存在。Java平台的动态性、热部署、模块化隔离等高级特性,全部根植于此。

  3. 沙箱安全与隔离的基础:不同的类加载器实例,构成了不同的"命名空间"。即使是同一个全限定名的类,被不同的类加载器加载,在JVM看来也是两个完全不同的、互不兼容的类。这为应用程序服务器(如Tomcat)实现多个Web应用(可能使用同一库的不同版本)的隔离提供了技术基础。


2、系统的基石:三层核心类加载器

为了保证Java程序最基本的运行秩序,JVM自身提供了三个不可或缺的类加载器,它们不是平行关系,而是有严格的父子层级关系。这种层级关系,是"双亲委派模型"得以运转的物理结构。

第一层:启动类加载器
  • 身份与实现 :这是整个类加载器体系的始祖与根基 。它不是一个Java类,而是由JVM底层的C/C++代码实现 ,并内嵌在JVM内核之中。因此,在Java代码中你无法直接引用到它(获取其对象引用时通常为null)。

  • 神圣职责 :它负责加载最最核心的Java运行时库。这些库位于JRE安装目录下的 lib 目录中(例如 rt.jar, charsets.jar 等),是Java世界得以存在的基石(如 java.lang.*, java.util.*, java.io.* 等核心包)。

  • 特权地位:由它加载的类,享有最高的信任级别。它们是虚拟机自身的一部分,其完整性决定了整个JVM的稳定性。

第二层:平台类加载器(旧称:扩展类加载器)
  • 身份与实现 :这是一个由Java语言编写的、实实在在的类(sun.misc.Launcher$ExtClassLoader)。它是"启动类加载器"的直接子加载器

  • 核心职责 :它负责加载Java的扩展功能库 。这些库位于JRE安装目录下的 lib/ext 目录中,或者由 java.ext.dirs 系统变量指定的路径中。这些库用于对Java核心平台进行功能扩展。

  • 承上启下:它继承自启动类加载器,又是应用程序类加载器的父加载器。在JDK 9模块化系统之后,其角色和范围有所调整,但"加载非核心标准扩展"的基本定位依然存在。

第三层:应用程序类加载器(或称:系统类加载器)
  • 身份与实现 :同样是一个Java类(sun.misc.Launcher$AppClassLoader)。它是"平台类加载器"的直接子加载器

  • 核心职责 :这是与开发者日常编程关系最密切 的类加载器。它负责加载 "用户类路径" 下的所有类库。所谓"用户类路径",就是通过 -classpath-cp 参数指定的路径,或者是环境变量 CLASSPATH 所包含的路径。我们自己编写的所有Java代码(.class文件),以及项目依赖的第三方Jar包,几乎都是由这个类加载器加载的。

  • 默认加载器 :在代码中,通过 ClassLoader.getSystemClassLoader() 方法获取到的,通常就是这个应用程序类加载器。它也是程序中默认的上下文类加载器

四、双亲委派

协同工作机制:双亲委派模型

这三层类加载器并非各自为战,它们通过一个被称为 "双亲委派模型" 的强制性的工作流程来协同工作。这个模型是保证Java程序稳定性的关键。

工作流程(通俗版):

当一个类加载器(例如应用程序类加载器)收到一个类加载请求时(比如要加载 com.example.MyClass),它绝对不会立即尝试自己去加载。它会做以下事情:

  1. 向上委托(询问父辈) :它首先将这个请求逐级向上传递 给自己的父加载器(请求链:应用程序类加载器 -> 平台类加载器 -> 启动类加载器)。

  2. 顶层检查:启动类加载器首先在自己的"势力范围"(核心库目录)里查找。如果找到了,就由它加载,流程结束。

  3. 逐级下行:如果启动类加载器没找到(说明不是核心类),则请求会返回到平台类加载器,让它在其"势力范围"(扩展库目录)里查找。如果找到了,就由它加载,流程结束。

  4. 自己动手:如果平台类加载器也没找到(说明不是核心类也不是标准扩展类),请求最终才会回到最初发起请求的应用程序类加载器这里。此时,它才会真正执行自己的加载逻辑,去用户类路径下寻找并加载这个类。

双亲委派的核心价值:

  1. 绝对的安全性 :防止核心API被恶意篡改。假设有个不法分子自己写了一个 java.lang.String 类放在用户类路径下,由于双亲委派机制,加载请求会优先交给顶层的启动类加载器,而启动类加载器会在核心 rt.jar 中找到官方的 String 类并加载。用户自定义的 String 类永远不会被加载,从而杜绝了通过伪造核心类破坏系统的可能。

  2. 避免类的重复加载 :确保了同一个类在JVM中只有一份定义。只要父加载器加载了某个类,子加载器就没有必要、也无法再次加载它。这保证了像 java.lang.Object 这样的基础类,在全虚拟机范围内有且仅有一个 Class 对象。

  3. 清晰的职责边界:每一层加载器都有明确的加载范围,形成了清晰的层次化治理结构,使得类库的依赖和管理井然有序。

相关推荐
木风小助理10 小时前
Android 数据库实操指南:从 SQLite 到 Realm,不同场景精准匹配
jvm·数据库·oracle
xxxmine10 小时前
JVM类加载机制
jvm
alonewolf_9910 小时前
JVM核心技术深度解析:从类加载到GC调优的全栈指南
jvm
peixiuhui1 天前
Iotgateway技术手册-2. 技术栈
jvm
a努力。1 天前
虾皮Java面试被问:JVM Native Memory Tracking追踪堆外内存泄漏
java·开发语言·jvm·后端·python·面试
这周也會开心1 天前
JVM-垃圾回收器
jvm·算法
找不到、了1 天前
JVM 跨代引用与 Card Table 机制
java·jvm
sunywz1 天前
【JVM】(2)java类加载机制
java·jvm·python
alonewolf_991 天前
深入浅出JVM:从Class文件到GC调优的全方位解析
jvm