01-JVM类加载机制

type
status
date
slug
summary
tags
category
icon
password

1️⃣ 类加载过程

当使用java命令运行main函数时,JVM通过类加载器将主类加载到内存。
1.加载(Loading)
查找并读取类的字节码文件(.class文件)到内存。
调用main()方法、创建对象(new)、访问静态成员等。
在方法区生成一个java.lang.Class对象,作为该类的元数据入口。
通过类加载器(如BootstrapExtensionApplicationClassLoader)从文件系统、JAR包或网络加载字节码。
2.验证(Verification)
确保字节码文件的格式和内容符合JVM规范,防止恶意或错误代码。
检查内容:
  • 文件格式(魔数、版本号)。
  • 语义正确性(方法调用、变量访问)。
  • 字节码指令合法性。
目的:保障安全性和正确性。
3.准备(Preparation)
为类的静态变量分配内存,并赋默认值(零值)。
仅处理静态变量,非静态变量在对象实例化时分配。
示例
static int a = 100; → 在准备阶段,a被分配内存并初始化为0(默认值)。
static final常量除外,常量在编译期已确定值。
4.解析(Resolution)
将符号引用替换为直接引用。
符号引用:类、方法、字段等的符号描述(如main()方法的字符串表示)。
直接引用:指向实际内存地址的指针或句柄。
类型
静态链接:类加载期间完成(如解析main()方法引用)。
动态链接:运行时完成(如动态调用的方法)。
示例:将MainClass.main的符号引用替换为指向方法区中main方法的内存地址。
5.初始化(Initialization)
执行类的初始化代码,为静态变量赋程序指定的值,运行静态代码块。
按代码中声明的顺序执行(从上到下)
执行内容
  • 静态变量的显式赋值(如static int a = 100;中的100)。
  • 静态代码块(static { ... })。
触发:类首次主动使用时(如调用main()、创建实例、访问静态成员)。
6.使用(Using)
类被JVM加载后,程序开始使用类(如执行main()方法逻辑、创建对象等)。
此阶段是类的实际运行期,可能涉及动态链接、对象分配等。
7.卸载(Unloading)
当类不再被引用且符合垃圾回收条件时,JVM卸载类,释放内存。
通常由垃圾回收器(GC)处理,开发者无需干预。
条件
  • 类的所有实例已被回收。
  • 类的Class对象无引用。
  • 类的类加载器已被回收。

运行main函数的类加载流程

  1. JVM启动,调用应用类加载器(Application ClassLoader)加载MainClass的字节码。
  1. 经过加载,在方法区创建MainClass的Class对象。
  1. 验证确保字节码合法(如检查public static void main(String[] args)签名)。
  1. 准备为MainClass的静态变量分配内存,赋默认值。
  1. 解析将main方法的符号引用替换为直接引用,指向方法区中main方法的代码。
  1. 初始化执行MainClass的静态变量赋值和静态代码块。
  1. JVM调用main方法,进入程序执行阶段(使用)。
  1. 程序结束后,若MainClass不再被引用,可能被卸载

2️⃣ 类加载器和双亲委派机制

Java的类加载器采用双亲委派模型,按层级分工加载不同来源的类。
1.引导类加载器(Bootstrap ClassLoader)
职责:加载JVM核心类库,位于<JAVA_HOME>/jre/lib目录下的rt.jar(包含java.lang.*等)、charsets.jar等。
特点
  • 由C++实现,非Java类,无父加载器。
  • 负责加载JVM运行所需的基础类(如Object、String)。
  • 无法直接访问(ClassLoader.getSystemClassLoader()不返回它)。
示例:加载java.lang.Object、java.util.ArrayList
2.扩展类加载器(Extension ClassLoader)
职责:加载<JAVA_HOME>/jre/lib/ext目录下的扩展JAR包或指定的扩展类库。
特点
  • 由Java实现,父加载器为引导类加载器(实际为null,表示引导类加载器)。
  • 用于加载标准库之外的扩展功能。
示例:加载安全管理类(如javax.crypto.*)。
3.应用程序类加载器(Application ClassLoader)
职责:加载ClassPath环境变量、-cp选项或JAR包中的类,主要加载用户编写的类。
特点
  • 默认加载用户代码(如运行main函数的类)。
  • 由Java实现,父加载器为扩展类加载器。
  • 也称为系统类加载器(ClassLoader.getSystemClassLoader()返回它)。
示例:加载MainClass或项目中的自定义类。
4.自定义类加载器(Custom ClassLoader)
职责:加载用户指定的非标准路径下的类或自定义加载逻辑。
特点
  • 继承java.lang.ClassLoader,重写findClassloadClass方法。
  • 用于特殊场景,如动态加载、加密类文件、热部署等。
  • 父加载器通常为应用程序类加载器(视定义而定)。
示例:加载网络下载的字节码、插件系统中的类。

1.双亲委派模型

类加载器遵循双亲委派机制,确保类的唯一性和安全性:
  • 收到加载请求时,先委托给父加载器(递归向上)
  • 父加载器无法加载时,子加载器才尝试加载。
  • 最终由引导类加载器或合适的加载器完成加载。
优点
  • 安全性:防止用户自定义类覆盖核心类(如java.lang.String)。
  • 唯一性:确保同一类只加载一次,避免冲突。
  • 效率:优先加载核心类库,减少重复加载。

2.类加载器在运行main函数中的作用

  • JVM启动,调用应用程序类加载器加载MainClass。
  • 加载过程中,依赖的核心类(如String)由引导类加载器加载。
  • 如果MainClass使用扩展库(如javax.crypto),由扩展类加载器加载。
  • 自定义加载器通常不直接参与,除非MainClass位于非标准路径或动态加载。
  • 加载完成后,进入验证、准备、解析、初始化阶段,最终执行main方法。

3.为什么要设计双亲委派机制?

沙箱安全机制
自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。
 
避免类的重复加载
当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性。

4.打破双亲委派

为什么需要打破双亲委派?
某些场景下,双亲委派模型限制了灵活性,需通过自定义类加载器打破规则:
  • 类隔离:不同模块或应用需加载同名但不同版本的类(如Web应用中的库冲突)。
  • 动态加载:运行时加载外部字节码(如插件系统、热部署)。
  • 版本管理:支持多个版本的同一类库共存(如不同Web应用依赖不同版本的Spring)。
  • 特殊加载逻辑:加载加密类、远程类或自定义路径的类。
如何打破双亲委派?
打破双亲委派通常通过自定义类加载器,重写ClassLoader的loadClass方法或直接实现加载逻辑。
重写loadClass方法
  • 默认情况下,ClassLoader.loadClass实现双亲委派逻辑。
  • 重写后可自定义加载顺序,优先尝试本地加载而非委托父加载器。
  • 示例代码:
直接调用defineClass
  • 绕过loadClass,直接从字节码定义类。
  • 常用于动态加载外部字节码(如插件或热部署)。
  • 示例:
使用线程上下文类加载器
  • 通过Thread.setContextClassLoader设置自定义类加载器,绕过默认加载器。
  • 常用于框架(如JDBC、Spring)动态加载实现类。
  • 示例:
典型场景与实现
1️⃣ Web容器(如Tomcat)
需求:每个Web应用需加载自己的依赖库(如不同版本的Spring),避免冲突。
实现
  • Tomcat使用WebappClassLoader为每个Web应用创建独立类加载器。
  • 优先加载WEB-INF/libWEB-INF/classes中的类,绕过双亲委派。
  • 核心库(如Servlet API)仍由共享的CommonClassLoader加载。
效果:实现类隔离,同一服务器上多个Web应用可使用不同版本的类库。
2️⃣ 模块化框架(如OSGi)
需求:支持动态加载和卸载模块,模块间类隔离。
实现
  • 每个Bundle(模块)有独立的类加载器。
  • 自定义加载逻辑,优先加载模块内部类,必要时导入其他模块的类。
效果:模块间互不干扰,支持热更新。
3️⃣ 热部署与插件系统
需求:运行时加载新类或更新类(如IDE插件)。
实现
  • 自定义类加载器从指定路径或网络加载字节码。
  • 直接调用defineClass定义类,绕过父加载器。
4️⃣ SPI(Service Provider Interface)
需求:核心类(如JDBC驱动)需加载用户实现的类,但核心类由引导类加载器加载,无法访问用户类。
实现
  • 使用线程上下文类加载器加载用户实现的类。
  • 示例:JDBC的DriverManager通过上下文类加载器加载第三方驱动。

3️⃣全盘负责委托机制

全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。
打破双亲委派意味着类加载器不再严格遵循“委托父加载器优先”的规则,而是由自定义类加载器全盘负责类的加载逻辑。这种方式常见于需要灵活控制加载行为的场景(如Web容器、模块化框架)。以下是“全盘负责”的机制和实现方式:
 
当打破双亲委派,自定义类加载器需要:
  • 完全控制类查找路径:从特定目录、网络、数据库等加载字节码。
  • 管理类隔离:确保同名类在不同上下文不冲突。
  • 处理依赖:手动解析类的依赖关系(如加载依赖的其他类)。
  • 验证与定义:确保字节码合法,并将其定义为Class对象。
 
上一篇
04-贪心算法
下一篇
02-JVM内存模型
Loading...