4、类加载及双亲委派及如何打破双亲委派

一、类加载器

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

1、类加载器的特点

1、动态加载

无需在程序一开始运行的时候加载,而是在程序运行的过程中,动态按需加载,字节码的来源也很多,压缩包jar、war中,网络中,本地文件等。类加载器动态加载的特点为热部署,热加载做了有力支持。

2、最终负责

当一个类加载器加载一个类时,这个类所依赖的、引用的其他所有类都由这个类加载器加载,除非在程序中显式地指定另外一个类加载器加载。所以破坏双亲委派不能破坏扩展类加载器以上的顺序。双亲委托类加载顺序:自定义类加载器->应用程序类加载器->扩展类加载器->启动类加载器

3、类唯一性

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

4、类加载器构成

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

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

扩展类加载器Extension ClassLoader):负责加载\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字段,指向父加载器。( 双亲委托类加载顺序:自定义类加载器->应用程序类加载器->扩展类加载器->启动类加载器一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载

双亲委派模型

2、为什么要双亲委派?

1、统一各类加载器:双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。(例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。)

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

三、破坏双亲委派

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。这个委派和加载顺序完全是可以被破坏的

如果想自定义类加载器,就需要继承ClassLoader,并重写findClass,如果想不遵循双亲委派的类加载顺序,还需要重写loadClass

1、直接自定义类加载器加载

如下是一个自定义的类加载器TestClassLoader,并重写了findClassloadClass

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 {
            System.out.println(name);
            String namePath = name.replaceAll("\\.", "\\\\");
            String classFile = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\" + namePath + ".class";
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            FileInputStream fis = new FileInputStream(new File(classFile));
            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{
        Class clazz = null;
        // 直接自己加载
        clazz = this.findClass(name);
        if (clazz != null) {
            return clazz;
        }

        // 自己加载不了,再调用父类loadClass,保持双亲委托模式
        return super.loadClass(name);
    }
}

测试
初始化自定义的类加载器,需要传入一个parent,指定其父类加载器,那就先指定为加载TestClassLoader的类加载器为TestClassLoader的父类加载器吧:

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("study.stefan.classLoader.Demo");
        System.out.println("Demo的类加载器:" + clazz.getClassLoader());
}

运行如下测试代码,发现报错了:
找不到java\lang\Object.class,我加载study.stefan.classLoader.Demo类和Object有什么关系呢?

转瞬想到java中所有的类都隐含继承了超类Object,加载study.stefan.classLoader.Demo,也会加载父类ObjectObjectstudy.stefan.classLoader.Demo并不在同个目录,那就找到Object.class的目录(将jre/lib/rt.jar解压),修改TestClassLoader#findClass如下:
遇到前缀为java.的就去找官方的class文件。

运行测试代码:还是报错了!!!

报错信息为:Prohibited package name: java.lang
跟了下异常堆栈:
TestClassLoader#findClass最后一行代码调用了java.lang.ClassLoader#defineClass
java.lang.ClassLoader#defineClass最终调用了如下代码:


看意思是java禁止用户用自定义的类加载器加载java.开头的官方类,也就是说只有启动类加载器BootstrapClassLoader才能加载java.开头的官方类。

得出结论,因为java中所有类都继承了Object,而加载自定义类study.stefan.classLoader.Demo,之后还会加载其父类,而最顶级的父类Object是java官方的类,只能由BootstrapClassLoader加载。

2、跳过AppClassLoader和ExtClassLoader

既然如此,先将study.stefan.classLoader.Demo交由BootstrapClassLoader加载即可。
由于java中无法直接引用BootstrapClassLoader,所以在初始化TestClassLoader时,传入parent为null,也就是TestClassLoader的父类加载器设置为BootstrapClassLoader

package com.stefan.DailyTest.classLoader;

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

双亲委派的逻辑在 loadClass,由于现在的类加载器的关系为TestClassLoader —>BootstrapClassLoader,所以TestClassLoader中无需重写loadClass

运行测试代码:

成功了,Demo类由自定义的类加载器TestClassLoader加载的,双亲委派模型被破坏了。

如果不破坏双亲委派,那么Demo类处于classpath下,就应该是AppClassLoader加载的,所以真正破坏的是AppClassLoader这一层的双亲委派。

3、自定义类加载器加载扩展类

假设classpath下由上述TestClassLoader加载的类中用到了\lib\ext下的扩展类,那么这些扩展类也会由TestClassLoader加载,但是会报类文件找不到的情况。
但是自定义类加载器也是能加载\lib\ext下的扩展类的,只要自定义类加载器能找准扩展类的类路径。

以扩展目录com.sun.crypto.provider下的类举例:
1、Demo中随便引用一个扩展类:

import com.sun.crypto.provider.ARCFOURCipher;
public class Demo {
    public Demo() {
        ARCFOURCipher arcfourCipher = new ARCFOURCipher();
        System.out.println("ARCFOURCipher.getClassLoader=" + arcfourCipher.getClass().getClassLoader());
    }
}

2、修改TestClassLoader#findClass:

3、测试代码中需要调用一下Demo类的构造器:

4、运行测试代码
自定义类加载器成功加载了扩展类。

由上得出结论,\lib\ext下的扩展类是没有强制只有ExtClassLoader能加载,自定义类加载器也能加载。

四、如何破坏双亲委托

1、线程上下文类加载器

线程上下文类加载器其实是一种类加载器传递机制。可以通过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虚拟机》周志明

2、Tomcat中破坏双亲委派的场景

只有官方库java.的类必须由启动类加载器加载,无法破坏,扩展类加载器和应用程序类加载器的双亲委派都是可以破坏的。

知道了理论,还需要根据实际场景,找准破坏双亲委派的位置。可以看看优秀的开源框架中是如何破坏双亲委派的,比如Tomcat:
tomcat破坏双亲委派

Tomcat源码就不贴了,Tomcat中可以部署多个web项目,为了保证每个web项目互相独立,所以不能都由AppClassLoader加载,所以自定义了类加载器WebappClassLoaderWebappClassLoader继承自URLClassLoader,重写了findClassloadClass,并且WebappClassLoader的父类加载器设置为AppClassLoaderWebappClassLoader.loadClass中会先在缓存中查看类是否加载过,没有加载,就交给ExtClassLoaderExtClassLoader再交给BootstrapClassLoader加载;都加载不了,才自己加载;自己也加载不了,就遵循原始的双亲委派,交由AppClassLoader递归加载。

3、一个比较完整的自定义类加载器

一般情况下,自定义类加载器都是继承URLClassLoader,具有如下类关系图:

public class TestClassLoader extends URLClassLoader {
    public TestClassLoader(ClassLoader parent) {
        super(new URL[0], parent);
    }
    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        // 1、先自己的路径找
        Class clazz = null;
        try {
            clazz = findClassInternal(name);
        } catch (Exception e) {
            // Ignore
        }
        if (clazz != null) {
            return clazz;
        }
        // 在 父类路径 找
        return super.findClass(name);
    }

    private Class findClassInternal(String name) throws IOException {
        byte[] data = null;
        try {
            String dir = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\";
            String namePath = name.replaceAll("\\.", "\\\\");
            String classFile = dir + namePath + ".class";
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            FileInputStream fis = new FileInputStream(new File(classFile));
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = fis.read(bytes)) != -1) {
                baos.write(bytes, 0, len);
            }
            data = baos.toByteArray();
            // 字节码加载到 JVM 的方法区,
            // 并在 JVM 的堆区建立一个java.lang.Class对象的实例
            // 用来封装 Java 类相关的数据和方法
            return this.defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            throw e;
        }
    }
    @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);
    }
}


四、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);
}

五、总结

1、java 的类加载,就是获取.class文件的二进制字节码数组并加载到 JVM 的方法区,并在 JVM 的堆区建立一个用来封装 java 类相关的数据和方法的java.lang.Class对象实例。

2、java默认有的类加载器有三个,启动类加载器(BootstrapClassLoader),扩展类加载器(ExtClassLoader),应用程序类加载器(也叫系统类加载器)(AppClassLoader)。类加载器之间存在父子关系,这种关系不是继承关系,是组合关系。如果parent=null,则它的父级就是启动类加载器。启动类加载器无法被java程序直接引用。

3、双亲委派就是类加载器之间的层级关系,加载类的过程是一个递归调用的过程,首先一层一层向上委托父类加载器加载,直到到达最顶层启动类加载器,启动类加载器无法加载时,再一层一层向下委托给子类加载器加载

  1. 加载一个类时,也会加载其父类,如果该类中还引用了其他类,则按需加载,且类加载器都是加载当前类的类加载器。
  2. 双亲委派的目的主要是为了保证java官方的类库\lib加载安全性,不会被开发者覆盖。
  3. \lib 和\lib\ext是java官方核心类库,一般不会去破坏ExtClassLoader及其以上的双亲委派。
  4. 破坏双亲委派有两种方式:第一种,自定义类加载器,必须重写findClassloadClass;第二种是通过线程上下文类加载器的传递性,让父类加载器中调用子类加载器的加载动作。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值