面试官:如何打破双亲委派机制?
面试连环 call
双亲委派机制是什么?如何打破双亲委派机制?
JVM 都有哪些类加载器?
如何构造一个自定义类加载器?
Tomcat 的类加载机制?Spring 的类加载机制
Class.forName()和 ClassLoader.loadClass()区别?
在开始讲述之前简单回顾一下之前的类加载过程
类加载过程:加载->连接->初始化。
其中连接过程又分为:验证->准备->解析。
类加载器作用
类加载器的主要作用就是加载 Java 类的字节码( .class
文件)到 JVM 中(在内存中生成一个代表该类的 Class
对象)。
字节码可以是 Java 源程序(.java
文件)经过 javac
编译得来,也可以是通过工具动态生成或者通过网络下载得来。
需要注意的是
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
每个 Java 类都有一个引用指向加载它的
ClassLoader
。数组类不是通过
ClassLoader
创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
Class.forName()和 ClassLoader.loadClass()区别?
Class.forName(): 将类的.class 文件加载到 jvm 中之外,还会对类进行解释,执行类中的 static 块
ClassLoader.loadClass(): 只是将.class 文件加载到 jvm 中,不会执行 static 中的内容, 只有在 newInstance 才会去执行 static 中内容
类加载器分类
启动类加载器
: Bootstrap ClassLoader,负责加载存放在JDK\jre\lib
(JDK 代表 JDK 的安装目录,下同)下,或被-Xbootclasspath 参数指定的路径中的,并且能被虚拟机识别的类库(如 rt.jar,所有的 java.*开头的类均被 Bootstrap ClassLoader 加载)。启动类加载器是无法被 Java 程序直接引用的。扩展类加载器
: Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader
实现,它负责加载JDK\jre\lib\ext
目录中,或者由java.ext.dirs
系统变量指定的路径中的所有类库(如 javax.*开头的类),开发者可以直接使用扩展类加载器。应用程序类加载器
: Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。
🌈 拓展一下:
rt.jar:rt 代表“RunTime”,rt.jar 是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.*都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.*、java.sql.*、java.math.*。
Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。
双亲委派机制
定义
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终会传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子类加载器才会尝试自己去加载该类。
这种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。
为什么要使用?
防止内存中出现多份同样的字节码。如果没有该机制而是由各个类加载器自行加载的话,用户编写了一个
java.lang.Object
的同名类并放在ClassPath
中,多个类加载器都能加载这个类到内存中,系统中将会出现多个不同的Object
类,那么类之间的比较结果及类的唯一性将无法保证,同时,也会给虚拟机的安全带来隐患。双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。
这样可以保证系统库优先加载,即便是自己重写,也总是使用 Java 系统提供的 System,自己写的 System 类根本没有机会得到加载,从而保证安全性。
注意 ⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型
执行流程
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,相关代码如下所示。
自定义类加载器
构造自定义加载器,需要继承 ClassLoader
。如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
不破坏双亲委派
实现自定义类加载器的实现,主要分三个步骤
创建一个类继承 ClassLoader 抽象类
重写 findClass()方法
在 findClass()方法中调用 defineClass()
破坏双亲委派
定义CoderWorldClass.java
文件,并使用javac
编译成.class
文件
在 MyClassLoader 类中,重写loadClass
方法,代码如下
通过重写 loadClass 方法,使得自己创建的类,让第一个加载器直接加载,不委托父加载器寻找,从而实现双亲委派的破坏
Tomcat 类加载
Tomcat 是如何实现应用 jar 包的隔离的?
在思考这个问题之前,我们先来想想 Tomcat 作为一个 JSP/Servlet 容器,它应该要解决什么问题?
一个 web 容器需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
部署在同一个 web 容器中相同的类库相同的版本可以共享。否则,如果服务器有 10 个应用程序,那么要有 10 份相同的类库加载进虚拟机,必然会带来内存消耗过高的问题。
web 容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开。
Tomcat 类加载机制
Common类加载器
作为Catalina类加载器
和Shared类加载器
的父加载器。Common类加载器
能加载的类都可以被Catalina类加载器
和Shared类加载器
使用。因此,Common类加载器
是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。Catalina类加载器
和Shared类加载器
能加载的类则与对方相互隔离。Catalina类加载器
用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。Shared类加载器
作为WebApp类加载器
的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。每个 Web 应用都会创建一个单独的
WebApp类加载器
,并在启动 Web 应用的线程里设置线程线程上下文类加载器为WebAppClassLoader
,各个WebAppClassLoader
实例之间相互隔离,进而实现 Web 应用之间的类隔。
Tomcat 如何破坏双亲委派?
假设 Tomcat 服务器加载了一个 Spring Jar 包。项目中用到的基础类库,由于其是 Web 应用之间共享的,因此会由 SharedClassLoader
加载。项目中一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 SharedClassLoader
)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 SharedClassLoader
的加载路径下,所以 SharedClassLoader
无法找到业务类,也就无法加载它们。
如何解决这个问题呢?
这个时候就需要用到 线程上下文类加载器(ThreadContextClassLoader
) 。
当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 WebAppClassLoader
,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader
。这样就可以让高层的类加载器(SharedClassLoader
)借助子类加载器( WebAppClassLoader
)来加载业务类,从而破坏了 Java 类加载的双亲委托机制。
文章转载自:程序员世杰
评论