Java 类加载器和双亲委派机制原理剖析

news2024/11/27 14:43:36

目录

    • 一、类加载器(ClassLoader)
    • 二、四种类加载器之间的关系
    • 三、双亲委派机制
      • 3.1、为什么要设计双亲委派机制
      • 3.2、类加载器loadClass(String name) 源码解析
      • 3.3、打破双亲委派机制

一、类加载器(ClassLoader)

      JVM的类加载机制是按需加载的模式运行的,也就是代表着:所有类并不会在程序启动时全部加载,而是当需要用到某个类发现它未加载时,才会去触发加载的过程。

在这里插入图片描述

  • 引导/启动类加载器: Bootstrap ClassLoader,负责加载存放在jdk\jre\lib(jdk代表jdk的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类,比如java.lang.Integer均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

    注意:因为JVM是通过全限定名加载类库的,所以,如果你的文件名不被虚拟机识别,就算你把jar包丢入到lib目录下,引导类加载器也并不会加载它。出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类文件。

  • 扩展类加载器: Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

    这个类加载器是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$ExtClassLoader位置。它主要负责加载<JAVA_HOME>\lib\ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库。它可以直接被开发者使用。

  • 系统/应用程序类加载器: Application ClassLoader,也被称为应用程序类加载器,也是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$AppClassLoader位置。它负责加载系统类路径java -classpath或-D java.class.path指定路径下的类库,也就是经常用到的classpath路径。应用程序类加载器也可以直接被开发者使用。

    一般情况下,该类加载器是程序的默认类加载器,我们可以通过ClassLoader.getSystemClassLoader()方法可以直接获取到它。

  • 自定义类加载器: Application ClassLoader,也被称为应用程序类加载器,也是由sun公司实现的,位于HotSpot源码目录中的sun.misc.Launcher$AppClassLoader位置。它负责加载系统类路径java -classpath或-D java.class.path指定路径下的类库,也就是经常用到的classpath路径。应用程序类加载器也可以直接被开发者使用。

二、四种类加载器之间的关系

如上分析的类加载器关系链如下:

Bootstrap引导类加载器 → Extension拓展类加载器 → Application系统类加载器 → User自定义类加载器

Bootstrap类加载器是在JVM启动时初始化的,它会负责加载ExtClassLoader,并将其父加载器设置为BootstrapClassLoader。BootstrapClassLoader加载完ExtClassLoader后会接着加载AppClassLoader系统类加载器,并将其父加载器设置为ExtClassLoader拓展类加载器。而自己定义的类加载器会由系统类加载器加载,加载完成后,AppClassLoader会成为它们的父加载器。

要注意的是:类加载器之间并不存在相互继承或包含关系,从上至下仅存在父加载器的层级引用关系。

例:

// 继承ClassLoader类,JDKClassLoaderDemo 相当于一个自定义类加载器
public class JDKClassLoaderDemo extends ClassLoader{
    public static void main(String[] args) {
        JDKClassLoaderDemo classLoader = new JDKClassLoaderDemo();
        System.out.println("自定义加载器:" + classLoader);
        System.out.println("自定义加载器的父类加载器:" + classLoader.getParent());
        System.out.println("Java程序系统默认的加载器:" + ClassLoader.getSystemClassLoader());
        System.out.println("系统类加载器的父加载器:" + ClassLoader.getSystemClassLoader().getParent());
        System.out.println("拓展类加载器的父加载器:" + ClassLoader.getSystemClassLoader().getParent().getParent());

        System.out.println();
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i]);
        }

        System.out.println();
        System.out.println("extClassloader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));

        System.out.println();
        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));
    }
}

// 输出结果
/*
自定义加载器:com.kerwin.jvm.classloader.JDKClassLoaderDemo@74a14482
自定义加载器的父类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
Java程序系统默认的加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
系统类加载器的父加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
拓展类加载器的父加载器:null

bootstrapLoader加载以下文件:
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_202/jre/classes

extClassloader加载以下文件:
C:\Program Files\Java\jdk1.8.0_202\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext

appClassLoader加载以下文件:
项目目录\target\classes;
...
*/

三、双亲委派机制

      类加载器在加载 class 文件的时候,遵从双亲委派原则,意思是加载依次由父加载器先执行加载动作,只有当父加载器没有加载到 class 文件时才由子类加载器进行加载。这种机制很好的保证了 Java API 的安全性,使得 JDK 的代码不会被篡改。
在这里插入图片描述

3.1、为什么要设计双亲委派机制

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

自定义String类运行测试:

package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("**************My String Class**************");
    }
}

// 运行结果:
/*
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
 */

3.2、类加载器loadClass(String name) 源码解析

//ClassLoader的loadClass方法,里面实现了双亲委派机制
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);
                } 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); //都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类

                // 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;
    }
}

3.3、打破双亲委派机制

      尽管双亲委派机制有着诸多好处,但有时我们也需要打破它,比如在某些场景下需要加载自定义的类库,或者需要实现热部署等功能。这时,我们就需要自定义类加载器,并在其中打破双亲委派机制。

      以Java代码为例,我们可以通过继承ClassLoader类并重写loadClass方法来打破双亲委派机制。在loadClass方法中,我们可以根据需要自行加载类文件,并通过defineClass方法将其转换为Class对象,但是有一点需要注意,包名不能以java.开头(如:java.lang.String),在java.lang.ClassLoader.preDefineClass方法中会判断如果包名以java.开头会抛出java.lang.SecurityException: Prohibited package name: java

注意:同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。

  • 自定义一个User
package com.kerwin.jvm.classloader;

public class User {
    public void printClassLoad(){
        System.out.println("当前User类的类加载器为:"+this.getClass().getClassLoader());
    }
}
  • 打破双亲委派机制实现
import java.io.FileInputStream;
import java.lang.reflect.Method;

public class MyClassLoaderTest {
    static class MyClassLoader extends ClassLoader {
        private String classPath;

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

        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name
                    + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;

        }
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }
        /**
         * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
         */
        protected Class<?> loadClass(String name, boolean resolve)
                throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t = System.nanoTime();
                    // 这里规定只有指定目录的类不走双亲委派加载,非自定义的类还是走双亲委派加载
                    if (!name.startsWith("com.kerwin.jvm.classloader")) {
                        c = this.getParent().loadClass(name);
                    } else {
                        c = findClass(name);
                    }
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }

    public static void main(String args[]) throws Exception {
        MyClassLoader classLoader = new MyClassLoader("D:\\classloader-example\\target\\classes");
        Class clazz = classLoader.loadClass("com.kerwin.jvm.classloader.User");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("printClassLoad", null);
        method.invoke(obj, null);
        System.out.println(clazz.getClassLoader());

        System.out.println();
        MyClassLoader classLoader1 = new MyClassLoader("D:\\classloader-example\\target\\classes");
        Class clazz1 = classLoader1.loadClass("com.kerwin.jvm.classloader.User");
        Object obj1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("printClassLoad", null);
        method1.invoke(obj1, null);
        System.out.println(clazz1.getClassLoader());
    }
}

// 输出结果
/*
当前User类的类加载器为:com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@7a7b0070
com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@7a7b0070

当前User类的类加载器为:com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@6ed3ef1
com.kerwin.jvm.classloader.MyClassLoaderTest$MyClassLoader@6ed3ef1
*/

可以看到两个User所属类加载器内存地址不同,代表是两个不同的类加载器,那么这个User类信息也在JVM中存在2份,两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个,如果存在双亲委派那么类加载器应该是同一个,类加载器的内存地址应该也是相同的,这里不同则代表已经打破双亲委派。

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

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

相关文章

万户 ezOFFICE convertFile 文件读取漏洞复现

0x01 产品简介 万户OA ezoffice是万户网络协同办公产品多年来一直将主要精力致力于中高端市场的一款OA协同办公软件产品,统一的基础管理平台,实现用户数据统一管理、权限统一分配、身份统一认证。统一规划门户网站群和协同办公平台,将外网信息维护、客户服务、互动交流和日…

Linux环境下socket本地通信

最近项目有用到了socket本地通信&#xff0c;故复习一下。之前都是基于本地虚拟机的ip地址通信的&#xff0c;现在项目&#xff0c;Linux单板上面有2个进程需要通信&#xff0c;故用到了本地socket通信&#xff0c;主要其实就是用了sockfd,文件描述符&#xff0c;也叫句柄。 服…

MySQL概述-安装与启动

数据库相关概念 MySQL数据库 下载地址 MySQL :: Download MySQL Installer (Archived Versions) 启动方法 启动密令&#xff1a;net start mysql80 停止密令&#xff1a;net stop mysql80 客户端链接方法 注意用系统自带的命令行工具执行指令需要设置环境在高级系统设置中…

Vue 创建虚拟DOM元素的几种方式和实际应用。

目录 创建虚拟DOM元素的方式 创建一个简单的元素&#xff1a; 创建一个带有属性的元素&#xff1a; 创建一个带有子元素的元素&#xff1a; 创建一个带有事件监听器的元素&#xff1a; 创建一个Vue组件 创建一个带Props的组件 创建一个带Slot的组件 实际应用 创建虚…

Python源码17:使用海龟画图turtle画五星红旗

turtle模块是一个Python的标准库之一&#xff0c;它提供了一个基于Turtle graphics的绘图库。Turtle graphics是一种流行的绘图方式&#xff0c;它通过控制一个小海龟在屏幕上移动来绘制图形。 turtle模块可以让您轻松地创建和控制海龟图形&#xff0c;从而帮助您学习Python编…

SpringSecurity6 | 登录成功后的JSON处理

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; Java从入门到精通 ✨特色专栏&#xf…

Gilisoft Video Editor——迈出剪辑的第一步

今天博主分享的是又一款剪辑软件——视频剪辑手&#xff08;GiliSoft Video Editor&#xff09;&#xff0c;对剪辑视频感兴趣的小伙伴千万不要错过。这是一款专门用于视频剪辑的软件&#xff0c;功能比较简单&#xff0c;相比于专业的pr是比不了的&#xff0c;但是制作一些简单…

内存映射机制

什么是内存映射 Linux通过将一个虚拟内存区域与一个磁盘上的对象关联起来&#xff0c;以初始化这个虚拟区域的内如&#xff0c;这个过程称为内存映射。 代码示例&#xff1a; /******************************************************************** > File Name: mmap…

如何查询川菜食材配料的API接口

在当今的美食文化中&#xff0c;菜谱不只是一张简单的食谱&#xff0c;更是了解美食文化和饮食知识的重要途径。然而&#xff0c;若没有准确的食材配料&#xff0c;烹制出的每道菜品都将难以达到完美的味道。因此&#xff0c;为了更好地满足人们对于菜谱和食谱的需求&#xff0…

Gitzip插件【Github免翻下载】

今天给大家推荐一个github下载的插件&#xff0c;平常大家下载应该无外乎就是以下两种&#xff1a; Download zip利用git clone 但是这两种各有各的弊端&#xff0c;前者一般需要科学上网才可以&#xff0c;后者下载不稳定经常中途断掉。 今天给推荐一个款浏览器插件-Gitzip.大…

单调栈结构

单调栈 单调栈是一种特殊设计的栈结构&#xff0c;为了解决如下的问题&#xff1a; 给定一个可能含有重复数值的 arr[]&#xff0c;i位置的数一定存在如下两种信息&#xff1a; arr[i]的左侧离 i 最近并且小于&#xff08;或者大于&#xff09;arr[i] 的数在哪&#xff1f;arr…

边缘AI行业报告:发展趋势、相关机遇、产业链及相关公司深度梳理

今天分享的AI系列深度研究报告&#xff1a;《边缘AI行业报告&#xff1a;发展趋势、相关机遇、产业链及相关公司深度梳理》。 &#xff08;报告出品方&#xff1a;慧 博 智 能 投 研&#xff09; 报告共计&#xff1a;31页 边缘计算及边缘 AI 概述 1.边缘AI 边缘 AI(本文指…

第四节JavaScript 条件语句、循环语句、break与continue语句

一、JavaScript条件语句 在通常的代码中&#xff0c;我们有一些需要决定执行不同动作&#xff0c;这就可以在代码中使用条件语句来完成。 下面是我们常使用的条件语句&#xff1a; if语句&#xff1a;只有当指定条件是true时&#xff0c;执行条件内代码。if…else语句&#…

代码序随想录二刷 |二叉树 | 二叉树的层序遍历II

代码序随想录二刷 &#xff5c;二叉树 &#xff5c; 二叉树的层序遍历II 题目描述解题思路代码实现 题目描述 107.二叉树的层序遍历II 给你二叉树的根节点 root &#xff0c;返回其节点值 自底向上的层序遍历 。 &#xff08;即按从叶子节点所在层到根节点所在的层&#xff0…

【Unity动画】Avatar Mask

创建 Avatar Mask可以设置那一部分骨骼运动和不运动 然后放在状态机里面的层中来混合 【后续完善】

JVM虚拟机系统性学习-运行时数据区(堆)

运行时数据区 JVM 由三部分组成&#xff1a;类加载系统、运行时数据区、执行引擎 下边讲一下运行时数据区中的构成 根据线程的使用情况分为两类&#xff1a; 线程独享&#xff08;此区域不需要垃圾回收&#xff09; 虚拟机栈、本地方法栈、程序计数器 线程共享&#xff08;数…

java接入gpt开发

前情提要 本次文章使用编译器为IDEA2020 使用GPT模型为百度旗下的千帆大模型 如果是个人用或者不流传出去&#xff0c;可以无脑入&#xff0c;因为会免费送20块钱&#xff08;够用上万次&#xff09; 代金卷查看 正式教程&#xff1a; 百度智能云控制台 (baidu.com) 按照步…

游戏提示找不到d3dx10_43.dll怎么办?5种方法教你如何修复

在计算机使用过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“缺少d3dx10_43.dll文件”。这个错误提示通常出现在运行某些游戏或应用程序时&#xff0c;它意味着系统无法找到所需的动态链接库文件。本文将详细介绍d3dx10_43.dll文件的作用以及导致其丢…

《文化创新比较研究》期刊发表杂志投稿

《文化创新比较研究》是由国家新闻出版总署批准&#xff0c;黑龙江文化产业投资控股集团有限公司主管主办的学术期刊&#xff08;旬刊&#xff09;。 以学术研究为基础&#xff0c;始终坚持双百方针&#xff0c;立足理论前沿&#xff0c;关注学术热点&#xff0c;推动学术文化交…

kotlin - ViewBinding

前言 为什么用ViewBinding&#xff0c;而不用findViewById()&#xff0c;这个有很多优秀的博主都做了讲解&#xff0c;就不再列出了。 可参考下列博主的文章&#xff1a; kotlin ViewBinding的使用 文章里也给出了如何在gradle中做出相应的配置。 &#xff08;我建议先看这位博…