JVM类加载机制—类加载器和双亲委派机制详解

news2024/11/15 21:42:25

一、概述

上篇我们介绍了JVM类加载机制—JVM类加载过程,类加载过程是类加载机制第一阶段,这一阶段主要做将类的字节码(class文件)加载JVM内存模型中,并转换为JVM内部的数据结构(如java.lang.Class实例),便于执行。其中类加载器是JVM用于加载类文件的一个子系统,主要是通过类的全限定名来定位和加载类的二进制文件。

二、类加载器

1、类加载器分类

JVM支持两种类型的类加载器,分别为JDK自带类加载器自定义类加载器。其中自带类加载器包括引导类加载器(启动类加载器/根类加载器)、扩展类加载器、应用程序类加载器。

引导类加载器(Bootstrap ClassLoader):也加启动类加载器或根类加载器。负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等。不继承java.lang.ClassLoader,是扩展类加载器的父加载器(不是父类)。底层有C++实现,在java代码中无法直接获取到引用,返回null。

扩展类加载器(Extension ClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包,是应用程序类加载器的父加载器。由java代码编写,继承ClassLoader类。

应用程序类加载器(Application ClassLoader):也称为系统类加载器,负责加载ClassPath路径下的类包,主要就是加载自己写的那些类。由java代码编写,继承ClassLoader类,为程序中默认的类加载器。

自定义类加载器(User-Defined ClassLoader):负责加载用户自定义路径下的类包,支持一些个性化的功能。自定义类加载器只需继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空 方法,所以我们自定义类加载器主要是重写findClass方法。

自定义类加载器示例代码如下

import java.io.FileInputStream;

/**
 * 自定义类加载器
 */
public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

        public MyClassLoader(String classPath) {
            this.classPath = classPath;
        }

        //根据类名从磁盘上将类文件 读取到byte[]数组中
        private byte[] loadByte(String className) throws Exception {
            className = className.replace("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + className + ".class");
            int length = fis.available();
            byte[] bytes = new byte[length];
            fis.read(bytes);
            fis.close();
            return bytes;

        }
        //根据类名 加载Class类
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] bytes= null;
            try {
                bytes = loadByte(name);
                //defineClass方法将一个字节数组转换为Class对象,这个字节数组是class文件读取后最终的字节数组
                return defineClass(name, bytes, 0, bytes.length);
            } catch (Exception e) {
                throw new ClassNotFoundException();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        //初始化类自定义类加载器,会先初始化父类ClassLoader
        MyClassLoader myClassLoader = new MyClassLoader("D:/test");
        //D盘创建test/com/test/jvm/ 几级目录,将User1.class放入该目录
        Class clazz = myClassLoader.loadClass("com.test.jvm.User1");
        Object object = clazz.newInstance();
        //通过反射获取类中sout方法
        Method method = clazz.getDeclaredMethod("sout", new Class<?>[]{});
        //执行该方法
        method.invoke(object, null);
        //打印这个类的类加载器 MyClassLoaderTest$MyClassLoader 
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

运行结果:

通过下面示例可以测试下类所使用的具体类加载器

/**
 * 类加载器过程
 */
public class TestJDKClassLoader {

    public static void main(String[] args) {
       //核心程序类 支撑JVM运行 位于lib目录下  引导类加载器
        System.out.println("引导类加载器:" +String.class.getClassLoader());
        //扩展程序类 支撑JVM运行 位于lib目录中ext下的包 扩展类加载器
        System.out.println("扩展类加载器:" +DESKeyFactory.class.getClassLoader());
        //自己编写的程序 运行自身业务流程  应用程序类加载器
        System.out.println("应用程序类加载器:" +TestJDKClassLoader.class.getClassLoader());
    }
}

运行结果

通过上面结果我们可以看出 引导类加载器底层由于是C++实现,在java中无法获取,而扩展类加载器和应用程序类加载器 都是sun.misc.Launcher类下的内部类。

引导类加载器是扩展类加载器的父加载器,扩展类加载器是应用程序类加载器的父加载器,但三者之间没有继承或实现的关系。

2、类加载初始化过程

通过类加载全过程图(加载过程)知道,类加载会创建JVM的sun.misc.Launcher类实例的启动器。sun.misc.Launcher初始化使用了单例模式设计,保证一个JVM虚拟机内只有一个 sun.misc.Launcher实例。通过Launcher类的getLauncher()方法来获取类自己的加载器ClassLoader。

getLauncher()源码

public class Launcher {
    private static URLStreamHandlerFactory factory = new Factory();
    //单例模式设计,保证一个JVM中只有一个Launcher实例
    private static Launcher launcher = new Launcher();
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler;

   //获取Launcher类实例
    public static Launcher getLauncher() {
        return launcher;
    }

    //Launcher类构造方法
    public Launcher() {
        ExtClassLoader var1;
        try {
            //构造扩展类加载器,在构造的过程中将其父加载器设置为null
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //构造应用程序类加载器,在构造过程中将其父加载器设置为ExtClassLoader
            //Launcher类的loader属性只是AppClassLoader,一般都是用这个类加载器来加载我们写的代码
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        //。。。。。省略一部分不关注的代码
    }
}

三、JVM类加载机制种类

全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另一个类加载器来载入。


双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。


缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为什么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

四、双亲委派机制

JVM为了避免类的重复加载,确保一个类的全局唯一性以及保护程序安全,防止核心API被随意篡改。JVM会采用双亲委派模型进行加载,双亲委派模型要求除了引导类加载器外,其余的类加载器都应当有自己的父加载器。

1、工作原理

当类加载器收到加载一个类的请求时,它首先不会尝试自己去加载这个类,而是为先委托父加载器寻找这个类,找不到就会在委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。简单来说先找父加载器加载,不行再由子加载器自己加载。加载流程如下:

我们来看下应用程序类加载器APPClassLoader加载类的双亲委派机制的源码。APPClassLoader加载器的loadClass方法最终会调用其父类ClassLoader的loadClass方法。该方法大体逻辑如下:

1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接 返回。

2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false));或者是调用bootstrap类加载器来加载。

3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的 findClass方法来完成类加载。

ClassLoader.loadClass()源码

//ClassLoader的loadClass方法,里面实现了双亲委派机制
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 {
                    //如果当前类加载器的父加载器不为空,则委托父加载器加载该类
                    //AppClassLoader 父加载器为ExtClassLoader,直接执行还是到这个loadClass方法
                    ExtClassLoader加载器的父加载器为null(引导类加载器是C++实现,所以为null)
                    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
                }
                //当该类通过类加载器加载为空时,通过findClass方法加载类
                //ExtClassLoader无findClass方法,通过父类URLClassLoader.findClass方法加载类
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //通过父类URLClassLoader.findClass方法加载类
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

URLClassLoader.findClass方法源码

//类加载器通过findClass方法尝试加载类
protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        //通过类的name 获取类的路径
                        //如com.test.jvm.Math 获取path = com/text/jvm/Math.class
                        String path = name.replace('.', '/').concat(".class");
                        //通过类路径从磁盘上拿到类文件
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                //通过defineClass方法将类装载到JVM中
                                //ExtClassLoader加载器是装载失败的,因为ExtClassLoader加载的是ext包下的路径,返回为null
                                //再会执行AppClassLoader加载器的defineClass进行加载,才会装载JVM成功
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

2、双亲委派机制意义

沙箱安全机制:确保一个类的全局唯一性以及保护程序安全,防止核心API被随意篡改。如自己写的java.lang.String.class类是不会被加载的

类的唯一性:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,避免重复加载,保证被加载类的唯一性

上一篇: JVM类加载机制—类加载过程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2073988.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java常用工具类之Date类和Calender类

1、 Date类中常用方法 1. Date类的常用方法 Date类的常用方法 方法 含义 new Date&#xff08;&#xff09; 实例化Date对象&#xff0c;常见于获得系统当前时间 new Date&#xff08;long time&#xff09; 实例化Date对象&#xff0c;并根据具体的时间偏移量time设置时…

C语言 | Leetcode C语言题解之第371题两整数之和

题目&#xff1a; 题解&#xff1a; int getSum(int a, int b){ int c; while(b) {c(unsigned int)(a&b)<<1;a^b;bc; }return a; }

静态网页的制作步骤

静态网页是由HTML、CSS和JavaScript等前端技术构建而成的&#xff0c;它们通常用于展示静态内容&#xff0c;不涉及动态数据的处理。制作静态网页的过程涉及多个步骤&#xff0c;包括规划、设计、编码和测试等。下面是一个详细的制作静态网页的步骤&#xff0c;希望对你有帮助。…

Apache Flink内存模型

Flink 内存模型 大数据中所有开源的框架都会使用到JVM&#xff0c;不如&#xff0c;MapReduce&#xff0c;Storm&#xff0c;Spark等&#xff0c;这些计算框架处理数据过程中涉及到将大量数据存储到内存中&#xff0c;此时如果内存管理过渡依赖JVM&#xff0c;会出现java对象存…

国产GD32单片机开发入门(二)GD32单片机详解

文章目录 一.概要二.单片机型号命名规则三.GD32F103系统架构四.GD32F103C8T6单片机启动流程五.GD32F103C8T6单片机主要外设资源六.单片机开发过程中查看芯片数据手册的必要性1.单片机外设资源情况2.GD32单片机内部框图3.GD32单片机管脚图4.GD32单片机每个管脚功能5.单片机功耗数…

Mybatis--其他查询操作和数据库连接池(下下)

序 准备工作&#xff1a; mysql数据库和表的信息更新&#xff1a; DROP TABLE IF EXISTS articleinfo;CREATE TABLE articleinfo (id INT PRIMARY KEY auto_increment,title VARCHAR ( 100 ) NOT NULL,content TEXT NOT NULL,uid INT NOT NULL,delete_flag TINYINT ( 4 ) DEF…

24 messagebox 组件

messagebox 组件使用指南 Tkinter messagebox 组件用于创建弹出式消息框&#xff0c;以显示信息、警告、错误或询问用户问题。它提供了一种简单的方法来向用户展示消息&#xff0c;并等待用户响应。以下是对 messagebox 组件的详细说明和一个使用案例。 messagebox 组件方法 …

数学基础(十)

一、层次聚类 层次聚类是聚类算法的一种&#xff0c;通过计算不同类别数据点间的相似度来创建一棵有层次的嵌套聚类树。在聚类树中&#xff0c;不同类别的原始数据点是树的最低层&#xff0c;树的顶层是一个聚类的根节点。 常见聚类树有自下而上合并和自上而下分裂两种方法 …

【计算机网络】mini HTTP服务器框架与代码

注注注&#xff1a;本篇博文都是代码实现细节&#xff0c;但不会进行演示&#xff0c;演示看孪生篇 另外&#xff0c;由于tcp套接字部分本质都是套路&#xff0c;所以就不再进行赘述。 目录 1 请求反序列化2 读取url文件内容3 构建响应 1 请求反序列化 我们肯定会先收到请求&…

VIVO社招入职SHL测评题库题型分析:动机问卷、性格问卷、归纳推理、数字推理、语言推理

VIVO社招入职SHL测评数字推理考什么&#xff1f;例题分析 VIVO社招入职SHL测评归纳推理考什么&#xff1f;例题分析

资产架构端口应用CDNWAF站库分离负载均衡

知识点&#xff1a; 1、资产架构-端口&目录&插件接口&多站点&多应用 2、番外安全-域名&服务器本身&服务厂商&管理人员 3、考虑阻碍-站库分离&CDN&WAF&负载均衡&主机防护 详细点&#xff1a; 1、前置条件-购买使用-云服务器&a…

【AI编程秘籍】Q-learning原理大揭秘!让AI学会自己做决策!

&#x1f31f;【AI编程秘籍】Q-learning原理大揭秘&#xff01;让AI学会自己做决策&#xff01;&#x1f680; Hey小伙伴们&#xff0c;今天要给大家带来的是一个非常酷炫的项目——深入浅出Q-learning原理&#xff01;无论你是编程新手还是AI老司机&#xff0c;都能从中收获满…

进阶-7.管理工具

管理工具 1.系统数据库2常用工具2.1 mysql2.2 mysqladmin2.3 mysqlbinlog2.4 mysqlshow2.5 mysqldump2.6mysqlimport /source 1.系统数据库 2常用工具 2.1 mysql C:\Users\Tracy>mysql -uroot -p123456 test -e "select * from student"; -------------------- |…

【小沐学Rust】Rust实现TCP网络通信

文章目录 1、简介2、安装2.1 安装Rust2.2 VsCode安装Rust插件 3、快速入门3.1 命令行构建3.2 Cargo构建3.3 Cargo添加依赖 4、基本语法4.1 main 的函数4.2 代码缩进4.3 todo! 宏4.4 println! 宏4.5 变量的使用4.6 元组4.7 结构4.8 枚举4.9 函数 5、TCP通信5.1 测试一5.1.1 TCP服…

【HTML】模拟二级菜单【附源代码】

模拟二级菜单 HTML部分&#xff1a; <!DOCTYPE html>: 声明文档类型为HTML5。<html>: HTML文档的根元素。<head>: 包含文档的元数据&#xff0c;如字符集、标题和样式。 <meta charset"utf-8">: 设置文档的字符编码为UTF-8。<title>:…

企业级web应用服务器之Tomcat

Tomcat介绍 Tomcat 由 Apache 软件基金会下属的 Jakarta 项目开发。它实现了 Java Servlet 和 JavaServer Pages&#xff08;JSP&#xff09;等 Java EE 技术规范&#xff0c;为基于 Java 的 Web 应用程序提供运行环境。 主要特点 轻量级 Tomcat 是一个相对轻量级的服务器&am…

浏览器解析流程

思考 不知道大家是否思考过这几个问题&#xff1a; 当我们在浏览器中输入url后&#xff0c;到底发生了什么&#xff1f; h5&#xff0c;css&#xff0c;js代码执行的顺序是什么&#xff1f;什么情况下会阻塞页面&#xff1f;又有什么办法可以提高页面响应速度呢&#xff1f; 如…

再说圆的面积

在微积分-圆的面积和周长(1)介绍微积分方法求解圆的面积&#xff0c;本文使用蒙特卡洛方法求解圆面积。 取&#xff08;0&#xff0c;1&#xff09;* (0,1)区间&#xff0c;也就是单位圆第一象限的端点区间对应的正方形区间。下面是计算机给出的结果&#xff1a;很显然第一象限…

基于vue3和audio封装的简易音频播放器

样式如图所示 <template><div class"audio-player"><div class"player_top" flex-ac flex-justify-between ><div class"fileName genericTitle" fs-28 l-height-32 height-64 pr-42 flex-ac><span class"t…

UE管理内容 —— FBX Morph Target Pipeline

目录 Naming Setting Up Morph Targets Importing Morph Targets Morph Target 是特定网格体的顶点位置的快照&#xff0c;该网格体在某种程度上已经变形&#xff1b;例如&#xff0c;可以使用一个角色模型&#xff0c;对其面部进行重塑以创建一个面部表情&#xff0c;然后将…