纯净、安全、绿色的下载网站

首页|软件分类|下载排行|最新软件|IT学院

当前位置:首页IT学院IT技术

JVM双亲委派 JVM要双亲委派的原因及怎样打破它

徐同学呀   2021-06-08 我要评论
想了解JVM要双亲委派的原因及怎样打破它的相关内容吗徐同学呀在本文为您仔细讲解JVM双亲委派的相关知识和一些Code实例欢迎阅读和指正我们先划重点:JVM双亲委派的原因,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接口提供者(SPIService 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内核设计剖析》汪建第十三章 公共与隔离的类加载

相关文章

猜您喜欢

网友评论

Copyright 2020 www.fresh-weather.com 【世纪下载站】 版权所有 软件发布

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 点此查看联系方式