一文弄懂JVM类加载器与双亲委派机制

类的加载器完成类的加载环节中的装载阶段的工作(通过一个类的全限定名来获取该类的二进制字节流,且这个动作在虚拟机外部实现,即开发者可以决定如何去获取所需的类),且不会影响后续的链接和初始化阶段,但类的加载器的存在使得类不会卸载

类的加载器的意义:

  • 加载器的意义不仅在于类的加载阶段,而且类和类的加载器共同确立其在虚拟机中的唯一性。一个类的加载器就都拥有一个独立的类名称空间,不同名称空间下的类即使从同一个字节码文件中加载依旧不等(equals()、isInstanceof)。
  • 由于可以自定义类的加载器,可以对需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作,或者可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现类库的动态加载和自定义的处理逻辑。
  • 自定义加载器能够实现应用隔离,例如 Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。(这也是Java相比C/C++的优势所在)

1 加载器分类

1.1 类的隐式加载与显式加载

class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。

  • 显式加载:指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)、this.getClass().getClassLoader().loadClass()加载class对象。
  • 隐式加载:则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

1.2. 加载器类别

  • 启动类加载器 Bootstrap Class Loader (引导类加载器)

C++语言实现,虚拟机内部的加载器(程序内无法获取),系统启动时自动加载核心包内的类。

  • 扩展类加载器 Extension Class Loader

Java语言实现,继承自ClassLoader,专门为加载扩展包、具有通用性的类库的加载器。

  • 应用程序加载器 Application Class Loader (系统类加载器)

Java语言实现,继承自ClassLoader,加载用户类路径(Class Path)上的所有类库(即开发者自定义的类都由这个加载器加载),也是默认的加载器。

  • 自定义类加载器

Java语言实现,继承自ClassLoader,用户自定义的类加载器。

说明:**数组**类的Class对象,不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的。

2 类加载器的双亲委派机制

2.1 双亲委派模型

除了顶层的启动类加载器,所有的类加载器都应有父类加载器(所谓的父类加载器不是由继承实现的,更多地体现为上下层的关系)

在这里插入图片描述

2.2 双亲委派模型的原理

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

规定了类加载的顺序是:

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

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

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

(4)如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类。

注意:双亲委派机制并非是强制的,重写了loadClass()方法将打破该机制,所以为了避免核心库冲突,JDK为核心类库提供了一层保护机制。不管是自定义的类加载器,还是系统类加载器抑或扩展类加载器,最终都必须调用 java.lang.ClassLoader.defineClass(String, byte[], int, int, ProtectionDomain)方法,而该方法会执行**preDefineClass()**接口,该接口中提供了对JDK核心类库的保护,java包下的类库会强制由启动类加载器加载(无法加载自定义的java.lang的重名包)。

2.3 双亲委派机制的优劣势

好处:1.带有优先级的层次关系避免了类的重复加载;2.保护程序安全,防止核心API被随意篡改

弊端:顶层的ClassLoader无法访问底层的ClassLoader所加载的类。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。

2.4 打破双亲委派机制

由于存在顶层的ClassLoader无法访问底层的ClassLoader所加载的类的弊端,对于一些特殊场合,不得已需要打破双亲委派机制。

  1. Java 1.2 之前的代码

由于Java1.2之前并没有双亲委派机制,所以为了兼容这些代码,需要打破双亲委派机制。

  1. 线程上下文类加载器ContextClassLoader

ContextClassLoader不是一个优雅的设计,但能解决问题。线程带有一个默认的系统类加载器,使得上层的加载器也能逆向使用下层。

  1. 热替换、热部署

在执行中,如果类代码有更新时不重新打包部署,而是优先用一个新的自定义类加载器去加载这个类,然后再连同加载器一起撤掉原有的类。

例子:tomcat

JDK核心jar包还是遵循双亲委派机制,只是对于tomcat自己相关的类文件有特殊的设计,首先检查在子父类加载器中是否已经加载(缓存),如果没有会直接跳到对应的类加载器加载,加载完了再让上层的类加载器加载尚未加载的内容。

系统类加载器加载tomcat的bin目录下的关键jar包,Common类加载器加载tomcat的lib包下的jar包,Catalina类加载器加载容器私有的类文件,webapp无法访问,Shared类加载器加载webapp之间共享的类文件,Webapp类加载器加载的webapp之间隔离。JSP类加载器用于实现热替换,每个JSP文件都有一个加载器。

当应用需要到某个类时,则会按照下面的顺序进行类加载:

1 使用bootstrap引导类加载器加载

2 使用system系统类加载器加载

3 使用应用类加载器在WEB-INF/classes中加载

4 使用应用类加载器在WEB-INF/lib中加载

5 使用common类加载器在CATALINA_HOME/lib中加载

tomcat是个web容器。tomcat类加载机制的设计,使得Java核心包依旧走双亲委派,对于一些公用的类库,用Common类加载器避免重复加载,而不同的webapp则通过Webapp类加载器来实现相互隔离,JSP类加载器则实现了热部署。

在这里插入图片描述

3 ClassLoader源码解析

首先是抽象类ClassLoader在java.lang包下,扩展类装载器和应用程序装载器分别对应sun.misc.Launcher类下的AppClassLoader和ExtClassLoader两个内部类。

在这里插入图片描述

首先看抽象类ClassLoader

3.1 ClassLoader需要关注的属性

private final ClassLoader parent;

记录了其父类加载器,所谓的父子类加载器,也就是在构造器中传入

protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), null, parent);
}

3.2 ClassLoader需要关注的方法

  1. public Class<?> loadClass(String name) throws ClassNotFoundException

加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回 ClassNotFoundException 异常。该方法中的逻辑就是双亲委派模式的实现。自定义加载器时不建议重写。

在未确定父类加载器不能加载该类时,会首先不停地向上层的加载器递归,让上层的加载器优先加载,上层加载器均加载失败时才调用findClass()方法尝试自己加载。

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) {
                    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) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
  1. protected Class<?> findClass(String name) throws ClassNotFoundException

**查找二进制名称为name的类,编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,返回结果为java.lang.Class类的实例。**这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用。

说明:loadClass()是加载器类默认会继承的方法,而推荐把自定义的加载逻辑放在findClass(),作为定义加载器时需要重写的方法,这样的抽取就是为了保证双亲委派机制的执行

// ClassLoader中关于该方法只是抛了类未找到的异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}
// 在URLClassLoader中有相关的实现,在自定义加载器时可以直接继承这个类
protected Class<?> findClass(final String name) throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<>() {
                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);
                        } 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;
}
  1. protected final Class<?> defineClass(String name, byte[] b, int off, int len)

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

  1. protected final void resolveClass(Class<?> c)

链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

  1. protected final Class<?> findLoadedClass(String name)

查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例。这个方法是final方法,无法被修改。

  1. private final ClassLoader parent

它也是一个ClassLoader的实例,这个字段所表示的ClassLoader也称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader可能会将某些请求交予自己的双亲处理。

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM的类加载是由类加载器及其子类实现的。类加载器Java运行时系统的重要组成部分,负责在运行时查找和加载类文件中的类。在JVM中,类加载器按照一定的层次结构进行组织,每个类加载器负责加载特定位置的类。其中,启动类加载器(Bootstrap ClassLoader)是负责加载存放在<JAVA_HOME>/lib目录中的核心类库,如rt.jar、resources.jar等,同时也可以加载通过-Xbootclasspath参数指定的路径中的类库。启动类加载器是用C语言编写的,随着JVM启动而加载。当JVM需要使用某个类时,它会通过类加载器查找并加载这个类。加载过程会经历连接阶段,包括验证、准备和解析。在验证阶段,JVM会确保加载的类信息符合JVM规范。在准备阶段,JVM会为类变量分配内存并设置初始值,在方法区中分配这些内存。在解析阶段,JVM会根据符号引用替换为直接引用,以便后续的使用。通过类加载器的协同工作,JVM能够在运行时动态加载类,从而实现Java的灵活性和跨平台性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [JVM 的类加载原理](https://blog.csdn.net/ChineseSoftware/article/details/119212339)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [JVM类加载器](https://blog.csdn.net/rockvine/article/details/124825354)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值