Java双亲委派模型

一、前言

平时做业务开发比较少接触类加载器,但是如果想深入学习Tomcat、Spring等开源项目,或者从事底层架构的开发,了解甚至熟悉类加载的原理是必不可少的。

java的类加载器有哪些?什么是双亲委派?为什么要双亲委派?如何打破它?多多少少对这些概念了解一些,甚至因为应付面试背过这些知识点,但是再深入一些细节,却知之甚少。

二、类加载器

类加载器,顾名思义就是一个可以将Java字节码加载为java.lang.Class实例的工具。这个过程包括,读取字节数组、验证、解析、初始化等。另外,它也可以加载资源,包括图像文件和配置文件。

类加载器的特点:

  • 动态加载,无需在程序一开始运行的时候加载,而是在程序运行的过程中,动态按需加载,字节码的来源也很多,压缩包jar、war中,网络中,本地文件等。类加载器动态加载的特点为热部署,热加载做了有力支持。
  • 全盘负责,当一个类加载器加载一个类时,这个类所依赖的、引用的其他所有类都由这个类加载器加载,除非在程序中显式地指定另外一个类加载器加载。所以破坏双亲委派不能破坏扩展类加载器以上的顺序。

一个类的唯一性由加载它的类加载器和这个类的本身决定(类的全限定名+类加载器的实例ID作为唯一标识)。比较两个类是否相等(包括Class对象的equals()isAssignableFrom()isInstance()以及instanceof关键字等),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。

从实现方式上,类加载器可以分为两种:一种是启动类加载器,由C++语言实现,是虚拟机自身的一部分;另一种是继承于java.lang.ClassLoader的类加载器,包括扩展类加载器应用程序类加载器以及自定义类加载器。

启动类加载器Bootstrap ClassLoader):负责加载<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果想设置Bootstrap ClassLoader为其parent可直接设置null

扩展类加载器Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定路径中的所有类库。该类加载器由sun.misc.Launcher$ExtClassLoader实现。扩展类加载器由启动类加载器加载,其父类加载器为启动类加载器,即parent=null

应用程序类加载器Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库,由sun.misc.Launcher$App-ClassLoader实现。开发者可直接通过java.lang.ClassLoader中的getSystemClassLoader()方法获取应用程序类加载器,所以也可称它为系统类加载器。应用程序类加载器也是启动类加载器加载的,但是它的父类加载器是扩展类加载器。在一个应用程序中,系统类加载器一般是默认类加载器。

三、双亲委派机制

1、什么是双亲委派

JVM 并不是在启动时就把所有的.class文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。除了启动类加载器外,其他所有类加载器都需要继承抽象类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 的类加载器是分层次的,它们有父子关系,而这个关系不是继承维护,而是组合,每个类加载器都持有一个 parent字段,指向父加载器。(AppClassLoaderparentExtClassLoaderExtClassLoaderparentBootstrapClassLoader,但是ExtClassLoaderparent=null。)
  • defineClass方法的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象。
  • findClass方法的主要职责就是找到.class文件并把.class文件读到内存得到字节码数组,然后调用 defineClass方法得到 Class 对象。子类必须实现findClass
  • loadClass方法的主要职责就是实现双亲委派机制:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载

2、为什么要双亲委派?

双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。

一个非常明显的目的就是保证java官方的类库<JAVA_HOME>\lib和扩展类库<JAVA_HOME>\lib\ext的加载安全性,不会被开发者覆盖。

例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。

如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。

四、破坏双亲委派

如果想自定义类加载器,就需要继承ClassLoader,并重写findClass,如果想不遵循双亲委派的类加载顺序,还需要重写loadClass。如下是一个自定义的类加载器,并重写了loadClass破坏双亲委派:

package com.stefan.DailyTest.classLoader;

import java.io.*;

public class TestClassLoader extends ClassLoader {
    public TestClassLoader(ClassLoader parent) {
        super(parent);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 1、获取class文件二进制字节数组
        byte[] data = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            FileInputStream fis = new FileInputStream(new File("C:\\study\\myStudy\\JavaLearning\\target\\classes\\com\\stefan\\DailyTest\\classLoader\\Demo.class"));
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = fis.read(bytes)) != -1) {
                baos.write(bytes, 0, len);
            }
            data = baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 2、字节码数组加载到 JVM 的方法区,
        // 并在 JVM 的堆区建立一个java.lang.Class对象的实例
        // 用来封装 Java 类相关的数据和方法
        return this.defineClass(name, data, 0, data.length);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException{
        // 1、找到ext classLoader,并首先委派给它加载,为什么?
        ClassLoader classLoader = getSystemClassLoader();
        while (classLoader.getParent() != null) {
            classLoader = classLoader.getParent();
        }
        Class<?> clazz = null;
        try {
            clazz = classLoader.loadClass(name);
        } catch (ClassNotFoundException e) {
            // Ignore
        }
        if (clazz != null) {
            return clazz;
        }
        // 2、自己加载
        clazz = this.findClass(name);
        if (clazz != null) {
            return clazz;
        }
        // 3、自己加载不了,再调用父类loadClass,保持双亲委派模式
        return super.loadClass(name);
    }
}


测试加载Demo类:

package com.stefan.DailyTest.classLoader;

public class Test {
    public static void main(String[] args) throws Exception {
        // 初始化TestClassLoader,并将加载TestClassLoader类的类加载器
        // 设置为TestClassLoader的parent
        TestClassLoader testClassLoader = new TestClassLoader(TestClassLoader.class.getClassLoader());
        System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());
        // 加载 Demo
        Class clazz = testClassLoader.loadClass("com.stefan.DailyTest.classLoader.Demo");
        System.out.println("Demo的类加载器:" + clazz.getClassLoader());
    }
}

//控制台打印
TestClassLoader的父类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
Demo的类加载器:com.stefan.DailyTest.classLoader.TestClassLoader@78308db1


注意破坏双亲委派的位置,自定义类加载机制先委派给ExtClassLoader加载,ExtClassLoader再委派给BootstrapClassLoader,如果都加载不了,然后自定义类加载器加载,自定义类加载器加载不了才交给AppClassLoader。为什么不能直接让自定义类加载器加载呢?

不能!双亲委派的破坏只能发生在AppClassLoader及其以下的加载委派顺序,ExtClassLoader上面的双亲委派是不能破坏的!

因为任何类都是继承自超类java.lang.Object,而加载一个类时,也会加载继承的类,如果该类中还引用了其他类,则按需加载,且类加载器都是加载当前类的类加载器。

Demo类只隐式继承了Object,自定义类加载器TestClassLoader加载了Demo,也会加载Object。如果loadClass直接调用TestClassLoaderfindClass会报错java.lang.SecurityException: Prohibited package name: java.lang

为了安全,java是不允许除BootStrapClassLOader以外的类加载器加载官方java.目录下的类库的。在defineClass源码中,最终会调用native方法defineClass1获取Class对象,在这之前会检查类的全限定名name是否是java.开头。(如果想完全绕开java的类加载,需要自己实现defineClass,但是因为个人能力有限,没有深入研究defineClass的重写,并且一般情况也不会破坏ExtClassLoader以上的双亲委派,除非不用java了。)

-
通过自定义类加载器破坏双亲委派的案例在日常开发中非常常见,比如Tomcat为了实现web应用间加载隔离,自定义了类加载器,每个Context代表一个web应用,都有一个webappClassLoader。再如热部署、热加载的实现都是需要自定义类加载器的。破坏的位置都是跳过AppClassLoader

五、Class.forName默认使用的类加载器

  1. forName(String name, boolean initialize,ClassLoader loader)可以指定classLoader

  2. 不显式传classLoader就是默认当前类的类加载器:

    public static Class<?> forName(String className)

                 throws ClassNotFoundException {
       Class<?> caller = Reflection.getCallerClass();
       return forName0(className, true, ClassLoader.getClassLoader(caller), caller);

    }

六、线程上下文类加载器

线程上下文类加载器其实是一种类加载器传递机制。可以通过java.lang.Thread#setContextClassLoader方法给一个线程设置上下文类加载器,在该线程后续执行过程中就能把这个类加载器取(java.lang.Thread#getContextClassLoader)出来使用。

如果创建线程时未设置上下文类加载器,将会从父线程(parent = currentThread())中获取,如果在应用程序的全局范围内都没有设置过,就默认是应用程序类加载器。

线程上下文类加载器的出现就是为了方便破坏双亲委派:

一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能去加载ClassPath下的类。

但是有了线程上下文类加载器就好办了,JNDI服务使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。

Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

摘自《深入理解java虚拟机》周志明

七、要点回顾

  1. java 的类加载,就是获取.class文件的二进制字节码数组并加载到 JVM 的方法区,并在 JVM 的堆区建立一个用来封装 java 类相关的数据和方法的java.lang.Class对象实例。
  2. java默认有的类加载器有三个,启动类加载器(BootstrapClassLoader),扩展类加载器(ExtClassLoader),应用程序类加载器(也叫系统类加载器)(AppClassLoader)。类加载器之间存在父子关系,这种关系不是继承关系,是组合关系。如果parent=null,则它的父级就是启动类加载器。启动类加载器无法被java程序直接引用。
  3. 双亲委派就是类加载器之间的层级关系,加载类的过程是一个递归调用的过程,首先一层一层向上委托父类加载器加载,直到到达最顶层启动类加载器,启动类加载器无法加载时,再一层一层向下委托给子类加载器加载。
  4. 双亲委派的目的主要是为了保证java官方的类库<JAVA_HOME>\lib和扩展类库<JAVA_HOME>\lib\ext的加载安全性,不会被开发者覆盖。
  5. 破坏双亲委派有两种方式:第一种,自定义类加载器,必须重写findClassloadClass;第二种是通过线程上下文类加载器的传递性,让父类加载器中调用子类加载器的加载动作。

参考:

  • 《深入理解java虚拟机》周志明(书中对类加载的介绍非常详尽,部分精简整理后引用。)
  • 《深入拆解Tomcat & Jetty》Tomcat如何打破双亲委托机制?李号双
  • 《Tomcat内核设计剖析》汪建,第十三章 公共与隔离的类加载器