zl程序教程

您现在的位置是:首页 >  后端

当前栏目

JAVA双亲委派机制、不适用的场景及类加载过程

JAVA 过程 机制 加载 场景 适用 委派 双亲
2023-09-11 14:16:24 时间

1 双亲委派

1.1 什么是双亲委派

The Java Class Loading Mechanism
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
详见 Understanding Extension Class Loading

Parents Delegation Model中一个类加载器收到类加载的请求,递归地把这个请求委派给父类加载器完成。当父加载器找不到指定的类时,子加载器尝试自己加载

1.2 类加载器的类别

在这里插入图片描述

1.2.1 类加载器类图

在这里插入图片描述

1.2.2 BootstrapClassLoader

BootstrapClassLoader(引导类加载器):用来加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar,sun.boot.class.path路径下的内容),是用原生代码(C语言)来实现的,并不继承自 java.lang.ClassLoader。
特点:

  • c++编写
  • 加载java核心库 java.*,如rt.jar中的类
  • 加载ExtClassLoader和AppClassLoader,并指定他们的父类加载器
  • 由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到引导类加载器的引用,所以不允许直接通过引用进行操作。

1.2.3 ExtClassLoader

ExtClassLoader (标准扩展类加载器)的特点:

  • java编写
  • 用来加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar,或java.ext.dirs路径下的内容) 。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java类
  • 由sun.misc.Launcher$ExtClassLoader实现

1.2.4 AppClassLoader

AppClassLoader(系统类加载器)的特点:

  • java编写
  • 根据 Java 应用的类路径(classpath,java.class.path 路径下的内容)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的
  • 由sun.misc.Launcher$AppClassLoader实现

1.2.5 CustomClassLoader

CustomClassLoader(用户自定义类加载器)的特点:

  • java编写
  • 开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求
  • 可加载指定路径的class文件

1.3 类查找优先列表

扩展框架使用了类加载委托机制。当运行时环境需要为应用程序加载一个新类时,它会按优先顺序在以下位置查找这个类:

  • 引导【Bootstrap 】类: rt.jar中的运行时【runtime】类、i18n.jar中的国际化类以及其他类。
  • 已安装的扩展:JRE的lib/ext目录中的JAR文件中的类,以及系统范围内、特定于平台的扩展目录中的类(例如Solaris™操作系统上的/usr/jdk/packages/lib/ext,但请注意,此目录的使用仅适用于Java™6和更高版本)。
  • 类路径(CLASSPATH):在系统属性java.class.path指定的路径上的类,包括JAR文件中的类。如果类路径上的JAR文件具有具有Class-Path属性的清单,那么也将搜索由Class-Path属性指定的JAR文件。默认情况下,java.class.path属性的值是当前目录。可以使用 -classpath或-cp命令行选项或设置CLASSPATH环境变量来更改该值命令行选项覆盖CLASSPATH环境变量的设置

例如,优先级列表告诉您,只有在rt.jar、i18n.jar或安装的扩展中没有找到要加载的类时,才会搜索类路径。

1.4 类加载源码分析

  • ClassLoader及其子类中的构造函数允许您在实例化新的类装入器时指定父类。如果没有显式指定父类,虚拟机的系统类装入器将被指定为默认父类。
  • ClassLoader的loadClass方法
    • 如果类已经装入,则返回该类。
    • 否则,它将新类的搜索委托给父类装入器。
    • 如果父类装入器没有找到该类,loadClass将调用findClass方法来查找并装入该类。
  • ClassLoader的findClass方法如果父类装入器没有找到该类,ClassLoader的findClass方法将在当前类装入器中搜索该类。在应用程序中实例化类装入器子类时,可能需要重写此方法。
  • 类java.net.URLClassLoader充当扩展和其他JAR文件的基本类装入器,覆盖java.lang.ClassLoader的findClass方法,以搜索一个或多个指定的url来查找类和资源
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
             // 首先检查这个class是否已经加载过了
            Class<?> c = findLoadedClass(name);
            //如果类没有被加载过
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                     //如果有父加载器则让父加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    	//如果父加载器为空 则说明递归到bootStrapClassloader了
                        //使用bootStrapClassloader加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //如果类没有被,则递归回来,尝试自己去加载class
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    //记录类加载的一些统计信息
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

1.5 类委派加载流程图

在这里插入图片描述

1.6 双亲委派模型缺陷及不适用的场景

双亲委派模型很好的解决了各个类加载器的基础类统一问题(越基础的类由越上层的类加载器加载),基础类之所以称为基础,是因为它们总是作为被用户调用的API,但如果基础类又要回调用户的代码,那该怎么办呢。

假定类加载器 A 作为 B 的 parent,A 加载的类 对 B 是可见的; 然而 B 加载的类 对 A 却是不可见的。这是由 classloader 加载模型中的可见性(visibility)决定的。可见性原则允许子类加载器查看父ClassLoader加载的所有类,但父类加载器看不到子类加载器的类。

双亲委派模型不适用场景:
常见的 SPI 有 JDBC、JNDI、JAXP 等,这些 SPI 的接口由核心类库提供,却由第三方实现,这样就存在一个问题:

  • SPI 的接口是 Java 核心库的一部分,由 BootstrapClassLoader 加载;并且接口的调用逻辑也是在核心库
  • SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。
  • 根据ClassLoader加载模型中的可见性原则,BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能将加载任务委派给 AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。

破坏双亲委派模型的典型示例:

  • JNDI服务: JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI service provider interface)的代码,但启动类加载器不可能认识这些代码
  • SPI 接口如 java.sql.DriverManager, java.sql.DriverManager 通过扫包的方式拿到指定的实现类,完成 DriverManager的初始化。问题来了,根据双亲委派的可见性原则,启动类加载器加载的 DriverManager 是不可能拿到系统应用类加载器加载的实现类

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

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

在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类(被线程上下文类加载器加载的类,能够被核心类库的调用方调用,且实现类不是通过双亲委派模型的方式加载,而是直接用传递的线程上下文类加载器加载

SPI ServiceLoader 通过线程上下文获取能够加载实现类的classloader,一般情况下是 application classloader,绕过了双亲委派模型的层级限制,逻辑上打破了双亲委派原则

下面是DriverManager根据配置加载数据库驱动类的源码

//DriverManager.loadInitialDrivers
private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

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

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

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

线程上下文 classloader并非具体的某个loader,一般情况下是 application classloader, 但也可以通过 java.lang.Thread#setContextClassLoader 这个方法指定 classloader。
Thread.currentThread().getClassLoader() 和 Thread.currentThread().getContextClassLoader() 这两者除了在许多底层框架中取得的 ClassLoader 可能会有所不同外,其他大多数业务场景下都是一样的,只要知道它是为了解决什么问题而存在的即可。

1.7 双亲委派的作用

  • 防止重复加载同一个类。父类加载到,子类不会重新加载,保证数据安全。
  • 解决了各个类加载器的基础类统一问题
  • 保证核心类不能被篡改。通过委托方式,不会去篡改核心类,保证了Class执行安全。
    • 即使篡改也不会去加载
    • 即使加载也不会是同一个类对象了。不同的加载器加载同一个类也不是同一个Class对象。

2 类加载

类加载 指将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象
Java中的类加载是一种懒加载的形式**。只有当这个类需要被使用的时候才会去加载,这样做的好处就是避免一次性加载全部,从而占用很大的内存。

2.1 类加载过程

在类加载的过程中,大致会经历以下7个阶段:加载、验证、准备、解析、初始化、使用、卸载。

  • 加载:查找并且加载类的二进制数据
  • 链接:
    • 验证:确保被加载类的正确性 【检查输入的字节流。为了确保当前加载的Class字节流符合虚拟机的要求,不会危害到虚拟机】
    • 准备:为类的静态变量分配内存,并将其初始化为默认值 【给类中静态变量赋予初值,其他的变量随着类的初始化,跟类存放在堆中】
    • 解析:把类中的符号引用转换为直接引用 【将常量池中的符号引用变成直接引用】
  • 初始化:执行构造方法 【初始化是类加载的最后一步,初始化阶段是执行类构造器 ()方法的过程】

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。
在这里插入图片描述

2.2 类卸载

类卸载即该类的Class对象被GC。

卸载类需要满足3个要求:

  • 该类的所有的实例对象都已被GC,也就是说堆不存在该类的实例对象。
  • 该类没有在其他任何地方被引用
  • 该类的类加载器的实例已被GC
    所以,在JVM生命周期类,由jvm自带的类加载器加载的类是不会被卸载的。但是自定义的类加载器加载的类是可能被卸载的。