JVM要双亲委派的原因及如何打破它

 更新时间:2021年06月08日 09:28:33   作者:徐同学呀  
平时做业务开发比较少接触类加载器,但是如果想深入学习,了解类加载的原理是必不可少的.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()方法获取应用程序类加载器,所以也可称它为系统类加载器。应用程序类加载器也是启动类加载器加载的,但是它的父类加载器是扩展类加载器。在一个应用程序中,系统类加载器一般是默认类加载器。

二、双亲委派机制

2.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.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了。)

defineClass

preDefineClass

通过自定义类加载器破坏双亲委派的案例在日常开发中非常常见,比如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内核设计剖析》汪建,第十三章 公共与隔离的类加载

到此这篇关于JVM要双亲委派的原因及如何打破它的文章就介绍到这了,更多相关JVM双亲委派内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • groovy脚本定义结构表一键生成POJO类

    groovy脚本定义结构表一键生成POJO类

    这篇文章主要为大家介绍了groovy脚本定义结构表一键生成POJO类示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • 基于Spring中的线程池和定时任务功能解析

    基于Spring中的线程池和定时任务功能解析

    下面小编就为大家带来一篇基于Spring中的线程池和定时任务功能解析。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Java实现开箱即用的redis分布式锁

    Java实现开箱即用的redis分布式锁

    这篇文章主要为大家详细介绍了如何使用Java实现开箱即用的基于redis的分布式锁,文中的示例代码讲解详细,具有一定的借鉴价值,需要的可以收藏一下
    2022-12-12
  • java分割字符串多种方法(附例子)

    java分割字符串多种方法(附例子)

    这篇文章主要给大家介绍了关于java分割字符串多种方法的相关资料,Java中有多种方法可以实现字符串分割,文中将每张方法都给出了代码示例,需要的朋友可以参考下
    2023-10-10
  • Springboot如何优雅地进行字段校验

    Springboot如何优雅地进行字段校验

    这篇文章主要给大家介绍了关于Springboot如何优雅地进行字段校验的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • Java利用反射自动封装成实体对象的方法

    Java利用反射自动封装成实体对象的方法

    这篇文章主要介绍了Java利用反射自动封装成实体对象的方法,可实现自动封装成bean对象功能,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-01-01
  • Java8函数式编程应用小结

    Java8函数式编程应用小结

    Java8非常重要的就是引入了函数式编程的思想,使得这门经典的面向对象语言有了函数式的编程方式,弥补了很大程度上的不足,函数式思想在处理复杂问题上有着更为令人称赞的特性,本文给大家介绍Java8函数式编程应用小结,感兴趣的朋友一起看看吧
    2023-12-12
  • 教你使用springboot配置多数据源

    教你使用springboot配置多数据源

    发现有很多小伙伴还不会用springboot配置多数据源,今天特地给大家整理了本篇文章,文中有非常详细的图文介绍及代码示例,对正在学习java的小伙伴很有帮助,需要的朋友可以参考下
    2021-05-05
  • Java实现一个简单的定时器代码解析

    Java实现一个简单的定时器代码解析

    这篇文章主要介绍了Java实现一个简单的定时器代码解析,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12
  • 一文解析Apache Avro数据

    一文解析Apache Avro数据

    本文是avro解析的demo,当前FlinkSQL仅适用于简单的avro数据解析,复杂嵌套avro数据暂时不支持。本文主要解析Apache Avro数据的相关内容,感兴趣的朋友一起看看吧
    2021-12-12

最新评论