【Java虚拟机】JVM类加载机制和双亲委派模型

简介: 【Java虚拟机】JVM类加载机制和双亲委派模型

1.JVM虚拟机类加载子系统

(1)什么是类加载子系统

  • 是Java虚拟机的一个重要子系统,主要负责将类的字节码加载到JVM内存的方法区,并将其转换为JVM内部的数据结构。
  • b7dfb78002d347f18bda191c1f96e0a2.jpg

(2)类加载器的三大特点

  • 双亲委派模型
  • Java虚拟机采用双亲委派模型来加载类,即先从父类加载器中查找类,如果找到了就直接返回
  • 否则再由自己的加载器加载,这种模型可以避免类的重复加载,提高系统的安全性。
  • 延迟加载
  • Java虚拟机采用延迟加载的策略,即只有在需要使用某个类时才进行加载
  • 这种策略可以减少系统启动时间,提高系统的性能
  • 动态加载
  • Java虚拟机支持动态加载类,即可以在程序运行时动态地加载和卸载类
  • 这种特性可以使Java程序更加灵活和可扩展

(3)类加载子系统由三个模块组成加载器(ClassLoader

  • 加载器负责将类的字节码加载到JVM中
  • 三种类型的加载器
  • 启动类加载器(Bootstrap ClassLoader)
  • c/c++实现,加载核心类库使用,不继承ClassLoader,没父加载器
  • 平台类加载器(Platform ClassLoader)
  • 注意:JDK9之前是扩展类加载器 Extension ClassLoader
  • 应用程序类加载器(Application ClassLoader)
  • 程序的默认加载器,我们写的代码基本都是由这个加载器负责加载
  • 类加载器用父类加载器、子类加载器这样的名字,虽然看似是继承关系,实际上是组合(Composition)关系
  • 链接器(Linker)
  • 负责将Java类的二进制代码链接到Java虚拟机中,并生成可执行的Java虚拟机代码,包括 验证、准备和解析
  • 验证操作主要是验证类的字节码是否符合JVM规范
  • 准备操作主要是为类的静态变量分配内存并设置默认值
  • 解析操作主要是将符号引用转换为直接引用
  • 初始化器(Initializer)
  • 负责执行Java类的静态初始化,包括静态变量的赋值、静态代码块的执行等
  • 初始化器是类加载子系统的最后一个阶段

2.双亲委派机制和JDK9模块化系统

(1)为啥需要这个双亲委派模型

  • 比如java.lang.Object 这些存放在rt.jar中的类,无论使用哪个类加载器加载,最终都会委派给最顶端的启动类加载器加载
  • 不同加载器加载的Object类都是同一个,如果没有使用双亲委派模型,各个类加载器自行去加载的话,就会出现问题
  • 比如用户编写了一个称为java.lang.Object或String的类,并放在classpath下
  • 那系统将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证

(2)什么是双亲委派模型

  • 是一种类加载机制,类加载器之间形成了一条类加载器链,每个类加载器都有一个父类加载器,形成了从下到上的一条继承链
  • 如果所有的类加载器都无法加载该类,则会抛出 ClassNotFoundException 异常

加载流程

  • 一个类的加载请求首先会被委派给其父类加载器进行处理
  • 如果父类加载器无法加载该类,则会将加载请求委派给其自身进行加载
  • 如果自身也无法加载该类,则会将加载请求委派给其子类加载器进行处理,直到找到能够加载该类的类加载器为止
  • 优点
  • 可以保证类的唯一性和安全性。由于每个类加载器都只能加载自己的命名空间中的类
  • 由于类加载器之间形成了一条继承链,因此可以保证类的安全性,防止恶意代码的注入

758801ecb2374a7f9db3de17eff1f926.jpg

(3)什么是JDK9模块化系统

  • 是一种新的Java平台的组织方式,将Java SE分成多个模块,每个模块都有自己的API和实现
  • 每个模块都有一个唯一的标识符和版本号,可以独立地进行开发、测试、部署和维护
  • 模块之间的依赖关系通过模块描述文件(module-info.java)来声明

  • 这个文件包含模块的名称、版本号、导出的包、依赖的模块等信息
  • 在编译和运行时,模块系统会根据模块描述文件来加载和链接模块,确保模块之间的依赖关系正确

(4)新版本的JDK9后的类加载器

模块化系统中的类加载器可以分为两种类型

  • 应用程序类加载器,加载器用于加载应用程序中的模块; 平台类加载器,加载器用于加载 JDK 中的模块
  • 当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中
  • 如果findLoadedModule 可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载
  • 在模块化系统中,每个模块都有一个类加载器,它根据模块的依赖关系来加载模块中的类和依赖的模块中的类
  • 在模块化系统中,类加载器的原理与传统的类加载器相似,都是采用双亲委派模型
  • 当一个类被加载时,类加载器首先会检查自己是否已经加载过该类,如果没有,则会将该类的加载请求委托给其父加载器
  • 直到达到顶层的 Bootstrap ClassLoader 为止,如果所有的父加载器都无法加载该类,则由当前加载器自己来加载该类
  • (5)新版本的JDK9后的类加载器
  • 模块化系统中的类加载器可以分为两种类型
  • 应用程序类加载器,加载器用于加载应用程序中的模块; 平台类加载器,加载器用于加载 JDK 中的模块
  • 当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中
  • 如果findLoadedModule 可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载
  • 在模块化系统中,每个模块都有一个类加载器,它根据模块的依赖关系来加载模块中的类和依赖的模块中的类
  • 在模块化系统中,类加载器的原理与传统的类加载器相似,都是采用双亲委派模型
  • 当一个类被加载时,类加载器首先会检查自己是否已经加载过该类,如果没有,则会将该类的加载请求委托给其父加载器
  • 直到达到顶层的 Bootstrap ClassLoader 为止,如果所有的父加载器都无法加载该类,则由当前加载器自己来加载该类

25926976086b4dcf93cc71b364b2747b.jpg

  • 关键类BuiltinClassLoader
  • BuiltinClassLoader 是 jdk9 中代替 URLClassLoader 的加载器,是 PlatformClassLoader 与 AppClassLoader 的父类

3.ClassLoader源码解读和自定义类加载器场景

(1)为什么需要用到自定义类加载器

  • 为 Java 应用程序提供更加灵活和可定制的类加载机制
  • 场景案例
  • 拓展加载源
  • 从网络,数据库等地方加载类
  • 防止源码泄漏
  • 自定义类加载器可以加载加密的类文件,保护类的安全性
  • 实现类隔离(tomcat里面大量应用)
  • 自定义类加载器可以实现类隔离,避免类之间的冲突和干扰
  • 注意
  • 比较两个类是否相等,只有两个类是由同一个类加载器加载的前提下才有意义
  • 否则即使两个类来自同一个class文件,但是由于加载他们的类加载器不同,那这两个类就不相等
  • 不同类加载器加载同一个class文件得到的类型是不同的
  • (2)ClassLoader核心源码解读
  • loadClass
  • 用于加载指定名称的类,双亲委派模型核心实现,一般不建议重写相关方法,直接由ClassLoader自己实现
  • 遵循双亲委派模型,首先委派给父类加载器进行加载,如果父类加载器无法加载该类,则自身进行加载
  • findClass
  • 是用于查找类的方法,它通常由子类加载器实现,用于查找自身命名空间中的类
  • 由于历史JDK1.2之前版本兼容问题,自定义类加载器则推荐重写这个方法
  • findClass()方法是在loadClass()方法中调用,当loadClass()方法中加载失败后,则调用自己的findClass()方法来完成类加载
  • 但是ClassLoader的findClass没有实现,需要自己实现具体逻辑,findClass方法通常是和defineClass方法一起使用的

defindClass

  • 是用于定义类的方法,它将字节数组转换为 Class 对象,并将其添加到类加载器的命名空间中
  • ClassLoader方法里面已经实现,findClass方法通常是和defineClass方法一起使用的
  • resolveClass
  • 是用于解析类的方法,它将类的引用转换为实际的类,并进行链接和初始化
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            //首先检查这个class是否已经加载过了
            Class<?> c = findLoadedClass(name);
            //c==null表示没有加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 如果有父类的加载器则让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    //如果父类的加载器为空 则递归到bootStrapClassloader,这里就是双亲委派模型的实现
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                  /如果bootstrapClassLoader 仍然没有加载过,则自己去加载class
                    long t1 = System.nanoTime();
                  //是一个空方法,返回内容为class,方法其中没有任何内容,只抛出了个异常,说明这个方法需要开发者自己去实现
                    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;
        }
    }

注意两个方法区别

  • findClass( ) 用于写类加载逻辑
  • loadClass( )方法的逻辑里,如果父类加载器加载失败则会调用自己的findClass( )方法完成加载,保证了双亲委派规则
  • 如果不想打破双亲委派模型,那么只需要重写findClass方法即可
  • 如果想打破双亲委派模型,多数情况下需要重写整个loadClass方法

4.自定义ClassLoader类加载器案例实战

  • 自定义类加载器流程
  • 继承ClassLoader类
  • 重写loadClass方法(会破坏双亲委派机制,不推荐)
  • 重写findClass方法 (推荐)
  • 先定义User类,将User.java定义成User.class
@Data
public class User {
    private String name;
    private int age;
}
javac User.java

b25d9e27e20f429681abca88307dec75.jpg

  • 编码实战
  • 自定义类加载器
public class MyClassLoader extends ClassLoader{
    private String codePath;
    public MyClassLoader(String codePath){
        this.codePath = codePath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = codePath + name + ".class";
        System.out.println(fileName);
        //获取输入流
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(fileName));
             ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                bos.write(data, 0, len);
            }
            //获取内存中字节数组
            byte[] byteCode = bos.toByteArray();
            //执行 defineClass 将字节数组转成Class对象
            Class<?> defineClass = defineClass(null, byteCode, 0, byteCode.length);
            return defineClass;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        //测试自定义类加载器
        MyClassLoader myClassLoader = new MyClassLoader("/Users/mac/IdeaProjects/jvm-test/src/main/java/com/lixiang/");
        //加载指定类
        Class<?> clazz = myClassLoader.loadClass("User");
        //创建对像
        Object obj = clazz.getDeclaredConstructor().newInstance();
        System.out.println( "当前类:"+obj.getClass().getName());
        System.out.println( "当前类加载器:"+obj.getClass().getClassLoader().getClass().getName());
    }
}

658658b6dbb445afac5a6e8fc7699bf7.jpg使用自定义的类加载器进行加载 。

5.不同类加载器加载同个class类

问题:两个不同的类加载器加载同一个class类,JVM是否认为它们相同

编码验证

public static void main(String[] args) throws Exception {
        //测试不同classloader加载同个class
        MyClassLoader myClassLoader = new MyClassLoader("/Users/mac/IdeaProjects/jvm-test/src/main/java/com/lixiang/");
        //用自定义的类加载器加载User.class
        Class<?> clazz = myClassLoader.loadClass("User");
        //创建对象
        Object obj = clazz.getDeclaredConstructor().newInstance();
        //判断创建的对象是否是User对象
        System.out.println("判断创建的对象是否是User对象:"+(obj instanceof User));
    }

88f0b75626fa4889bcdf0feebc4bd407.jpg

        //打印相应的类加载器
        System.out.println("obj 的 类加载器:"+obj.getClass().getClassLoader().getClass().getName());
        System.out.println("User 的 类加载器:"+User.class.getClassLoader().getClass().getName());


628ac6a1888043a78b06d7e68bf36f8a.jpg

在 JVM 中,不同类加载器加载同一个类时,可能会出现重复加载的情况

当不同类加载器加载同一个类时,每个类加载器都会在自己的命名空间中创建一个新的 Class 对象

即使这些 Class 对象的字节码是一样的,也会被认为是不同的类

重复加载同一个类会导致一些问题,例如类的静态变量和代码块会被多次执行,导致出现意料之外的行为

JVM 采用了类的双亲委派模型来避免重复加载同一个类


相关文章
|
1月前
|
存储 SQL 小程序
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
这篇文章详细介绍了Java虚拟机(JVM)的运行时数据区域和JVM指令集,包括程序计数器、虚拟机栈、本地方法栈、直接内存、方法区和堆,以及栈帧的组成部分和执行流程。
31 2
JVM知识体系学习五:Java Runtime Data Area and JVM Instruction (java运行时数据区域和java指令(大约200多条,这里就将一些简单的指令和学习))
|
22天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
22天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
15 1
|
1月前
|
存储 算法 Java
深入理解Java虚拟机(JVM)及其优化策略
【10月更文挑战第10天】深入理解Java虚拟机(JVM)及其优化策略
41 1
|
1月前
|
安全 Java API
🌟探索Java宇宙:深入理解Java技术体系与JVM的奥秘
本文深入探讨了Java技术体系的全貌,从Java语言的概述到其优点,再到Java技术体系的构成,以及JVM的角色。旨在帮助Java开发者全面了解Java生态,提升对Java技术的认知,从而在编程实践中更好地发挥Java的优势。关键词:Java, JVM, 技术体系, 编程语言, 跨平台, 内存管理。
34 2
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
41 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
6天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
4天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
7 1
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
60 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS