首页 > 其他分享 >一文弄懂JVM类加载器与双亲委派机制

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

时间:2024-07-24 20:55:20浏览次数:19  
标签:name 自定义 ClassLoader 弄懂 双亲 JVM Class 加载

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

类的加载器的意义:

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

3.1 加载器分类

  1. 类的隐式加载与显式加载

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

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

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

  • 扩展类加载器 Extension Class Loader

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

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

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

  • 自定义类加载器

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

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

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

3.2.1 双亲委派模型

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

在这里插入图片描述

3.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的重名包)。

3.2.3 双亲委派机制的优劣势

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

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

3.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.3 ClassLoader源码解析

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

在这里插入图片描述

首先看ClassLoader

需要关注的属性

private final ClassLoader parent;

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

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

需要关注的方法

  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可能会将某些请求交予自己的双亲处理。

标签:name,自定义,ClassLoader,弄懂,双亲,JVM,Class,加载
From: https://blog.csdn.net/m0_72397750/article/details/140673023

相关文章

  • OpenSSL Engine的三种加载方式
    本文测试代码基于Openssl版本:1.1.1f创建一个Enginelib#include<openssl/evp.h>#include<openssl/engine.h>#include<iostream>staticintencryptfn(EVP_PKEY_CTX*ctx,unsignedchar*out,size_t*outlen,constunsignedchar*in,size_tinlen){*outlen......
  • Failed to create JVM.JVM Path:D:\IntelliJ IDEA 2024.1.4\jbr Error launching I
     IDEA修改启动参数掉大坑!情况说明:在Help>EditCustom VMOptions修改IDEA的VM产生之后无法打开(重启/重装多次依然无法打开),修改C盘的idea64.exe.vmoptions和安装目录的idea64.exe.vmoptions依然无法启动!,后面把IDEA安装目录的jar文件夹删了,又重装JDK。由于我有多个JDK环境,......
  • 【7.PIE-Engine案例—— 加载Terra星全球500m地表反射率8天合成产品(MOD09A1 V61)数据
    加载Terra星全球500m地表反射率8天合成产品(MOD09A1V61)数据集原始路径欢迎大家查看本案例原始来源最终结果具体代码/***@File:MOD09A1*@Time:2020/7/21*@Author:piesat*@Version:1.0*@Contact:400-890-0662*@License......
  • elementui中实现loding实现局部加载,以el-dialog为例
    效果 封装loading加载(也可以直接使用,封装为了方便多次调用)组件定义:loadDiy.jsimport{Loading}from"element-ui";exportconstservicesLoading=(node,str,lock)=>{returnLoading.service({target:document.querySelector(node),//loading需要覆盖的DO......
  • Android超复杂布局加载速度优化
    一、概述有时候由于实际业务的需要,或者产品经理或设计师考虑的不够全面,会导致某一个或某些页面的布局超级复杂。这些超级复杂的UI在经过程序员通过传统布局优化过后仍然是复杂的(优化布局层级、优化层级布局数量等)。这就会导致布局加载速度过于缓慢。直接的结果就是打开A......
  • 为什么重新加载后“globals()”中会出现未声明的变量,并且使用它们来识别重新加载是否
    我发现重新加载模块时下面的代码片段test出乎意料地已经在globals()/locals()中定义了所有变量。为什么会发生这种情况?我注意到了这个"xxx"inlocals()模式|||BlenderPython脚本中有很多,因为人们通常使用它来检查模块之前......
  • 深入理解Java虚拟机:JVM高级特性与最佳实践-第三章-垃圾收集器与内存分配策略
    在java内存运行时区域中的各个部分中,程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存自然就跟随着回收了。但是Java堆和方法区这两个区域具有......
  • Arthas调试JVM参数的使用
    一、对于JVM介绍1.JVM区域JVM区域总体分两类,heap区和非heap区。heap区又分:EdenSpace(伊甸园)、SurvivorSpace(幸存者区)、TenuredGen(老年代-养老区)。非heap区又分:CodeCache(代码缓存区)、PermGen(永久代)、JvmStack(java虚拟机栈)、LocalMethodStatck(本地方法栈)。2.Ho......
  • 如何在 PySide 中从 .ui 文件加载子项?
    现在我像这样加载它们:if__name__=='__main__':app=QApplication(sys.argv)loader=QUiLoader()file=QFile('main.ui')file.open(QFile.ReadOnly)window=loader.load(file)file.close()window.show()#Here:......
  • 我如何为 yolov5 制作 gui,从 pytorch 和 opencv 加载到 tkinker?
    请帮助我,我不明白如何使用yolo和tkinker作为gui来制作用于实时检测的gui。以及如何将边界框从pytorch渲染到tkinker?这里是代码:importtorchfrommatplotlibimportpyplotaspltimportnumpyasnpimportcv2model=torch.hub.load('ultralytics/yolov5......