精华内容
下载资源
问答
  • 主要介绍了java 详解类加载器的双亲委派及打破双亲委派的相关资料,需要的朋友可以参考下
  • NULL 博文链接:https://flyfoxs.iteye.com/blog/2114149
  • 1. 双亲委派就是类加载器之间的层级关系,加载类的过程是一个递归调用的过程,首先一层一层向上委托父类加载器加载,直到到达最顶层启动类加载器,启动类加载器无法加载时,再一层一层向下委托给子类加载器加载。...

    一、前言

    平时做业务开发比较少接触类加载器,但是如果想深入学习Tomcat、Spring等开源项目,或者从事底层架构的开发,了解甚至熟悉类加载的原理是必不可少的。

    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()方法获取应用程序类加载器,所以也可称它为系统类加载器。应用程序类加载器也是启动类加载器加载的,但是它的父类加载器是扩展类加载器。在一个应用程序中,系统类加载器一般是默认类加载器。

    三、双亲委派机制

    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、为什么要双亲委派?

    双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。

    一个非常明显的目的就是保证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内核设计剖析》汪建,第十三章 公共与隔离的类加载器
    展开全文
  • 双亲委托有个弊端:不能向下委派,不能不委派怎么打破双亲委派机制:(也就是能向下委派和不委派)自定义类加载器(不委派)spi机制(向下委派)打破双亲委派打破双亲委派的两种方式:1.通过spi机制,使用ServiceLoad...

    双亲委派机制

    在加载类的时候,会一级一级向上委托,判断是否已经加载,从自定义类加载器-》应用类加载器-》扩展类加载器-》启动类加载器,如果到最后都没有加载这个类,则回去加载自己的类。

    双亲委托有个弊端:

    不能向下委派,不能不委派

    怎么打破双亲委派机制:(也就是能向下委派和不委派)

    自定义类加载器(不委派)

    spi机制(向下委派)

    打破双亲委派

    打破双亲委派的两种方式:

    1.通过spi机制,使用ServiceLoader.load去加载

    2.通过自定义类加载器,继承classloader,重写loadclass方法

    SPI机制

    spi机制是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在JDBC中就使用到了SPI机制。

    为什么通过spi机制就能打破双亲委托?

    因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件。

    以Driver接口为例,DriverManager通过Bootstrap ClassLoader加载进来的,而com.mysql.jdbc.Driver是通过Application ClassLoader加载进来的。由于双亲委派模型,父加载器是拿不到通过子加载器加载的类的。这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派。

    SPI机制demo:

    public interface HelloService {

    public String getName();

    }

    public class Hello1 implements HelloService{

    @Override

    public String getName() {

    return "hello1";

    }

    }

    public class Hello2 implements HelloService{

    @Override

    public String getName() {

    return "hello2";

    }

    }

    来一个main方法去加载它,将使用ServiceLoader来进行加载

    public class SPITest {

    public static void main(String[] args) {

    ServiceLoader serviceLoader = ServiceLoader.load(HelloService.class);

    for (HelloService helloService :serviceLoader){

    System.out.println(helloService.getName());

    }

    }

    }

    配置文件,文件名为接口的全路径,文件内容为实现类的全路径,如我的为:com.chuan.service.Hello1

    输出结果:hello1

    只配置了Hello1,所以只发现了这一个实现类。

    自定义类加载器

    实现逻辑:自定义类继承classLoader,作为自定义类加载器,重写loadClass方法,不让它执行双亲委派逻辑,从而打破双亲委派。

    先看一个没有重写的demo

    结果:

    sun.misc.Launcher$AppClassLoader@58644d46

    发现是app类加载器。

    然后重写loadClass方法

    public class MyClassLoader extends ClassLoader{

    public static void main(String[] args) throws ClassNotFoundException {

    // ServiceLoader.load()

    MyClassLoader myClassLoader = new MyClassLoader();

    Class> aClass = myClassLoader.loadClass(Test1.class.getName());

    System.out.println(aClass.getClassLoader());

    }

    protected Class> findClass(String className) throws ClassNotFoundException {

    System.out.println("My findClass!");

    return null;

    }

    protected Class> loadClass(String name, boolean resolve)

    throws ClassNotFoundException

    {

    synchronized (getClassLoadingLock(name)) {

    Class> c = findLoadedClass(name);

    if (c == null) {

    long t0 = System.nanoTime();

    try {

    //修改classloader的原双亲委派逻辑,从而打破双亲委派

    if (name.startsWith("com.chuan")){

    c=findClass(name);

    }

    else {

    c=this.getParent().loadClass(name);

    }

    } catch (ClassNotFoundException e) {

    }

    if (c == null) {

    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;

    }

    }

    }

    class Test1{

    }

    运行报错,因为没有委派给app类加载器,所以找不到加载不了这个类。

    那么新的问题又来了,如果我自定义类记载器和核心类重名怎么办,该怎么加载,又或者我想篡改核心类内容,jvm又是怎么解决的?

    jvm肯定解决了这个问题,openjdk源码在AccessController.doPrivileged

    学名叫做沙箱安全机制,主要作用是:保护核心类,防止打破双亲委派机制,防篡改,如果重名的话就报异常,这里的重名指包名加类名都重复。

    demo:

    package java.lang;

    public class Integer {

    public static void main(String[] args) {

    System.out.println("1");

    }

    }

    运行报错:

    错误: 在类 java.lang.Integer 中找不到 main 方法, 请将 main 方法定义为:

    public static void main(String[] args)

    否则 JavaFX 应用程序类必须扩展javafx.application.Application

    标签:委派,String,示例,public,双亲,class,加载

    来源: https://blog.csdn.net/qq_39404258/article/details/112065471

    展开全文
  • 一、前言笔者曾经阅读过周志明的《深入理解Java虚拟机》这本书,阅读完后自...前段时间,笔者同事提出了一个关于类加载器破坏双亲委派的问题,以我们常见到的数据库驱动Driver为例,为什么要实现破坏双亲委派,下面...

    一、前言

    笔者曾经阅读过周志明的《深入理解Java虚拟机》这本书,阅读完后自以为对jvm有了一定的了解,然而当真正碰到问题的时候,才发现自己读的有多粗糙,也体会到只有实践才能加深理解,正应对了那句话——“Talk is cheap, show me the code”。前段时间,笔者同事提出了一个关于类加载器破坏双亲委派的问题,以我们常见到的数据库驱动Driver为例,为什么要实现破坏双亲委派,下面一起来重温一下。

    二、双亲委派

    想要知道为什么要破坏双亲委派,就要先从什么是双亲委派说起,在此之前,我们先要了解一些概念:

    对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性。

    什么意思呢?我们知道,判断一个类是否相同,通常用equals()方法,isInstance()方法和isAssignableFrom()方法。来判断,对于同一个类,如果没有采用相同的类加载器来加载,在调用的时候,会产生意想不到的结果:

    public class DifferentClassLoaderTest {

    public static void main(String[] args) throws Exception {

    ClassLoader classLoader = new ClassLoader() {

    @Override

    public Class> loadClass(String name) throws ClassNotFoundException {

    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";

    InputStream stream = getClass().getResourceAsStream(fileName);

    if (stream == null) {

    return super.loadClass(name);

    }

    try {

    byte[] b = new byte[stream.available()];

    // 将流写入字节数组b中

    stream.read(b);

    return defineClass(name, b, 0, b.length);

    } catch (IOException e) {

    e.printStackTrace();

    }

    return super.loadClass(name);

    }

    };

    Object obj = classLoader.loadClass("jvm.DifferentClassLoaderTest").newInstance();

    System.out.println(obj.getClass());

    System.out.println(obj instanceof DifferentClassLoaderTest);

    }

    }

    输出结果:

    class jvm.DifferentClassLoaderTest

    false

    如果在通过classLoader实例化的使用,直接转化成DifferentClassLoaderTest对象:

    DifferentClassLoaderTest obj = (DifferentClassLoaderTest) classLoader.loadClass("jvm.DifferentClassLoaderTest").newInstance();

    就会直接报java.lang.ClassCastException:,因为两者不属于同一类加载器加载,所以不能转化!

    2.1、为什么需要双亲委派

    基于上述的问题:如果不是同一个类加载器加载,即时是相同的class文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采用了双亲委派的方式来加载类。

    双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

    这里有几个流程要注意一下:

    子类先委托父类加载

    父类加载器有自己的加载范围,范围内没有找到,则不加载,并返回给子类

    子类在收到父类无法加载的时候,才会自己去加载

    jvm提供了三种系统加载器:

    启动类加载器(Bootstrap ClassLoader):C++实现,在java里无法获取,负责加载/lib下的类。

    扩展类加载器(Extension ClassLoader): Java实现,可以在java里获取,负责加载/lib/ext下的类。

    系统类加载器/应用程序类加载器(Application ClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。

    附上三者的关系:

    88d5d0bdf9336c835781f7996d5fc4a7.png

    2.2、双亲委派的实现

    双亲委派的实现其实并不复杂,其实就是一个递归,我们一起来看一下ClassLoader里的代码:

    protected Class> loadClass(String name, boolean resolve)

    throws ClassNotFoundException

    {

    // 同步上锁

    synchronized (getClassLoadingLock(name)) {

    // 先查看这个类是不是已经加载过

    Class> c = findLoadedClass(name);

    if (c == null) {

    long t0 = System.nanoTime();

    try {

    // 递归,双亲委派的实现,先获取父类加载器,不为空则交给父类加载器

    if (parent != null) {

    c = parent.loadClass(name, false);

    // 前面提到,bootstrap classloader的类加载器为null,通过find方法来获得

    } else {

    c = findBootstrapClassOrNull(name);

    }

    } catch (ClassNotFoundException e) {

    }

    if (c == null) {

    // 如果还是没有获得该类,调用findClass找到类

    long t1 = System.nanoTime();

    c = findClass(name);

    // jvm统计

    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);

    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);

    sun.misc.PerfCounter.getFindClasses().increment();

    }

    }

    // 连接类

    if (resolve) {

    resolveClass(c);

    }

    return c;

    }

    }

    三、破坏双亲委派

    3.1、为什么需要破坏双亲委派?

    因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。

    3.2、破坏双亲委派的实现

    我们结合Driver来看一下在spi(Service Provider Inteface)中如何实现破坏双亲委派。

    先从DriverManager开始看,平时我们通过DriverManager来获取数据库的Connection:

    String url = "jdbc:mysql://localhost:3306/testdb";

    Connection conn = java.sql.DriverManager.getConnection(url, "root", "root");

    在调用DriverManager的时候,会先初始化类,调用其中的静态块:

    static {

    loadInitialDrivers();

    println("JDBC DriverManager initialized");

    }

    private static void loadInitialDrivers() {

    ...

    // 加载Driver的实现类

    AccessController.doPrivileged(new PrivilegedAction() {

    public Void run() {

    ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);

    Iterator driversIterator = loadedDrivers.iterator();

    try{

    while(driversIterator.hasNext()) {

    driversIterator.next();

    }

    } catch(Throwable t) {

    }

    return null;

    }

    });

    ...

    }

    为了节约空间,笔者省略了一部分的代码,重点来看一下ServiceLoader.load(Driver.class):

    public static ServiceLoader load(Class service) {

    // 获取当前线程中的上下文类加载器

    ClassLoader cl = Thread.currentThread().getContextClassLoader();

    return ServiceLoader.load(service, cl);

    }

    可以看到,load方法调用获取了当前线程中的上下文类加载器,那么上下文类加载器放的是什么加载器呢?

    public Launcher() {

    ...

    try {

    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

    } catch (IOException var9) {

    throw new InternalError("Could not create application class loader", var9);

    }

    Thread.currentThread().setContextClassLoader(this.loader);

    ...

    }

    在sun.misc.Launcher中,我们找到了答案,在Launcher初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,而这个AppClassLoader,就是之前上文提到的系统类加载器Application ClassLoader,所以上下文类加载器默认情况下就是系统加载器。

    继续来看下ServiceLoader.load(service, cl):

    public static ServiceLoader load(Class service,

    ClassLoader loader){

    return new ServiceLoader<>(service, loader);

    }

    private ServiceLoader(Class svc, ClassLoader cl) {

    service = Objects.requireNonNull(svc, "Service interface cannot be null");

    // ClassLoader.getSystemClassLoader()返回的也是系统类加载器

    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;

    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;

    reload();

    }

    public void reload() {

    providers.clear();

    lookupIterator = new LazyIterator(service, loader);

    }

    上面这段就不解释了,比较简单,然后就是看LazyIterator迭代器:

    private class LazyIterator implements Iterator{

    // ServiceLoader的iterator()方法最后调用的是这个迭代器里的next

    public S next() {

    if (acc == null) {

    return nextService();

    } else {

    PrivilegedAction action = new PrivilegedAction() {

    public S run() { return nextService(); }

    };

    return AccessController.doPrivileged(action, acc);

    }

    }

    private S nextService() {

    if (!hasNextService())

    throw new NoSuchElementException();

    String cn = nextName;

    nextName = null;

    Class> c = null;

    // 根据名字来加载类

    try {

    c = Class.forName(cn, false, loader);

    } catch (ClassNotFoundException x) {

    fail(service,

    "Provider " + cn + " not found");

    }

    if (!service.isAssignableFrom(c)) {

    fail(service,

    "Provider " + cn + " not a subtype");

    }

    try {

    S p = service.cast(c.newInstance());

    providers.put(cn, p);

    return p;

    } catch (Throwable x) {

    fail(service,

    "Provider " + cn + " could not be instantiated",

    x);

    }

    throw new Error(); // This cannot happen

    }

    public boolean hasNext() {

    if (acc == null) {

    return hasNextService();

    } else {

    PrivilegedAction action = new PrivilegedAction() {

    public Boolean run() { return hasNextService(); }

    };

    return AccessController.doPrivileged(action, acc);

    }

    }

    private boolean hasNextService() {

    if (nextName != null) {

    return true;

    }

    if (configs == null) {

    try {

    // 在classpath下查找META-INF/services/java.sql.Driver名字的文件夹

    // private static final String PREFIX = "META-INF/services/";

    String fullName = PREFIX + service.getName();

    if (loader == null)

    configs = ClassLoader.getSystemResources(fullName);

    else

    configs = loader.getResources(fullName);

    } catch (IOException x) {

    fail(service, "Error locating configuration files", x);

    }

    }

    while ((pending == null) || !pending.hasNext()) {

    if (!configs.hasMoreElements()) {

    return false;

    }

    pending = parse(service, configs.nextElement());

    }

    nextName = pending.next();

    return true;

    }

    }

    好了,这里基本就差不多完成整个流程了,一起走一遍:

    dce4e5f6195789e9138714d13339bd7f.png

    四、总结

    Driver剩余的加载过程就省略了,有兴趣的园友可以继续深入了解一下,不得不说,jvm博大精深,看起来容易,真正到了用起来才发现各种问题,也只有实践才能加深理解,最后谢谢各位园友观看,如果有描述不对的地方欢迎指正,与大家共同进步!

    参考部分:

    展开全文
  • 双亲委派机制以及打破双亲委派机制 双亲委派机制 Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存中生成class对象,而且加载某个类的class文件时,Java虚拟机...

    双亲委派机制以及打破双亲委派机制

    双亲委派机制

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

    双亲委派机制的优点:

    • 1、避免类的重复加载
    • 2、保护程序的安全性,防止核心API随意被篡改
      在这里插入图片描述
      在这里插入图片描述

    沙箱安全机制:保证对Java信息源代码的保护

    何为沙箱安全机制?即如果我们自己建立和源代码相同的包,例如java/lang/String.Class,在我们去使用类加载器去加载此类时,为了防止你自定义的类对源码的破坏,所以他默认不是使用你的String类的本身的系统加载器去加载它,而是选择率先使用引导类加载器去加载,而引导类在加载的过程中会先去加载JDK自带的文件(rt.jar包中的java/lang/String.class),而不是你自己定义的String.class,报错信息会提示没有main方法 ,就是因为加载的是rt.jar包下的String类,这样就可以做到保证对java核心源代码的保护,这即是沙箱保护机制.

    那么双亲委派机制存在哪些缺点,又有哪些打破双亲委派机制的例子呢?

    通过上面双亲委派机制的特点,我们知道了以下结论:

    由于BootStrapClassLoader是顶级类加载器,并且它不能使用AppClassLoader加载器加载的类,因为BootstrapClassloader无法委派AppClassLoader来加载类。故当我们的上层类加载器需要下层类加载器帮忙加载类,就会出现问题,这便是双亲委派机制的缺点,那么哪些场景发生了这些事情,而Java又是如何打破这种双亲委派机制的呢?

    我们来举一个经典的打破双亲委派机制的例子:Java SPI机制

    SPI(Service Provider Interface),主要是应用于厂商自定义组件或插件中。

    java SPI提供一种服务发现的机制,即为某个接口寻找服务实现的机制,我们在需要在jar包的META-INF/services/目录里面创建一个一服务接口命令的文件,而该文件就是实现这个接口的具体实现类,当有外部程序需要装配我们的这个模块接口的时候,就可以通过META-INF/services/目录的配置文件找到对应的实现类的类名,并将其实例化,完成该接口模块的注入。而在JDK中,也提供了一个服务实现查找的一个工具类:java.util.ServiceLoader。

    我们以数据库连接jar包为例:mysql-connector-java-5.1.37.jar,它遵循SPI的规范

    在这里插入图片描述

    我们的java.sql.Driver接口定义在java.sql包下,而该包的位置位于jdk\jre\lib\rt.jar,其中java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类等等,这样我们就很清楚地知道了java.sql是由BootStrapClassLoader加载器加载的

    而我们的接口的实现类com.mysql.jdbc.Driver是第三方实现的类库,是由AppClassLoader加载器加载的

    那么问题来了:我们使用DriverManager获取Connection的时候,必然会加载到com.mysql.jdbc.Driver类,此时即是BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖!!!

    JVM采取了一种作弊的方式打破了双亲委派机制,从而完成了上层类加载器使用下层类加载器进行加载的类:

    在BootstrapClassLoader或ExtClassLoader加载的类A中,如果使用到AppClassLoader类加载器加载的类B,由于双亲委托机制不能向下委托,那可以在类A中通过线程上下文类加载器获得AppClassLoader,从而去加载类B

    我们通过源码来看一下:

    首先我们创建一个web项目,然后导入mysql的驱动包,我们编写一下测试代码:

    public class TestClassLoader {
        public static void main(String[] args) throws SQLException, ClassNotFoundException {
            String driverClassName = "com.mysql.jdbc.Driver";
            String url = "jdbc:mysql://localhost:3306/mybatis";
            String username = "root";
            String password = "root";
            Connection connection = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
    
            //执行驱动"com.mysql.jdbc.Driver"
            Class.forName("com.mysql.jdbc.Driver");
    
            //获取连接
            connection = DriverManager.getConnection(url,username,password);
        }
    }
    

    此时我们知道这段代码肯定是能正常执行了,第一步加载我们的mysql的驱动,然后利用DriverManager获取连接

    那么我们将第一步的执行驱动代码注释掉,你猜猜还会执行成功吗??
    根据上面我们说的,答案是肯定的!!!我们进入源码探究一下

    DriverManager类的内部存在一个静态代码块,其中有一个**loadInitialDrivers()**方法:

    在这里插入图片描述

    我们点进去,里面有一行代码:

    //JDK中提供的ServiceLoader来实现服务实现查找功能
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    

    在这里插入图片描述

    我们点进去这个方法看看:

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

    这段代码便是打破双亲委派机制,实现com.mysql.jdbc.Driver驱动类加载的核心,即我们获取线程上下文加载器Thread.currentThread().getContextClassLoader(),然后利用线程上下文类加载器进行mysql驱动的类加载,以达到打破双亲委派机制的作用

    PS:此时如果我们改变线程上下文类加载器,就会使得此时程序出现错误,大家也就明白了其中的流程和原理了

    public class TestClassLoader {
        public static void main(String[] args) throws SQLException, ClassNotFoundException {
            String driverClassName = "com.mysql.jdbc.Driver";
            String url = "jdbc:mysql://localhost:3306/mybatis";
            String username = "root";
            String password = "root";
            Connection connection = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
    
            //改变当前线程上下文类加载器为当前类的扩展类加载器
            Thread.currentThread().setContextClassLoader(TestClassLoader.class.getClassLoader().getParent());
            
            //执行驱动"com.mysql.jdbc.Driver"
            //Class.forName("com.mysql.jdbc.Driver");
    
            //获取连接
            connection = DriverManager.getConnection(url,username,password);
        }
    }
    

    执行结果:

    Exception in thread "main" java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/mybatis
    	at java.sql.DriverManager.getConnection(DriverManager.java:689)
    	at java.sql.DriverManager.getConnection(DriverManager.java:247)
    	at it.feng.test.TestClassLoader.main(TestClassLoader.java:25)
    
    展开全文
  • 双亲委派

    2021-02-16 16:37:06
    1、什么是双亲委派? 2、为什么需要双亲委派,不委派有什么问题? 3、”父加载器”和”子加载器”之间的关系是继承的吗? 4、双亲委派是怎么实现的? 5、我能不能主动破坏这种双亲委派机制?怎么破坏? 6、为什么...
  • 双亲委派模型和破坏性双亲委派模型详解

    千次阅读 多人点赞 2019-06-17 12:28:45
    由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。...
  • 双亲委派机制        第一次知道何为打破双亲委派机制是通过阅读周志明的《深入理解Java虚拟机》,我们知道双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给...
  • 主要介绍了JVM的类加载过程以及双亲委派模型详解,类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。,需要的朋友可以参考下
  • 参考文章: 1.双亲委派模型的破坏(JDBC例子) ... 2.面试官:说说双亲委派模型? ...wfr=spider&for=pc ...3.【JVM】浅谈双亲委派和破坏双亲委派 https://www.cnblogs.com/joemsu/p/9310226.html ...
  • 一、什么是双亲委派机制? 当某个特定的类加载器它在接到需要加载类的请求时,这个类会首先查看自己已加载完的类中是否包含这个类,如果有就返回,没有的话就会把加载的任务交给父类加载器加载,以此递归,父类加载...
  • 双亲委派模式 双亲委派模式的实现是依赖于 ClassLoader抽象类定义的loadClass() 方法。 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized ...
  • 1 双亲委派(又称双亲委托) 作用:保证类加载的有序性和安全性 类加载器的分类: 作用:保证JVM的核心类和用户的类都能得到正常加载 双亲委派机制: 向上委派:当类加载器加载一个类时,首先会向他的父类加载...
  • 双亲委派机制 要了解双亲委派,必须得知道类加载器,类加载器是用来把class文件加载进内存的 类加载器3层架构 启动类加载器:C++实现,是虚拟机的一部分,java虚拟机能够识别,无法被java程序直接引用(null)。负责...
  • 最近一段时间,我在面试的过程中,很喜欢问双亲委派的一些问题,因为我发现这个问题真的可以帮助我全方位的了解一个候选人。记得前几天一次面试过程中,我和一位候选人聊到了JVM的类加载机制的问题,他谈到了双亲...
  • 目录什么是双亲委派?为什么要破坏双亲委派?如何破坏?使用双亲委派的好处如何破坏双亲委派破坏双亲委派的例子什么是SPI机制SPI的优缺点每日一皮 什么是双亲委派?为什么要破坏双亲委派?如何破坏? 我们都知道,类...
  • 本文讲解JAVA双亲委派机制及类加载过程
  • 双亲委派机制

    2020-10-28 23:25:10
    3.2.2. 双亲委派机制 1)双亲委派介绍 双亲委派的理念:在当前类加载器的缓存中寻找是否加载过该类,没有则向父加载器中继续寻找,一直到顶层加载器为止,如果没有则再交由子加载器处理,一直到自定义类加载器为止,...
  • JVM - 彻底理解打破双亲委派机制

    千次阅读 多人点赞 2020-06-12 00:11:41
    文章目录Pre 双亲委派何为打破双亲委派演示 Pre 双亲委派 JVM-白话聊一聊JVM类加载和双亲委派机制源码解析 JVM - 自定义类加载器 何为打破双亲委派 举个例子 有个类 Artisan 我们希望通过自定义加载器 直接从某个...
  • SPI如何破坏双亲委派模型 # 双亲委派# SPI 类加载器(classloader) 先从类加载器说起,凡事先问是什么,首先什么是类加载器? 我们知道,一个 *.java 的代码源文件要执行起来之前,必须通过 javac 构建抽象...
  • JVM成神之路-类加载机制-双亲委派,破坏双亲委派

    万次阅读 多人点赞 2018-08-21 09:24:50
    概述 概念 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接时候用的Java类型。 类的生命周期 类从被加载到虚拟机内存中开始,到卸载出内存为止,它...
  • Java双亲委派模型是什么、优势在哪、双亲委派模型的破坏 前言 双亲委派模型是Java加载类的机制.采用双亲委派模型的好处是Java类随着它的类加载器一起具备了一种带有优先级的层级关系,通过这种层级关系可以避免类...
  • 详解双亲委派的实现原理概述JVM默认的3类类加载器加载器的优先级双亲委派模式(Parent delegate)ClassLoader 委派如何工作的打破规则重写loadClass自定义线程上下文类加载器热加载类加载器的基本原理 概述 ...
  • 一. 类的加载 java class是由class loader 类加载器加载到JVM内存中的。 看下下面的demo,java中有三种类... 双亲委派机制 2. 为什么采用双亲委派机制 这种机制能保证系统级别的类不会被用户覆盖,防止危险代码的植入。
  • 【高级开发进阶】1.1.3 双亲委派模型及如何打破

    千次阅读 多人点赞 2021-02-03 20:24:19
    首先得知道什么是双亲委派模型?为什么要打破它?打破它用途是什么? 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。 上面所介绍的这几种类加载器的层次关系,称为类加载...
  • 手写破坏双亲委派 其实在上述案例中大概讲述了,我们阐述了mysql打破双亲委派是如何做到的,那么我们是不是可以自己尝试一下打破一次双亲委派呢? 首先,我们需要先创建一个只会被Extention类加载器加载的类...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 36,723
精华内容 14,689
关键字:

双亲委派