Skip to content

Latest commit

 

History

History
212 lines (150 loc) · 13 KB

类加载器.md

File metadata and controls

212 lines (150 loc) · 13 KB

image

类加载器的分类

JVM支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个

image

这里的四者之间是包含关系,不是上层和下层,也不是子系统的继承关系。

我们通过一个类,获取它不同的加载器

public class ClassLoaderDemo {
    static {
        System.out.println("classLoaderDemo static is start");
    }
    public static void main(String[] args) throws ClassNotFoundException {

        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);

        // 获取其上层的:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);

        // 试图获取 根加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);

        // 获取自定义加载器
        ClassLoader classLoader = ClassLoaderDemo.class.getClassLoader();
        System.out.println(classLoader);

        // 获取String类型的加载器
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);

        // 数组的加载器类型与数组的类型一致,引用类型需要类加载器加载
        String[] arrStr = new String[10];
        ClassLoader classLoader2 = arrStr.getClass().getClassLoader();
        System.out.println(classLoader2);

        // 它的加载器为null  意味着它不需要加载  基本数据类型由虚拟机预先定义,
        int[] arr = new int[10];
        ClassLoader classLoader3 = arr.getClass().getClassLoader();
        System.out.println(classLoader3);
    }
}

结果:

获取系统类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
扩展类加载器:sun.misc.Launcher$ExtClassLoader@45ff54e6
根加载器:null
获取自定义加载器sun.misc.Launcher$AppClassLoader@18b4aac2
获取String类型的加载器:null
引用类型数组加载器:null // 它是使用的启动类加载器
基本数据类型加载器null  // 它是不需要类加载器

这里有一个重点是数组的类加载器是谁?

image

与数组当中元素类型的类加载器是一样的:如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的,由虚拟机预先定义。如果是引用类型元素那么它的加载器就是引用类型的类加载器

用户自定义类加载器

为什么要自定义类加载器?

  • 隔离加载类
  • 修改类加载的方式
  • 扩展加载源
  • 防止源码泄漏

用户自定义类加载器实现步骤

  • 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求

在自定义 ClassLoader 的子类时候,我们常见的会有两种做法:

  • 方式一:重写 loadClass() 方法
  • 方式二:重写 findClass() 方法

这两种方法本质上差不多,毕竟 loadClass() 也会调用 findClass(),但是从逻辑上讲我们最好不要直接修改 loadClass() 的内部逻辑。建议的做法是只在findClass() 里重写自定义类的加载方法,根据参数指定类的名字,返回对应的 Class 对象的引用

  • loadClass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写loadClass() 方法的过程中必须写双亲委托的重复代码,从代码的复用性来看, 不直接修改这个方法始终是比较好的选择
  • 当编写好自定义类加载器后,便可以在程序中调用 loadClass() 方法来实现 类加载操作

关于ClassLoader

ClassLoader 是 Java 的核心组件,所有的 Class 都是由 ClassLoader 进行加载的,ClassLoader负责通过各种方式将 Class 信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过 ClassLoader 去改变类的链接和初始化行为。至 于它是否可以运行,则由 Execution Engine 决定

image

获取ClassLoader的途径

  • 获取当前ClassLoader:clazz.getClassLoader()
  • 获取当前线程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
  • 获取系统的ClassLoader:ClassLoader.getSystemClassLoader()
  • 获取调用者的ClassLoader:DriverManager.getCallerClassLoader()
双亲委派机制

Java虚拟机对class文件采用的是按需加载的方式也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

工作原理

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行; 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器; 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

image

通过loadClass源码的剖析来看双亲委派模型的实现

测试代码:

ClassLoader.getSystemClassLoader().loadClass("com.lcyanxi.java.User")

双亲委派模型在代码中的实现

// resolve = true 加载class的同时进行解析操作
protected Class<?> loadClass(String name, boolean resolve){
    synchronized (getClassLoadingLock(name)) { //同步操作,保证只能加载一次
        // 首先 在缓存中判断是否已经加载类同名的类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 递归调用获取当前类加载器的父加载器
                if (parent != null) {
                    // 如果存在父加载器,则父加载器加载
                    c = parent.loadClass(name, false);
                } else {// parent == null说明是引导类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {}

            if (c == null) { //当前类的加载器的父加载器未加载此类 or 当前类的加载器未加载此类
                // 调用当前classLoader 的findClass
                long t1 = System.nanoTime();
                c = findClass(name);
                // ........
            }
        }
        // 是否进行解析操作,也就是是否进行类初始化操作
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
Class.forName 与 ClassLoader.loadClass有什么区别
  • Class.forName:是一个静态方法,最常用的是 Class.forName;根据传入的类的权限定名返回一个 Class 对象。该方法在将Class文件加载到内存的同时,会执行类的初始化。

如:Class.forName("com.atguigu.java.HelloWorld");

image

  • ClassLoader.loadClass:这是一个实例方法,需要一个 ClassLoader 对象来调 用该方法。该方法将Class 文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器

image

破坏双亲委派机制场景
  • Java的SPI机制对双亲委派模型的破坏-线程上下文类加载器

双亲委派模型的第二次"被破坏"是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载)。

基础类型之所以被称为"基础",是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户代码,那该怎么办?

这并非是不可能出现的事情,一个典型的例子便是 JNDI 服务,JNDI 现在已经是 Java 的标准服务,它的代码由启动类加载器来完成加载(在 JDK 1.3 时加入到 rt.jar),肯定属于 Java 中很基础的类型了。但 JNDI 存在的目的就是对资源进行查找和集中管理,它需要调用由其它厂商实现并部署在应用程序的 ClassPath 下的 JNDI 服务提供者接口(Service Provider Interface. SPI)的代码。

现在问题来了,启动类加载器时绝对不可能认识、加载这些代码的,那该怎么办?(SPI:在 Java 平台中,通常把核心类 rt.jar 中提供外部服务、可由应用层自行实现的接口称为 SPI)。

为了解决这个困境,Java 的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader这个类加载器可以通过java.lang.Thread 类的 setContextClassLoader() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器

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

Java中涉及SPI的加载基本上都采用这种方式来完成,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的方式,在 JDK 6 时,JDK 提供了 java.util.ServiceLoader 类,以 META-INF/Services 中的配置信息,辅以责任链模式,这才算是给 SPI 的加载提供了一种相对合理的解决方案。

image

  • 热部署机制对双歧委派模型的破坏
沙箱安全机制

自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制

双亲委派机制的优势

通过上面的例子,我们可以知道,双亲机制可以

  • 避免类的重复加载
  • 保护程序安全,防止核心API被随意篡改
    1. 自定义类:java.lang.String
    2. 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
如何判断两个class对象是否相同

在JVM中表示两个class对象是否为同一个类存在两个必要条件:

  • 类的完整类名必须一致,包括包名。
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

换句话说,在JvM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。