彻底搞懂Java的双亲委派模式

259 阅读6分钟

一、类与类加载器

对于任意一个类,都必须由加载它的类加载器这个类本身一起确定其在Java虚拟机中的唯一性。比较两个类是否”相等“,只有在这个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。

二、Java的类加载结构

1、三层类加载器

  • 启动类加载器(Bootstrap Class Loader)

    这个类加载器负责加载存放在 <JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,XX.jar)类库加载到虚拟机的内存中。

    启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,直接使用null代替即可。

  • 扩展类加载器(Extension Class Loader)

    负责加载 <JAVA_HOME>\lib\ext目录中或者被java.ext.dirs系统变量所指定的路径中所有的类库。开发者可以直接在程序中使用扩展类加载器来加载Class文件。

  • 应用程序类加载器(Application Class Loader)

    负责加载用户类路径ClassPath上的所有类库,开发者可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这就是程序中默认的类加载器

2、双亲委派

如图展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”。

image.png

  • 双亲委派模型的工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,子加载器才会尝试自己去完成加载。

  • 双亲委派模型的好处

    • 保证类的唯一性(避免类的重复加载) :Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载环境中都能保证是同一个类。

      如果没有使用双亲委派模型,都由各个类加载器自行去加载的话,系统中会出现多个不同的Object类,应用程序一片混乱。

    • 安全因素:Java核心API中定义类型不会被随意替换。假设通过网络传递一个名为java.lang.Object的类,通过双亲委派模式传递到启动类加载器,而启动类加载在核心API中发现存在这个类,并且这个类已被加载,就不会重新加载网络传递过来的java.lang.Object,这样可以防止核心API库被随意篡改。

三、破坏双亲委派模型

双亲委派机制的两个特征:不能向下委派,不能不委派。所以可以从这两个方面破坏双亲委派机制:

  • SPI机制(向下委派)

    Java在核心类库中定义了许多接口,并且给出了针对这些接口的调用逻辑,然后并未给出实现。开发者需要定制一个实现类,在resources/META-INF/services中注册实现类信息,以供核心类使用。

    数据库加载驱动就是典型的SPI机制:java.sql.DriverManager通过Bootstrap Class Loader加载,而com.mysql.jdbc.Driver是由Application Class Loader加载的,由于双亲委派模型,启动类加载器加载的DriverManager是拿不到应用程序类加载器加载的实现类,这时就需要启动类加载器委托子类来加载Driver实现,从而打破了双亲委派机制。

    查看Driver的类加载器:AppClassLoader

    Class<?> c = Class.forName("com.mysql.cj.jdbc.Driver");
    System.out.println(c.getClassLoader());
    

    image.png

    查看DriverManager的类加载器:

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    
    // loadInitialDrivers()代码片段
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    

    可以看到,DriverManager的类加载利用了ServiceLoader去委派子加载器完成Driver.class的加载

    一个利用ServiceLoader加载自定义类的例子:

    1、编写接口TestService和两个实现类Test1和Test2

    public interface TestService {
        public String printName();
    }
    
    public class Test1 implements TestService{
        @Override
        public String printName() {
            return "Test1";
        }
    }
    
    public class Test2 implements TestService{
        @Override
        public String printName() {
            return "Test2";
        }
    }
    

    2、在resources/META-INF/services下注册实现类的信息

    image.png

    3、测试类加载是否成功

    public class SPITest {
        public static void main(String[] args) {
    
            // 用ServiceLoader去加载TestService
            ServiceLoader<TestService> services = ServiceLoader.load(TestService.class);
    
            for(TestService testService :services){
                System.out.println(testService.printName());
    
            }
        }
    }
    

    image.png

自定义类加载器(不委派)

1、继承java.lang.ClassLoader

2、重写父类的findClass方法

public class MyClassLoader extends ClassLoader {
    // 指定路径
    private String filePath ;

    public MyClassLoader(String filePath){
        this.filePath = filePath;
    }

    /**
     * 重写findClass方法
     * @param name 类的全路径名
     * @return
     * @throws ClassNotFoundException
     */

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class Hello = null;
        // 获取该class文件字节码数组
        byte[] classData = new byte[0];
        try {
            classData = getData();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if(classData != null){
            // 将class的字节码数组转换成Class类的实例
            Hello = defineClass(filePath,classData,0,classData.length);
        }
        return Hello;
    }

    /**
     * 将class文件转化为字节码数组
     * @return
     */
    private byte[] getData() throws IOException {
        FileInputStream in = null;
        ByteArrayOutputStream out = null;
        try{
            in = new FileInputStream(filePath);
            out = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int size = 0;
            while((size = in.read(buffer))!= -1){
                out.write(buffer,0,size);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            in.close();
        }
        return out.toByteArray();
    }
}

public class Hello {
    public void sayHello(){
        System.out.println("Hello World!");
    }
}
public class TestMyClassLoader {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        // 类的class路径
        String classPath = "D:\\IdeaProjects\\AllDemos\\language-specification\\src\\main\\java\\com\\example\\parentdelegation\\Hello.class";

        MyClassLoader myClassLoader = new MyClassLoader(classPath);
        // 类的全限定名
        String className = "com.example.parentdelegation.Hello";
        // 加载Hello的class文件
        Class<?> c = myClassLoader.loadClass(className);
        System.out.println("Class Loader is :"+c.getClassLoader());

        // 利用反射运行Hello的sayHello方法
        Object object = c.newInstance();
        Method method = c.getDeclaredMethod("sayHello");
        method.invoke(object);
    }

}

如何没有使用MyClassLoad进行类加载,加载Hello.class的类加载器会是?

image.png

可以看到,成功使用自定义的类加载器MyClassLoader对Hello.java进行了类加载。而自定义类都是由Application ClassLoader进行加载

四、类加载过程

  1. 加载

    • 通过类的全限定名来获取定义此类的二进制字节流
    • 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构
    • 在内存中生成一个代表此类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  2. 验证

    确保Class文件的字节流中包含的信息不会危害虚拟机安全

  3. 准备

    正式为类中定义的静态变量分配内存并设置类变量初始值的阶段,初始值通常是指数据类型的零值。

    static final变量在准备阶段就会初始化为指定的值。

  4. 解析

    将Java虚拟机常量池内的符号引用替换为直接引用的过程。

  5. 初始化

    执行类构造器()方法的过程。()方法由类中的所有类变量的赋值动作和静态语句块中的语句合并产生。