JVM-类加载机制
类加载的过程
-
加载
- 通过类名查找
.class
文件(如从磁盘、网络等),将字节码读入内存。 - 在堆中生成该类的
Class
对象(后续访问入口)。
- 通过类名查找
-
链接
- 验证:检查字节码合法性(如文件格式、语法)。
- 准备:为类静态变量分配内存并赋默认值(如
int
初始化为0
)。 - 解析:将符号引用(如方法名)替换为直接内存地址。
-
初始化
- 执行静态变量的显式赋值(如
static int a=5;
)和静态代码块(static{}
)。 - JVM保证多线程下初始化仅执行一次。
- 执行静态变量的显式赋值(如
⚠️ 类是按需动态加载的(用时才加载),非一次性加载所有类。
案例演示
public class Demo {static int value = 10; // 显式赋值在初始化阶段执行static { System.out.println("静态块执行"); }public static void main(String[] args) {System.out.println(Demo.value); }
}
执行过程
- 加载:找到
Demo.class
,创建堆中Class
对象。 - 链接-准备:为
value
分配内存,赋默认值0
。 - 初始化:
- 执行
value=10
(显式赋值) - 执行静态块 → 打印
"静态块执行"
- 执行
main
方法调用System.out.println(Demo.value)
→ 输出10
。
关键点
- 若类未初始化(如未调用
main
),静态代码块不会执行。- 静态变量在准备阶段分配空间(默认值),初始化阶段才赋值。
类加载器
ClassLoader分类
1. 启动类加载器(Bootstrap Class Loader)
- 实现:由JVM内核的C++代码实现(非Java类)。
- 职责:加载核心类库(如
java.lang
包中的String
、Object
等),路径为<JAVA_HOME>/lib
。 - 特点:唯一无父类加载器,无法通过Java代码获取(
String.class.getClassLoader()
返回null
)。
2. 扩展类加载器(Extension Class Loader)
- 实现:Java实现(
sun.misc.Launcher$ExtClassLoader
)。 - 职责:加载
<JAVA_HOME>/lib/ext
目录的扩展类库(如javax.*
包)。 - 特点:父加载器为
Bootstrap
。
3. 应用类加载器(Application Class Loader)
- 实现:Java实现(
sun.misc.Launcher$AppClassLoader
)。 - 职责:加载用户类路径(ClassPath)下的类(即项目代码及第三方jar包)。
- 特点:默认的类加载器,父加载器为
Extension
。
4. 自定义类加载器
- 实现:用户继承
ClassLoader
类重写findClass()
方法。 - 用途:按需动态加载(如热部署)、隔离类(如Tomcat隔离Web应用),打破双亲委派。
双亲委派机制
双亲委派机制介绍
当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
双亲委派机制的作用
比如一个定义在java.lang包下的类,因为它被存放在rt.jar之中,所以在被加载过程汇总,会被一直委托到Bootstrap ClassLoader,最终由Bootstrap ClassLoader所加载。
而一个用户自定义的com.hollis.ClassHollis类,他也会被一直委托到Bootstrap ClassLoader,但是因为Bootstrap ClassLoader不负责加载该类,那么会在由Extention ClassLoader尝试加载,而Extention ClassLoader也不负责这个类的加载,最终才会被Application ClassLoader加载。
这种机制有几个好处。
首先,通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类。
另外,通过双亲委派的方式,还保证了安全性。因为Bootstrap ClassLoader在加载的时候,只会加载%JRE_HOME%/lib中的jar包里面的类,如java.lang.Integer,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的JDK。
那么,就可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改。
"父子加载器"之间的关系是继承吗?
很多人看到父加载器、子加载器这样的名字,就会认为Java中的类加载器之间存在着继承关系。
甚至网上很多文章也会有类似的错误
观点。
这里需要明确一下,双亲委派模型中,类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合
(Composition)关系来复用父加载器的代码的。
如下为ClassLoader中父加载器的定义:
public abstract class ClassLoader {// The parent class loader for delegationprivate final ClassLoader parent;}
双亲委派是怎么实现的?
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中:
// 双亲委派机制核心实现 (java.lang.ClassLoader 源码简化版)
protected Class<?> loadClass(String name, boolean resolve) {synchronized (getClassLoadingLock(name)) {// 1、检查请求的类是否已经被加载过了Class<?> c = findLoadedClass(name);if (c == null) {try {// 2、将类加载请求先委托给父类加载器if (parent != null) {// 父类加载器不为空时,委托给父类加载进行加载c = parent.loadClass(name, false);} else {// 父类加载器为空,则代表当前是Bootstrap,从Bootstrap中加载类c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 如果父类加载器抛出ClassNotFoundException// 说明父类加载器无法完成加载请求// 父加载器加载失败,不报错继续}// 3、在父类加载器无法加载的时候,再调用本身的findClass方法来进行类加载if (c == null) {c = findClass(name); // 关键自定义加载点}}return c;}
}
代码不难理解,主要就是以下几个步骤:
1、先检查类是否已经被加载过
2、若没有加载则调用父加载器的loadClass()方法进行加载
3、若父加载器为空则默认使用启动类加载器作为父加载器。
4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
自定义类加载器
自定义类加载器实现
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;public class SimpleClassLoader extends ClassLoader {private final String classPath; // 类文件存放路径public SimpleClassLoader(String classPath) {this.classPath = classPath;}// 核心方法:重写findClass// 不破坏双亲委派模型:只重写 findClass()(不重写 loadClass)JVM会先尝试用父加载器加载,失败时才调用我们的自定义逻辑@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {// 1. 读取.class文件字节码byte[] classBytes = loadClassData(name);// 2. 将字节码转换为Class对象return defineClass(name, classBytes, 0, classBytes.length);} catch (IOException e) {throw new ClassNotFoundException(name, e);}}// 从文件系统加载字节码private byte[] loadClassData(String className) throws IOException {// 将包路径转换为文件路径 (com.example.Test -> com/example/Test.class)String path = classPath + File.separator + className.replace('.', File.separatorChar) + ".class";try (FileInputStream fis = new FileInputStream(path)) {byte[] buffer = new byte[fis.available()];fis.read(buffer);return buffer;}}
}
测试类
public class TestClass {public void print() {System.out.println("成功使用自定义加载器加载此方法!");}
}
使用示例
public class Main {public static void main(String[] args) throws Exception {// 1. 创建自定义加载器(从当前目录的classes文件夹加载)SimpleClassLoader loader = new SimpleClassLoader("./classes");// 2. 加载TestClass类Class<?> clazz = loader.loadClass("TestClass");// 3. 创建实例并调用方法Object instance = clazz.getDeclaredConstructor().newInstance();clazz.getMethod("print").invoke(instance);}
}
打破双亲委派机制
- 自定义类加载器
- 自定义类加载器并且重写loadClass方法,就可以将双亲委派机制的代码去除
- Tomcat通过这种方式实现应用之间类隔离
- 线程上下文加载器
- 利用上下文类加载器加载类,比如JDBC和JNDI等。SPI机制+线程上下文类加载器
场景1: Tomcat
在初学时部署项目,我们是把war包放到tomcat的webapp下,这意味着一个tomcat可以运行多个Web应用程序。
那假设我现在有两个Web应用程序,它们都有一个类,叫做User,并且它们的类全限定名都一样,比如都是com.yyy.User。但是他们的具体实现是不一样的。那么Tomcat是如何保证它们是不会冲突的呢?
答案就是,Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找。那这样就做到了Web应用层级的隔离。
并不是Web应用程序下的所有依赖都需要隔离的,比如Redis就可以Web应用程序之间共享(如果有需要的话),因为如果版本相同,没必要每个Web应用程序都独自加载一份啊。
做法也很简单,Tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载。(无非就是把需要应用程序之间需要共享的类放到一个共享目录下嘛)
为了隔绝Web应用程序与Tomcat本身的类,又有类加载器(CatalinaClassLoader)来装载Tomcat本身的依赖。
如果Tomcat本身的依赖和Web应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享。
各个类加载器的加载目录可以到tomcat的catalina.properties配置文件上查看
场景2: JDBC
在JDBC驱动加载的场景中,Java核心库的DriverManager需要加载第三方厂商的JDBC驱动实现类。由于DriverManager位于rt.jar包中,由Bootstrap类加载器加载
,而厂商的驱动实现类在应用classpath下,由AppClassLoader加载
。这就不符合双亲委派机制的规定。
类加载器命名空间可见性
- 子可见父:子加载器可直接访问父加载器加载的类。
- 父不见子:父加载器无法访问子加载器加载的类。
DriverManager是如何准确找到第三方的驱动的,它使用了spi
机制。
SPI
- JDK 提供的标准接口,供服务提供者实现(如数据库驱动、日志框架)。
- 目的 动态加载实现类,无需修改调用方代码(即“面向接口编程,运行时替换实现”)。
- 工作流程
- 定义接口(如
java.sql.Driver
)- 提供实现(如 MySQL 的
com.mysql.cj.jdbc.Driver
)- 注册配置:在
META-INF/services/接口全限定名
文件中声明实现类- 运行时发现:通过
ServiceLoader
自动加载匹配的实现类。SPI = 标准接口 + 实现类配置文件 + 动态加载*,解决灵活扩展的底层机制。
又有一个问题,DriverManager是在rt.jar下的,那它是如何委托我们的应用程序类加载器的?
采用了线程的上下文类加载器
,我们可以来看看serviceloader的源码。
这段代码其实就是获取了加载器,然后使用迭代器进去加载器驱动,我们进入load方法。
可以看见cl就是通过线程获取的加载器,它就是我们的应用类加载器。
我们可以来总结一下JDBC的流程
- 启动类加载器加载DriverManager。
- 在初始化DriverManager的时候通过spi机制去找到要加载的驱动
- spi机制使用了线程上下文类加载器也就是应用程序类加载器来加载我们的驱动。