类加载器
什么是类加载器(ClassLoader)
是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。
从这个定义可以得出结论:Java是允许开发者写点代码获取字节码信息的
类加载器只参与加载过程中的字节码获取并加载到内存这一部分
类加载的应用场景
类加载器的分类
类加载器分为两类
- 一类是Java代码中实现的
- 一类是Java虚拟机底层源码实现的
下图中
左边的属于第二类,我们Java开发者一般不用过多关注,这种类加载器保证我们程序运行时候的基础类调用的可靠性
右边的属于第一类,我们Java开发者要重点关注,这些类加载器继承自ClassLoader
类加载器的设计JDK8与8+的版本差别很大,9开始出现了模块化,所以8到9是一个分水岭
JDK8及以前的版本中默认的类加载器有如下几种:
不过学习编程一定要养成质疑的好习惯,真的只有三种类加载器吗?可以用Arthas查看一下
Arthas中类加载器相关的功能
类加载器的详细信息可以通过classloader命令查看:
classloader - 查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource
按类加载类型查看统计信息
发现Bootstrap加载器加载了3633个核心类
Application加载器加载了4个自定义类或者第三方类
似乎并没有上图的Extension加载器,这个加载器是用来加载通用类的
这个原因应该是由于图中使用的JDK版本是Oracle的,而我使用的是Adoptium Eclipse Temurin的
命名不同,源码不同
启动类加载器
启动类加载器(BootstrapClassLoader)是由Hotspot虚拟 机提供的、使用C++编写的类加载器
默认加载Java安装目录/jre/lib下的类文件,比如rt.jar, tools.jar,resources.jar等
下图是Oracle OpenJDK的
这个图是Adoptium Eclipse Temurin的
看的出来还是有很多区别的
加载自定义jar包
有两种方式
- 放入jre/lib下进行扩展
不推荐,尽可能不要去更改JDK安装目录中的内容,会出 现即使放进去由于文件名不匹配的问题也不会正常地被加 载
- 使用参数进行扩展
推荐,使用-Xbootclasspath/a:jar包目录/jar包名 进行扩 展
java -Xbootclasspath/a:/path/to/jar/my-library.jar -jar your-application.jar
Java中的默认类加载器
- 扩展类加载器和应用程序类加载器都是JDK中提供的,使用Java编写的类加载器
- 它们的源码都位于sun.misc.Launcher中,是一个静态内部类。继承自URLClassLoader。具备通过目录 或者指定jar包将字节码文件加载到内存中
扩展类加载器
扩展类加载器(Extension Class Loader)是JDK中提供的、 使用Java编写的类加载器
默认加载Java安装目录/jre/lib/ext下的类文件。
加载自定义jar包
- 放入/jre/lib/ext下进行扩展
不推荐,尽可能不要去更改JDK安装目录中的内容
- 使用参数进行扩展
推荐,使用-Djava.ext.dirs=jar包目录 进行扩展,这种方 式会覆盖掉原始目录,
可以用;(windows):(macos/linux) 追加上原始目录
java -Djava.ext.dirs=C:\path\to\jar;C:\Program Files\Java\jdk1.8.0_xx\jre\lib\ext -jar your-application.jar
双亲委派机制(重点)
我的理解:
在 Java 的类加载机制中,父类加载器无法加载某个类 的情况通常是因为 双亲委派模型 的约束。根据 Java 类加载的双亲委派机制,当父类加载器无法加载某个类时,才会将加载任务委派给子类加载器。换句话说,子类加载器只有在父类加载器尝试加载失败后才会介入。
每个由Java实现的类加载器中保存了一个成员变量叫 "父" (Parent)类加载器,并不是继承关系,可以理解为它的上级
- 应用程序类加载器的parent父类加载器是扩展类加载器
- 扩展类加载器的parent父类加载器是null,空的
- 启动类加载器使用C++编写,没有上级类加载器
- 在类加载的过程中,每个类加载器都会首先检查是否已经加载了该类,如果已经加载了则直接返回,否则会将加载请求委派给父类加载器
- 如果类加载的parent为null,则会提交给启动类加载器处理
- 如果所有的父类加载器都无法加载该类,则由当前类加载器自己尝试加载。所以看上去是自顶向下的顺序尝试加载
不过一般什么情况下,连parent父类加载器都无法加载呢?
比如:
1.类不存在于父类加载器的加载路径中
类加载器通常会根据类的路径(如文件系统路径、JAR 包等)来加载类。如果类文件不在父类加载器的路径中,父类加载器就无法加载该类。此时,父类加载器会把加载请求交给子类加载器。
- 示例:假设父类加载器负责加载 Java 标准库中的类,而应用程序类(如自定义的类)则由应用类加载器来加载。
- 路径问题 :如果你尝试加载的类
MyClass
是应用程序的自定义类,而不是 Java 标准库类,父类加载器(系统类加载器)将无法在其默认路径中找到该类,它会将加载请求交给子加载器(比如应用程序的类加载器)去加载。
2.类加载器的职责范围不同
在 双亲委派模型 中,类加载器有不同的职责范围。例如:
- 启动类加载器(Bootstrap ClassLoader) 主要加载 Java 标准库中的核心类(如
java.lang.*
)。- 扩展类加载器(Extension ClassLoader) 加载 JDK 扩展目录中的类。
- 系统类加载器(System ClassLoader) 或应用类加载器,主要负责加载应用程序代码。
这些类加载器的职责范围是不同的,如果一个类属于某个类加载器的职责范围外,父加载器就无法加载该类,并会将加载请求交给下一级加载器。例如,系统类加载器无法加载核心类库中的类,因为这些类是由启动类加载器加载的。
还有很多情况,我大致总结了一下
父类加载器被显式禁止加载某些类
类加载器的命名空间冲突
父类加载器加载失败或发生异常
自定义类加载器的特性
动态代理与反射生成的类可能无法加载
- 第二次再去加载相同的类,仍然会向上进行委派,如果某个类加载器加载过就会直接返回
再看一个案例:
先自下而上查找每个加载器的加载目录
发现都没加载过,那么开始自上而下挨个尝试加载,若最上面的不能加载,就向下挨个尝试
双亲委派机制的作用(重点)
- 保证类加载的安全性
通过双亲委派机制,让顶层的类加载器去加载核心类,避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性
- 避免重复加载
双亲委派机制可以避免同一个类被多次加载,上层的类加载器如果加载过类,就会直接返回,避免重复加载
Arthas中类加载器相关的功能
类加载器的继承关系可以通过classloader -t查看
解决三个问题
上文说过,Java允许开发者用Java代码调用类加载器,
那么如何使用代码主动加载一个类呢?
**方式1:**使用Class.forName()方法,使用当前类的类加载器去加载指定的类
java
public class MyTest {
public static void main(String[] args) throws Exception{
// 动态加载类
Class<?> aClass = Class.forName("com.autismbtkrsr.demo21.MyTest");
// 创建实例
aClass.getDeclaredConstructor().newInstance();
System.out.println("加载类:" + aClass.getName());
}
}
**方法2:**获取到类加载器,通过类加载器的loadClass方法指定某个类加载器进行加载
java
public class MyTest {
public static void main(String[] args) throws Exception{
// 获取main方法所在类的类加载器,应用程序类加载器
Class<MyTest> aClass = MyTest.class;
ClassLoader classLoader = aClass.getClassLoader();
// 使用程序类加载器加载 com.autismbtkrsr.demo21.MyTest
Class<?> aClass1 = classLoader.loadClass("com.autismbtkrsr.demo21.MyTest");
System.out.println(aClass1.getClassLoader());
System.out.println("加载类:" + aClass1.getName());
}
}
打破双亲委派机制(重点)
三种方式
打破双亲委派机制的三种方式
自定义类加载器
- 一个Tomcat程序中是可以运行多个Web应用的,如果这两个应用中出现了相同限定名的类,比如Servlet类, Tomcat要保证这两个类都能加载并且它们应该是不同的类。
- 如果不打破双亲委派机制,当应用类加载器加载Web应用1中的MyServlet之后,Web应用2中相同限定名的 MyServlet类就无法被加载了
- Tomcat使用了自定义类加载器来实现应用之间类的隔离。 每一个应用会有一个独立的类加载器加载对应的类。
- 先来分析ClassLoader的原理,ClassLoader中包含了4个核心方法。
- 双亲委派机制的核心代码就位于loadClass方法中。
- 阅读双亲委派机制的核心代码,分析如何通过自定义的类加载器打破双亲委派机制。
- 打破双亲委派机制的核心就是将下边这一段代码重新实现。
自定义加载器的默认父类加载器
- 以JDK8为例,ClassLoader类中提供了构造方法设置parent的内容
- 这个构造方法由另一个构造方法调用,其中父类加载器由getSystemClassLoader方法设置,该方法返回的是AppClassLoader
实现自定义加载器
- 正确的实现一个自定义加载器的方法是重写findClass方法,这样不会破坏双亲委派机制
JDBC案例
- JDBC中使用了DriverManager来管理项目中引入的不同数据库的驱动,比如MySQL驱动,Oracle驱动
- DriverManager类位于rt.jar包中,由启动类加载器加载。
- 依赖中的mysql驱动对应的类,由应用程序类加载器来加载
- DriverManager属于rt.jar是启动类加载器加载的,而用户jar包中的驱动需要由应用类加载器加载,这就违反了双亲委派机制
SPI机制
- DriverManage使用SPI机制,最终加载jar包中对应的驱动类。
- SPI中使用了线程上下文中保存的类加载器进行类的加载,这个类加载器一般是应用程序类加载器。
1、启动类加载器加载DriverManager。
2、在初始化DriverManager时,通过SPI机制加载jar包中的myql驱动
3、SPI中利用了线程上下文类加载器(应用程序类加载器)去加载类并创建对象。
这个案例真的打破了机制吗?
似乎有争议?
OSGi模块化
- 历史上,OSGi模块化框架。它存在同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署的 功能。
- 历史上,OSGi模块化框架。它存在同级之间的类加载器的委托加载。OSGi还使用类加载器实现了热部署的 功能。
JDK9之后的类加载器
下一章: