讲个故事:
以前, 爱捣鼓的小明突然灵机一动, 写出了下面的代码
- package java.lang;
- public class String {
- //... 复制真正 String 的其他方法
- public boolean equals(Object anObject) {
- sendEmail(xxx);
- return equalsReal(anObject);
- }
- //...
- }
这样, 只要引用 java.lang.String 的人, 小明能随时收到他的系统的相关信息, 这简直是个天才的注意. 然而实施的时候却发现, JVM 并没有加载这个类.
这是为什么呢?
小明能想到的事情, JVM 设计者也肯定能想到.
双亲委派模型
上述故事纯属瞎编, 不过, 这确实是以前 JVM 存在的一个问题, 这几天看 Tomcat 源代码的时候, 发现频繁出现 ClassLoader 为什么要用这个东西呢?
想要解答这个问题, 得先了解一个定义: 双亲委派模型.
这个词第一次看见是在《深入理解 JVM》中, 目的也是为了解决上面所提出来的问题.
在 JVM 中, 存在三种类型的类加载器:
启动类 (Bootstrap) 加载器: 用于加载本地 (Navicat) 代码类的加载器, 它负责装入 %JAVA_HOME%/lib 下面的类. 由于引导类加载器涉及到虚拟机本地实现细节, 开发者无法直接获取到启动类加载器的引用, 所以不允许直接通过引用进行操作.
标准扩展 (Extension) 类加载器: 由 ExtClassLoader 实现, 负责加载
%JAVA_HOME/lib/ext%
或者系统变量 java.ext.dir(可使用
System.out.println("java.ext.dir")查看
)指定的类加载到内存中
系统 (System) 类加载器: 由 AppClassLoader 实现, 负责加载系统类 (环境变量 %CLASSPATH%) 指定, 默认为当前路径的类加载到内存中.
除去以上三种外, 还有一种比较特殊的线程上下文类加载器. 存在于 Thread 类中, 一般使用方式为 new Thread().getContextClassLoader()
可以看出来, 三种类型的加载器负责不同的模块的加载. 那怎么才能保证我所使用的 String 就是 JDK 里面的 String 呢? 这就是双亲委派模型的功能了:
上面三种类加载器中, 他们之间的关系为:
也就是 Bootstrap ClassLoader 作为 Extension ClassLoader 的父类, 而 Extension ClassLoader 作为 Application ClassLoader 的父类, Application ClassLoader 是作为 User ClassLoader 的父类的.
而双亲委派机制规定: 当某个特定的类加载在接收到类加载的请求的时候, 首先需要将加载任务委托给父类加载器, 依次递归到顶层后, 如果最高层父类能够找到需要加载的类, 则成功返回, 若父类无法找到相关的类, 则依次传递给子类.
补充:
如果 A 类引用了 B, 则 JVM 将使用加载类 A 的加载器加载类 B
类加载器存在缓存, 如果某个加载器以前成功加载过某个类后, 再次接受到此类加载请求则直接返回, 不再向上传递加载请求
可以通过
ClassLoader.loadClass()
或
Class.ForName(xxx,true,classLoader)
指定某个加载器加载类
类类型由加载它的加载器和这个类本身共同决定, 如果类加载器不同, 类名相同, instanceof 依然会返回 false
父加载器无法加载子加载器能够加载的类
可以看到, 通过双亲委派机制, 能够保证使用的类的安全性, 并且可以避免类重名的情况下 JVM 存在多个相同的类名相同, 字节码不同的类.
回到刚开始讲的故事, 虽然小明自定义了 String, 包名也叫 java.lang, 但是当用户使用 String 的时候, 会由普通的 Application ClassLoader 加载 java.lang.String, 此时通过双亲委派, 类加载请求会上传给 Application ClassLoader 的父类, 直到传递给 Bootstrap ClassLoader, 而此时, Bootstrap ClassLoader 将在 %JAVA_HOME%/lib 中寻找 java.lang.String 而此时正好能够找到 java.lang.String, 加载成功, 返回. 因此小明自己写的 java.lang.String 并没有被加载.
可以看见, 如果真的想要实现小明的计划, 只能将小明自己编写的 java.lang.String 这个 class 文件替换到 %JAVA_HOME%/lib/rt.jar 中的 String.class
自定义 ClassLoader
到这里, 估计能明白为什么需要双亲委派模型了, 而某些时候, 我们可以看见许多框架都自定义了 ClassLoader, 通过自定义 ClassLoader, 我们可以做很多好玩的事情, 比如: 设计一个从指定路径动态加载类的类加载器:
- public class DiskClassLoader extends ClassLoader {
- private String libPath;
- public DiskClassLoader(String path){
- libPath=path;
- }
- @Override
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- try(FileInputStream fileInputStream=new FileInputStream(new File(libPath,getFileName(name)));
- BufferedInputStream bufferedInputStream=new BufferedInputStream(fileInputStream);
- ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream()){
- for (int len=0;(len=bufferedInputStream.read())!=-1;){
- byteArrayOutputStream.write(len);
- }
- byte[] data=byteArrayOutputStream.toByteArray();
- return defineClass(name,data,0,data.length);
- }catch (IOException e){
- e.printStackTrace();
- }
- return super.findClass(name);
- }
- private String getFileName(String name) {
- int index = name.lastIndexOf('.');
- if(index == -1){
- return name+".class";
- }else{
- return name.substring(index+1)+".class";
- }
- }
- }
上面是一个简单的例子, 可以看见想要自定义 ClassLoader, 只需要继承 ClassLoader, 然后覆盖 findClass()方法即可, 其中 findClass()是负责获取指定类的字节码的, 在获取到字节码后, 需要手动调用 defineClass()加载类.
在 ClassLoader 类中, 我们能找到 loadClass 的源代码:
- protected Class<?> loadClass(String name, boolean resolve) {
- // First, check if the class has already been loaded
- Class<?> c = findLoadedClass(name);
- if (c == null) {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- if (c == null) {
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
在删减掉一些模板代码后, 我们可以看到 loadClass()方法就是实现双亲委派的主要代码: 首先查看类是否有缓存, 如果没有, 就调用父类的 loadClass 方法, 让父类去加载, 如果父类加载失败, 则自己加载, 如果自己加载失败, 那就返回 null, 注意: 并没有再找自己的子类去寻找类, 也就是在哪里发起的加载, 就在哪里结束.
这里可以看到, loadClass()方法并没有被标记为 final 的, 也就是我们依然可以重载它的 loadClass()方法, 破坏原本的委派双亲模型.
破坏双亲委派机制
有些时候, 双亲委派机制也会遇到一些问题, 在介绍双亲委派机制的时候, 我列举了一些补充. 而在一些 JDK 中, 存在一些基础 API 他们的加载由比较上层的加载器负责, 这些 API 只是一些简单的接口, 而具体的实现可能会由其他用户自己实现, 这个时候就存在一个问题, 如果这些基础的 API 需要调用 / 加载用户的代码的时候, 会发现由于父类无法找到子类所能加载的类的原因, 调用失败.
最典型的例子便是 JNDI 服务, JNDI 服务是在 JDK1.3 的时候放入 rt.jar 中, 而 rt.jar 有 Bootstrap ClassLoader 加载, JNDI 的功能是对资源进行集中管理和查找, 它需要调用独立厂商实现部部署在应用程序的 classpath 下的 JNDI 接口提供者 (SPI, Service Provider Interface) 的代码, 但启动类加载器不可能 "认识" 之些代码, 该怎么办?
这就需要用到最开始讲的特殊的加载器: 上下文类加载器
上下文类加载器的使用方式为: Thread.currentThread().getContextClassLoader()
上下文类加载器是什么意思呢? 可以看源码, Thread 初始化是通过本地方法 currentThread(); 初始化的, 而 classLoader 也正是通过 currentThread 初始化, currentThread 指的是当前正在运行的线程.
而默认情况下, 启动 Launcher 后, Launcher 会将当前线程的上下文加载器设置为 Application ClassLoader
- public Launcher() {
- //...
- try {
- this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
- } catch (IOException var9) {
- throw new InternalError("Could not create application class loader", var9);
- }
- Thread.currentThread().setContextClassLoader(this.loader);
- //...
- }
因此, 上下文类加载器默认就是系统加载器, 通过上下文加载器, 更高级别的加载器便可以调用系统加载器加载一个类.
Tomcat 与类加载器
Tomcat 作为一个 web 容器, 会包含各种 Web 应用程序, 而为了使各个应用程序不互相干扰, 至少需要达到以下要求:
部署在同一个 Web 容器上的两个 Web 应用程序所使用的 Java 类库可以实现相互隔离
部署在同一个 Web 容器上的两个 Web 应用程序所使用的 Java 类库可以相互共享
Web 容器需要保证自身的安全不受 Web 应用程序所影响
只是 JSP 的容器, 需要支持热部署功能
因为这些需求, 所以在 Tomcat 中, 类的加载不能使用简单的 ClassLoader 来加载, 而是需要自定义分级的 ClassLoader.
在 Tomcat 中, 定义了 3 组目录结构 / common/*,/server/* 和 / shared/* 可以存放 Java 类库, 另外还有 Web 应用程序自身的结构:/Web-INF/*, 而这几级目录结构分别对应了不同的加载器
common: 类库可以被 Tomcat 和所有 Web 应用程序共同使用
server: 类库可以被 Tomcat 使用, 对其他 Web 程序不可见
shared: 类库可以被所有的 Web 应用程序共同使用, 但对 Tomcat 不可见
Web-INF: 类库仅仅能被自身 Web 应用程序使用
因此, 需要支持以上结构, 可以通过自定义遵循双亲委派模型的 ClassLoader 来完成.
参考链接:
一看你就懂, 超详细 java 中的 ClassLoader 详解
Java 类加载机制与 Tomcat 类加载器架构
[JVM] 浅谈双亲委派和破坏双亲委派
关于 Java 类加载双亲委派机制的思考
来源: https://www.cnblogs.com/dengchengchao/p/11844022.html