双亲委派模型和破坏场景

前言:

         Java虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。虚拟机设计团队把类记载阶段中通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块叫做“类加载器”。

       类加载器虽然只用于实现类的加载动作,但是在java程序中起到的作用却远远不限于类加载阶段。对于任意的一个类,都需要由加载他的类加载器和这个类本身一同确立其唯一性。比较两个类是否相等,只有在这两个类在同一个类加载器的前提下才有意义。

       从java虚拟机的角度来说,只存在两种不同类加载器:一种是启动类加载器,这个类加载器使用C++语言实现,是虚拟机的一部分;另外一种就是所有其他的类加载器,这些类加载器都由java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader。


双亲委派模型

         如果一个类加载器收到加载类的请求时,该加载器并不会去加载该类,而是把请求委托给父类加载器,所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该类反馈给子类,子类加载器才会去尝试自己加载。

       这样做的好处:一个是安全性,一个是可以提高性能(避免重复加载和避免核心类被篡改)


JDBC为什么要破坏双亲委派模型

        在jdbc1.4之后,我们不需要再调用Class.forName来加载驱动程序,只需要把需要的驱动jar包放在工程的类加载路径里面,驱动就会自动被加载。

        这个自动加载采用的技术叫SPI,数据库驱动厂商也都做了更新。在jar包里面的META-INF/services目录里面有一个java.sql.Driver文件,文件里面包含驱动的全路径名。我们通过下面就可以获取到数据库连接:

Connection con =  DriverManager.getConnection(url , username , password ) ;   

 因为类加载器收到加载范围的限制,在某些情况下父类加载器无法加载到需要的文件,这时候就需要委托子类加载器去加载class文件。

JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包。DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说BootStrap类加载器还要去加载jar包中的Driver接口的实现类。我们知道,BootStrap类加载器默认只负责加载 $JAVA_HOME中jre/lib/rt.jar 里所有的class,所以需要由子类加载器去加载Driver实现,这就破坏了双亲委派模型。

查看DriverManager类的源码,看到在使用DriverManager的时候会触发其静态代码块,调用 loadInitialDrivers() 方法,并调用ServiceLoader.load(Driver.class) 加载所有在META-INF/services/java.sql.Driver 文件里边的类到JVM内存,完成驱动的自动加载。

         

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

    }

 

   public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

 这个子类加载器是通过 Thread.currentThread().getContextClassLoader() 得到的线程上下文加载器。

那么上下文加载器又如何获得呢?

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);
    ...
}

可以看到,在sun.misc.Laucher初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,所以线程上下文类加载器默认情况下就是系统加载器。


 Tomcat打破双亲委派模型

   Tomcat为什么不能使用默认的类加载机制呢:

1. 一个web容器可能需要部署两个应用程序,不同应用程序可能依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

2. 部署在同一个web中相同的类库相同的版本可以共享。

3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器类库和程序类库隔离开来。

4. web容器要支持jsp的热部署。

   Tomcat类加载机制

 

Tomcat类加载机制.png

  • CommonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
  • CommonClassLoader容器私有的类加载器,加载路径中的class对于Webapp不可见;
  • SharedClassLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
  1. CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用。
  2. CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
  3. WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
  4. JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。 (JSP热部署原理)

Tomcat 为了实现隔离性,没有遵守双亲委派模型,每个WebAppClassLoader加载自己的目录下的class文件,不会传递给父类加载器。

 

 


版权声明:本文为u014753478原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。