详解JVM之双亲委派机制

双亲委派机制是Java类加载的核心特性,保证了类的唯一性和安全性。当类加载请求时,先由父类加载器尝试加载,只有父类加载失败时才会由子加载器加载。这种方式避免了类的重复加载,但也存在如系统类无法直接访问应用类的问题。线程上下文类加载器打破了这种机制,允许特定情况下启动类加载器加载应用类加载器的类。
摘要由CSDN通过智能技术生成

双亲委派机制

定义

如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

加载器的分类

在这里插入图片描述

  • 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器。

  • 不同类加载器看似是继承(Inheritance) 关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用

本质

规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4prgy8dl-1677758966594)(双亲委派机制.assets/image-20230301172857234.png)]

在这里插入图片描述

双亲委派机制优势与劣势

双亲委派机制优势
  • 避免类的重复加载,确保一个类的全局唯一性。

    Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子classLoader再加载一次。

  • 保护程序安全,防止核心API被随意算改

    如果定义一个和String相同包的类java.lang.String,在运行时会抛出java.lang.SecurityException异常,起到了保护核心API的作用。

在这里插入图片描述

双亲委派机制劣势

检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类

通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。

源码逻辑

双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)接口中体现。该接口的逻辑如下:

(1)先在当前加载器的缓存中查找有无目标类,如果有,直接返回。

(2)判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadclass(name,false)接口进行加载。

(3)反之,如果当前加载器的父类加载器为空,则调用findBootstrapclassOrNull(name)接口,让引导类加载器进行加载。

(4)如果通过以上3条路径都没能成功加载,则调用findclass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的

defineClass系列的native接口加载目标Java类。双亲委派的模型就隐藏在这第2和第3步中。

源码实现

抽象类classLoader的主要方法:

1. loadClass方法
protected Class<?> loadClass(String name, boolean resolve)

加载名称为name的类,返回结果为java.lang.class类的实例。如果找不到类,则返回 CassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {		//同步操作,保证只能加载一次
        // First, check if the class has already been loaded
        //在缓存中判断是否已经加载同名的类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //获取当前类加载器的父类加载器
                if (parent != null) {
                    //如果存在父类加载器,则调用父类加载器进行类加载,父类加载器再次调用loadClass方法,类似于递归操作。
                    c = parent.loadClass(name, false);
                } else {
                    //如果不存在父类加载器,则说明当前加载器是引导类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
			//当前类加载器的父类和当前类加载器都没有加载当前类,则调用加载类方法
            if (c == null) {
                //加载类
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
2. findeClass方法

如果需要重写,官方建议重写此方法(findClass),仍然能满足双亲委派机制,而不是重写loadClass方法。

protected Class<?> findClass(String name)

查找二进制名称为name的类,返回结果为java.lang.class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadclass()方法调用。

URLClassLoader类中找到此方法的重写。

protected Class<?> findClass(final String name) //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 {
                            //生成class实例
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        } catch (ClassFormatError e2) {
                            if (res.getDataError() != null) {
                                e2.addSuppressed(res.getDataError());
                            }
                            throw e2;
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

defineClass方法,生成class实例,

根据给定的字节数组b转换为Class的实例,off和len参数表示实际class信息在byte数组中的位置和长度,其中byte数组b是classLoader从外部获取的。这是受保护的方法,只有在自定义classLoader子类中可以使用.

private Class<?> defineClass(String name, Resource res)

破坏双亲委派机制----线程上下文类加载器

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载》,基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

这并非是不可能出现的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那孩怎么办?SPI:在Java平台中,通常把核心类rt.ar中提供外部服务、可由应用层自行实现的接口称为SPI)
为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计: 线程上下文类加载器 (Thread ContextlassLoader)。这个类加载器可以通过iava,lang,Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

在这里插入图片描述

默认上下文加载器就是应用类加载器,这样以上下文加载器为中介,使得启动类加载器中的仅码也可以访问应用类加载
器中的类。

简单来说就是线程上下文类加载器让启动类加载器和系统类加载器直接联系起来了,中间的扩展类加载器被省略了,所以这破坏了双亲委派机制,其中线程上下文类加载器就是系统类加载器,这个证明在01-概述—>载器之间的关系中有解释

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值