赞
踩
目录
在开始文章内容之前,先来看三个问题
以上三个问题本文会逐一来讲解,下面先来看下 JVM 的类加载机制。
Java 的类加载就是把字节码格式 “.class” 文件加载到 JVM 方法区,并在 JVM 的堆中建立一个 java.lang.class 对象实例,用来封装 Java 类相关的数据和方法。
JVM 并不是在启动时就把所有的 “.class” 文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。JVM 类加载是由类加载器来完成的,JDK 提供一个抽象类 ClassLoader,这个抽象类中定义了三个关键方法理解清楚他们的作用和关系非常重要。
- public abstract class ClassLoader {
-
- // 每个类加载器都有个父加载器
- private final ClassLoader parent;
-
- public Class<?> loadClass(String name) {
- // 查找一下这个类是不是已经加载过了
- Class<?> c = findLoadedClass(name);
-
- // 如果没有加载过
- if( c == null ){
- // 先委托给父加载器去加载,注意这是个递归调用
- if (parent != null) {
- c = parent.loadClass(name);
- } else {
- // 如果父加载器为空,查找Bootstrap加载器是不是加载过了
- c = findBootstrapClassOrNull(name);
- }
- }
- // 如果父加载器没加载成功,调用自己的findClass去加载
- if (c == null) {
- c = findClass(name);
- }
-
- return c;
- }
-
- protected Class<?> findClass(String name){
- //1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
- ...
-
- //2. 调用defineClass将字节数组转成Class对象
- return defineClass(buf, off, len);
- }
-
- // 将字节码数组解析成一个Class对象,用native方法实现
- protected final Class<?> defineClass(byte[] b, int off, int len){
- ...
- }
- }
JVM 双亲委派如图
这些类加载器的工作原理是一样的,区别是他们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。双亲委派机制是为了保证一个 Java 类在 JVM 中是唯一的,假如不小心写了一个与 JRE 核心类同名的类,比如 Object 类,双亲委派机制能保证加载的是 JRE 里的那个Object 类,而不是你自己写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。
注意,类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象。
Tomca t的自定义类加载器 WebAppClassLoader 打破了双亲委派机制,首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。看下下面的源码
- public Class<?> findClass(String name) throws ClassNotFoundException {
- ...
-
- Class<?> clazz = null;
- try {
- //1. 先在Web应用目录下查找类
- clazz = findClassInternal(name);
- } catch (RuntimeException e) {
- throw e;
- }
-
- if (clazz == null) {
- try {
- //2. 如果在本地目录没有找到,交给父加载器去查找
- clazz = super.findClass(name);
- } catch (RuntimeException e) {
- throw e;
- }
-
- //3. 如果父类也没找到,抛出ClassNotFoundException
- if (clazz == null) {
- throw new ClassNotFoundException(name);
- }
-
- return clazz;
- }
在 findClass 方法里,主要有三个步骤:
- public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- synchronized (getClassLoadingLock(name)) {
- Class<?> clazz = null;
-
- //1. 先在本地cache查找该类是否已经加载过
- clazz = findLoadedClass0(name);
- if (clazz != null) {
- if (resolve)
- resolveClass(clazz);
- return clazz;
- }
-
- //2. 从系统类加载器的cache中查找是否加载过
- clazz = findLoadedClass(name);
- if (clazz != null) {
- if (resolve)
- resolveClass(clazz);
- return clazz;
- }
-
- // 3. 尝试用ExtClassLoader类加载器类加载,为什么?
- ClassLoader javaseLoader = getJavaseClassLoader();
- try {
- clazz = javaseLoader.loadClass(name);
- if (clazz != null) {
- if (resolve)
- resolveClass(clazz);
- return clazz;
- }
- } catch (ClassNotFoundException e) {
- // Ignore
- }
-
- // 4. 尝试在本地目录搜索class并加载
- try {
- clazz = findClass(name);
- if (clazz != null) {
- if (resolve)
- resolveClass(clazz);
- return clazz;
- }
- } catch (ClassNotFoundException e) {
- // Ignore
- }
-
- // 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
- try {
- clazz = Class.forName(name, false, parent);
- if (clazz != null) {
- if (resolve)
- resolveClass(clazz);
- return clazz;
- }
- } catch (ClassNotFoundException e) {
- // Ignore
- }
- }
-
- //6. 上述过程都加载失败,抛出异常
- throw new ClassNotFoundException(name);
- }
loadClass 方法稍微复杂一点,主要有六个步骤:
Tomcat 的类加载器打破了双亲委派机制,没有一上来就直接委托给父加载器,而是先在本地目录下加载,为了避免本地目录下的类覆盖 JRE 的核心类,先尝试用 JVM 扩展类加载器 ExtClassLoader 去加载。
先回答开头提到的第一个问题,如果我们使用 JVM 的 AppClassLoader 来加载 web 应用,AppClassLoader 只能加载一个 Servlet,再加载第二个同名的 Servlet 时,会返回第一个加载的 Servlet,同名的只被加载一次。Tomcat的解决方案是自定义一个类加载器 WebAppClassLoader,并且给每个 web 应用创建一个类加载器。不同的类加载器加载的类被认为是不同的类,即使名称相同,web 应用通过各自的类加载器来实现隔离。
在来看第二个问题,多个 web 应用之间需要共享类库,并且不能重复加载相同的类。在双亲委派机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也是通过这种方式共享 JRE 核心类。因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就结局了。
第三个问题,如何隔离 Tomcat 本身的类和 web 应用的类?要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,他们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。因此 Tomcat 又设计了一个类加载器CatalinaClassLoader,专门来加载 Tomcat 自身的类,这样设计有个问题,那 Tomcat 和 web 需要共享一些类时怎么办呢?
还是再增加一个 CommonClassLoader,作为 CatalinaClassLoader 和 SharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。
在 JVM 的实现中有一条规则,如果一个类由类加载器 A 加载,那么这个类的依赖类也由相同的类加载器完成。Spring 作为一个 bean 工厂,需要创建业务实体类,并且在创业业务类之前只要创建依赖类。
前面提到,web 应用之间共享的 JAR 包可以交给 SharedClassLoader 来加载,从而避免了重复,Spring 作为共享的三方 JAR 包,它自己是由 SharedClassLoader 加载的,但是Spring又要去加载业务类,但是业务类不在 SharedClassLoader 对应的目录下,那该怎么办呢?
Tomcat 使用了线程上下文加载器,它其实是一种类加载传递机制。这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中,就能把这个加载器取出来用。
Tomcat 为每个 Web 应用创建一个 WebAppClassLoader 类加载器,并在启动 Web 应用的线程里设置线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。这样就完成了 SharedClassLoader 创建的 Spring 可以创建 WebAppClassLoader 下的业务类,是不是设计的很精妙呢?
好了本期内容就介绍到这里。
往期经典推荐:
决胜高并发战场:Redis并发访问控制与实战解析-CSDN博客
TiDB内核解密:揭秘其底层KV存储引擎如何玩转键值对-CSDN博客
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。