2、JVM 类加载机制深度剖析

今天我们就来看看JVM的类加载机制到底是怎么样的,搞清楚这个过程了,那么以后在面试时,对面试官常问的JVM类加载机制,就能把一些核心概念说清楚了。

2.1、JVM在什么情况下会加载一个类?

类加载过程虽然繁琐复杂,但在日常工作中,我们主要需要掌握其核心工作原理。一个类从加载到使用,通常会经历以下过程:加载、验证、准备、解析、初始化、使用和卸载。

首先要弄清楚的问题是,在执行我们编写的代码过程中,JVM在什么情况下会加载一个类?也就是说,什么时候会从“.class”字节码文件中加载这个类到JVM内存中?其实答案很简单,就是在你代码中使用到这个类的时候。

以一个简单的例子来说明,假设有一个类(User.class),其中包含一个“main()”方法作为主入口。那么,一旦JVM进程启动,它一定会先将这个类(User.class)加载到内存中,然后从“main()”方法的入口代码开始执行。

public class User {
    public static void main() {
        // 业务代码
    }
}

我们还是坚持一步一图,大家先看看下图,感受一下:
在这里插入图片描述

接着假设上面的代码中,出现了如下的这么一行代码:

public class User {
    public static void main() {
        // 业务代码
        UserManager userManager = new UserManager();
    }
}

在编程过程中,我们经常会遇到需要实例化某个类的对象的情况。例如,我们的代码中可能需要使用“UserManager”这个类来创建一个对象。在这种情况下,我们需要将“UserManager.class”字节码文件中的这个类加载到内存中,以便程序能够正常运行。

那么,这个过程是如何实现的呢?

首先,当我们的代码中出现需要实例化“UserManager”这个类的对象时,JVM会触发类加载器进行工作。类加载器的主要职责就是从字节码文件(如“UserManager.class”)中加载对应的类到内存中。

在这个过程中,类加载器会读取“UserManager.class”字节码文件,解析其中的二进制数据,然后将其转换为Java虚拟机可以识别和执行的指令。这样,当代码中需要创建“UserManager”的对象时,JVM就可以直接从内存中获取到这个类的相关信息,从而完成对象的实例化。我们来看下面的图:
在这里插入图片描述

上述内容是一个例子,旨在帮助大家更好地理解。可以简单概括为:

在Java程序中,当启动JVM(Java虚拟机)进程后,主类中的"main()"方法将被加载到内存中并执行。这个主类通常包含一个名为"main()"的方法,它是程序的入口点,用于启动应用程序。

如果在"main()“方法中使用了其他类,比如"UserManager”,那么对应的类将会被加载到内存中。这些类是通过从相应的".class"字节码文件中加载而得到的。

总结来说,JVM进程启动后,主类中的"main()"方法将被加载并执行,如果使用了其他类,它们也会相应地被加载到内存中。

2.2、轻松掌握验证、准备与初始化的必备步骤

关于类加载时机的问题,对于许多有经验的开发者来说,这可能并不是什么难题。然而,对于那些刚刚踏入编程世界的新手而言,这是一个至关重要且需要清晰理解的概念。下面,我们将从实际应用的角度,简洁地介绍另外三个相关概念:验证、准备和初始化。

实际上,对于这三个概念,我们并不需要深入挖掘其内部细节。这些细节繁琐复杂,对于大多数开发者来说,只需在脑中形成以下基本概念即可。

2.2.1、验证阶段

简单来说,这一步骤的目的是根据Java虚拟机规范,对加载进来的".class"文件内容进行校验,以确保其符合特定的规范。

这个过程中,我们首先要理解的是,如果一个".class"文件被篡改,其中的字节码可能完全不符合JVM规范。在这种情况下,JVM将无法执行这个字节码。

因此,在将".class"文件加载到内存之后,必须对其进行验证。只有当校验结果显示该文件完全符合JVM规范时,才能将其交给JVM进行运行。

以下是相应的代码示例,用于演示如何进行".class"文件的校验:

import java.io.*;
import java.util.jar.*;

public class ClassFileVerifier {
    public static void main(String[] args) {
        String classFilePath = "path/to/your/ClassFile.class";
        try {
            // 读取".class"文件
            byte[] classBytes = readClassFile(classFilePath);

            // 创建ClassReader对象
            ClassReader reader = new ClassReader(classBytes);

            // 创建Verifier对象,用于校验".class"文件内容是否符合JVM规范
            Verifier verifier = new Verifier();

            // 调用verify方法进行校验
            verifier.verify(reader);

            // 校验通过,可以进行后续操作
            System.out.println("Class file is valid and can be executed by JVM.");
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassFormatError e) {
            System.err.println("Class file format error: " + e.getMessage());
        }
    }

    private static byte[] readClassFile(String filePath) throws IOException {
        FileInputStream fis = new FileInputStream(filePath);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = fis.read(buffer)) != -1) {
            baos.write(buffer, 0, bytesRead);
        }
        fis.close();
        return baos.toByteArray();
    }
}

上述代码中,我们使用ClassReaderVerifier类来对".class"文件进行校验。首先,通过readClassFile方法读取".class"文件的字节码内容,然后创建一个ClassReader对象,并将其传递给Verifier对象的verify方法进行校验。如果校验通过,则输出"Class file is valid and can be executed by JVM.",表示该文件符合JVM规范,可以由JVM执行。如果校验失败,将抛出相应的异常。下面用一张图,展示了这个过程:
在这里插入图片描述

2.2.2、准备阶段

在当前的这个阶段,我们能够轻松理解一个基本概念。在我们编写的类中,通常会包含一些类变量。例如,我们来看下面的“UserManager”这个类:

public class UserManager {
    public static int demoNum;
}

当你有一个名为"UserManager"的类时,在将其"UserManager.class"文件的内容加载到内存后,首先会进行验证,以确保该字节码文件的内容是符合规范的。接下来,会进行一些准备工作。

这个准备工作主要包括为"UserManager"类分配一定的内存空间,然后为其内部的类变量(即使用static修饰的变量)分配内存空间,并赋予它们一个默认的初始值。

以示例中的"demoNum"类变量为例,会为其分配内存空间,并将其初始值设置为0。整个过程,如下图所示:
在这里插入图片描述

2.2.3、解析阶段

在这个阶段,我们实际上是在进行一种替换,将符号引用转换为直接引用。这个过程的复杂度较高,深入涉及到JVM的底层机制。

然而从实践的角度来看,对于大多数同学来说,在日常工作中应用JVM技术时,可能并不需要深入到这个阶段。所以,目前大家只需要了解这个阶段的存在和它的基本概念就足够了。同样,我还是给大家画图展示一下:
在这里插入图片描述

2.2.4、三个阶段的小结

在这三个阶段中,最需要大家关注的核心阶段是“准备阶段”。

在这个阶段,系统会为加载进来的类分配内存空间。具体来说,它为类的实例变量和类变量分配了内存空间。同时,这些变量也会被赋予默认的初始值。

这个重要的概念,大家必须牢记于心。

2.3、核心阶段:初始化

之前说过,在准备阶段时,就会把我们的“UserManager”类给分配好内存空间
另外他的一个类变量“demoNum”也会给一个默认的初始值“0”,那么接下来,在初始化阶段,就会正式执行我们的类初始化的代码了。
那么什么是类初始化的代码呢?我们来看看下面这段代码:

在之前的讨论中,我们提到,在准备阶段,会为我们的“UserManager”类分配内存空间。此外,该类的一个类变量“demoNum”会被赋予一个默认的初始值“0”。接下来,在初始化阶段,将会正式执行类的初始化代码。

那么,什么是类的初始化代码呢?让我们来观察下面的代码片段:

public class UserManager {    
    public static int demoNum = Configuration.getInt("config.demoNum");
}

这段代码展示了在初始化阶段,类初始化代码的执行情况。它包含了一些特定的操作和设置,用于确保类的正确初始化和运行。通过这段代码,我们可以了解到如何进行类的初始化操作以及相关的逻辑处理。

大家可以看到,对于“demoNum”这个类变量,我们计划通过Configuration.getInt("config.demoNum")这段代码来获取一个值,并将其赋值给它。

然而,在准备阶段会执行这个赋值逻辑吗?答案是否定的!在准备阶段,我们仅仅是为“demoNum”类变量分配内存空间,并赋予其初始值“0”。

那么这段赋值的代码何时执行呢?答案是在初始化阶段进行。

在这个阶段,我们会执行类的初始化代码,例如上面的Configuration.getInt("config.demoNum")代码将在这里执行。它将完成一个配置项的读取,然后将值赋给类变量“demoNum”。

此外,比如下面的static静态代码块,也会在这个阶段执行。可以理解为类初始化的时候,调用“loadUserInfoFromDish()”方法从磁盘中加载数据副本,并且放在静态变量“userInfos”中:

public class UserManager {    
    public static int demoNum = Configuration.getInt("config.demoNum");
    public static Map<String, UserInfo> userInfos;
    static {
        loadUserInfoFromDB();
    }
    public static void loadUserInfoFromDB() {
        this.userInfos = new HashMap<String, UserInfo>();
    }
}

理解了类的初始化是什么之后,我们接下来探讨类的初始化规则。

那么,何时会进行类的初始化呢?通常有以下几种情况:

  1. 当我们使用 “new UserManager()” 这样的语句来实例化一个类的对象时,会触发类的加载到初始化的全过程。在这个过程中,系统会先准备好这个类,然后实例化出一个对象。

  2. 对于包含 “main()” 方法的主类,必须立即进行初始化。

此外,还有一个非常重要的规则需要了解:在初始化一个类的过程中,如果发现其父类尚未初始化,必须先初始化其父类。比如下面的代码:

public class UserManager extends AbstractBaseManager{    
    public static int demoNum = Configuration.getInt("config.demoNum");
    public static Map<String, UserInfo> UserInfos;
    static {
        loadUserInfoFromDB();
    }
    public static void loadUserInfoFromDB() {
        this.UserInfos = new HashMap<String, UserInfo>();
    }
}

在尝试通过 “new UserManager()” 语句创建该类的实例时,系统将会先加载 UserManager 类。然而,在初始化 UserManager 类之前,系统会检查发现其父类 AbstractManager 尚未被加载和初始化。

为了解决这个问题,系统必须首先加载 AbstractBaseManager 父类,并完成其初始化过程。只有当 AbstractBaseManager 父类加载和初始化完成后,UserManager 类才能继续进行加载和初始化。

这一步骤确保了在初始化子类之前,所有依赖的父类已经被正确地加载和初始化。这是面向对象编程中的一个重要概念,有助于避免运行时错误和未预期的行为。
这个规则,大家必须得牢记,再来一张图,借助图片来进行理解:
在这里插入图片描述

2.4、类加载器和双亲委派机制

现在,我相信大家已经对整个类加载的过程有了清晰的理解,从触发的时机到初始化的过程。接下来,我将向大家介绍类加载器的概念。

类加载器是实现上述过程的关键工具。在Java中,有多种类型的类加载器,简单来说,主要包括以下几种:

2.4.1、启动类加载器

Bootstrap ClassLoader,主要负责加载我们计算机上安装的Java目录下的核心类。

众所周知,无论是在Windows笔记本还是Linux服务器上运行自己编写的Java程序,都需要安装JDK。

在你的Java安装目录中,会有一个名为“lib”的文件夹,你可以自行查找。这个文件夹包含了一些支撑您Java系统运行的最核心类库。

因此,每当你的JVM启动时,首先会依赖启动类加载器,去加载Java安装目录下的“lib”目录中的核心类库。

2.4.2、扩展类加载器

Extension ClassLoader,这是一个类加载器,其工作原理与之前描述的类似。在Java安装目录下,会有一个名为“lib\ext”的目录。这个目录中存放了一些类文件,这些类文件需要通过这个类加载器进行加载,以支持系统的运行。

当JVM启动时,它确实需要从Java安装目录下的“libext”目录中加载这些类。

2.4.3、应用程序类加载器

Application ClassLoader,这是一种类加载器,它的主要职责是加载由“ClassPath”环境变量所指定的路径中的类。你可以大致理解为,它负责将你编写的Java代码加载到内存中。这个类加载器的工作就是将你所编写的那些类加载到内存里。

2.4.4、自定义类加载器

除了上面那几种之外,还可以自定义类加载器,去根据你自己的需求加载你的类。

2.4.5、双亲委派机制

JVM的类加载器采用分层结构,其中启动类加载器位于最顶层,负责加载核心Java库中的类。紧接着是扩展类加载器,它位于第二层,主要负责加载Java安装目录的ext文件夹下的类。第三层是应用程序类加载器,负责加载应用程序中的所有类。最后一层是自定义类加载器,允许开发人员根据需要自定义加载策略来加载特定路径下的类。大家看下图:
在这里插入图片描述

在Java中,类加载器遵循一个称为双亲委派的层级结构。这个机制的核心思想是,当一个类加载器需要加载一个类时,它首先会将这个任务委托给它的父类加载器。这个过程会一直上溯到顶层的类加载器,也就是启动类加载器。

具体来说,如果一个类不在当前类加载器的加载范围内,它会将加载请求委托给其父类加载器。如果父类加载器也无法加载该类,它会进一步将请求委托给更上层的类加载器。这个过程会一直持续,直到找到能够加载该类的类加载器为止。

让我们通过一个例子来说明这个过程。假设JVM需要加载一个名为"UserManager"的类。应用程序类加载器首先会询问其父类加载器,也就是扩展类加载器,是否能够加载这个类。扩展类加载器会进一步询问其父类加载器,也就是启动类加载器,是否能够加载这个类。

如果启动类加载器在其负责的目录(例如Java安装目录)中找不到这个类,它会将加载请求回传给扩展类加载器。扩展类加载器在自己的负责目录中也找不到这个类,它会将加载请求回传给应用程序类加载器。

最后,应用程序类加载器在自己的负责范围(例如用户编写的jar包)中找到了"UserManager"类,并将其加载到内存中。

这就是双亲委派模型的工作原理:首先尝试由父类加载器加载类,如果失败则由子类加载器加载。这种机制可以避免重复加载同一个类,确保了类的唯一性和一致性。

最后,再通过一张图来感受一下类加载器的双亲委派模型。
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一文读懂Partisia Blockhain:兼顾去中心化、安全性与可扩展性

“Partisia Blockhain 解决了区块链领域长期存在的问题&#xff0c;其兼顾了去中心化、安全性以及可扩展性” Partisia Blockchain 是一个具有独特零知识证明预言机以及分片解决方案的 Layer1&#xff0c;解决了困扰整个区块链行业的问题。 目前&#xff0c;多样化的区块链层出…

【Linux驱动层】iTOP-RK3568学习之路(二):vscode中设置头文件路径-完成代码自动补全

在Ubuntu下用vscode写Linux驱动层的时候&#xff0c;需要添加头文件&#xff1a; #include<linux/module.h> #include<linux/init.h> #include<linux/kernel.h>但vscode没有智能提示&#xff0c;因此需要我们手动添加自己的头文件路径&#xff1a; topeetu…

个人主页源码 翻盖式LOGO

源码介绍 衍生自 Vno Jekyll 主题页面部分加载效果借鉴于 Mno Ghost 主题借鉴了北岛向南的小屋的头像样式主页的 Logo 字体已经过压缩&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改 效果截图 源码下载 个人主页源码 翻盖式LOGO…

【C++】双指针算法:盛最多水的容器

1.题目 2.算法思路 有两种方法&#xff1a; 第一种&#xff1a; 暴力穷举法&#xff0c;就是用两次循环将所有的可能性算出来&#xff0c;然后求出最大值。 这种方法最容易想到&#xff0c;但时间复杂度是O(n^2)&#xff0c;一定会超时的&#xff01; 第二种&#xff1a; …

【面试经典 150 | 数组】最后一个单词的长度

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;遍历 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对于本题涉及到的数据结构等内容进行回顾…

数字陷波器的设计

数字陷波器的设计 陷波器&#xff1a;一种特殊的带阻滤波器&#xff0c;其阻带在理想情况下只有一个频率点&#xff0c;主要用于消除某个特定频率的干扰。 例子 设计一个数字陷波器将输入信号中的50Hz工频干扰信号滤除&#xff0c;尽可能保留其他频率成分&#xff0c;设系统…

【论文笔记】基于预训练模型的持续学习(Continual Learning)(增量学习,Incremental Learning)

论文链接&#xff1a;Continual Learning with Pre-Trained Models: A Survey 代码链接&#xff1a;Github: LAMDA-PILOT 持续学习&#xff08;Continual Learning, CL&#xff09;旨在使模型在学习新知识的同时能够保留原来的知识信息了&#xff0c;然而现实任务中&#xff…

Xxl-job适配达梦数据库

项目说明 项目本身开发中采用定时框架&#xff1a;xxl-job是一个分布式任务调度平台&#xff0c;它是依托于MySQL数据库执行。但后续客户要求必须满足信创环境&#xff0c;因此调整MySQL数据库为达梦数据库。由此就有了xxl-job适配达梦数据库的一系列操作。 Xxl-job表结构导入…

spring注解驱动系列-- BeanPostProcessor与BeanFactoryPostProcessor

一、BeanPostProcessor与BeanFactoryPostProcessor的定义 一、BeanPostProcessor bean后置处理器&#xff0c;bean创建对象初始化前后进行拦截工作的 二、BeanFactoryPostProcessor beanFactory的后置处理器&#xff0c;在BeanFactory标准初始化之后调用&#xff0c;来定制和…

服务器Linux上杀死特定进程的命令:kill

1、查看用户XXX正在运行的进程 top -u xxx2、查看想要杀死的进程对应的PID 先找到此进程对应的命令 取其中的main-a3c.py即可 ps -aux | grep main-a3c.py可以看到对应的PID是1325390使用kill杀死对应PID的进程 kill -9 1325390成功&#xff0c;gpustat可以看到之前一直占…

JVM虚拟机(十二)ParallelGC、CMS、G1垃圾收集器的 GC 日志解析

目录 一、如何开启 GC 日志&#xff1f;二、GC 日志分析2.1 PSPO 日志分析2.2 ParNewCMS 日志分析2.3 G1 日志分析 三、GC 发生的原因3.1 Allocation Failure&#xff1a;新生代空间不足&#xff0c;触发 Minor GC3.2 Metadata GC Threshold&#xff1a;元数据&#xff08;方法…

Microchip 32位MCU CAN驱动图文教程-附源码

文章目录 创建一个新的32位MCU工程Microchip MCC Harmony配置界面说明在MCC下配置系统的时钟在MCC下配置所需要使用的模块配置调试打印模块配置CAN模块配置管脚功能修改系统堆栈大小生成代码 添加用户代码 创建一个新的32位MCU工程 确保电脑上已经安装最新的MPlab X IDE、XC32编…

【Qt】探索Qt框架:跨平台GUI开发的利器

文章目录 1. Qt框架概述1.1. Qt框架的优点1.2. Qt框架支持的系统1.3. Qt开发环境 2. 搭建 Qt 开发环境2.1. Qt SDK 的下载和安装2.2. 新建项目: 3. Qt 框架内容简介总结 在当今软件开发领域&#xff0c;跨平台性和用户界面的友好性是至关重要的。而Qt框架作为一款跨平台的C图形…

西安大秦软件

西安大秦软件 大秦软件 想做小程序、APP、Web 系统&#xff0c;请找我&#xff0c;包您满意&#xff01; 刘大强 &#xff08;销售经理&#xff09; 电话&#xff1a;198 8892 6712 微信&#xff1a;198 8892 6712 欢迎咨询 西安大秦时代网络科技有限公司

认知觉醒 PDF电子版 下载

认知觉醒 PDF电子版 开启自我改变的原动力 周岭 / 人民邮电出版社 / 2020-10 链接&#xff1a;https://pan.baidu.com/s/1EHUK_AhvE5TWAZsYXFQ5QA?pwdwrho 提取码&#xff1a;wrho

面试后,公司如何决定你的去留

在现代职场中&#xff0c;求职者在经历了一系列严格的面试流程后&#xff0c;往往会进入一段等待期。在这段时间里&#xff0c;他们满怀希望地等待企业的最终反馈。但有一个现象普遍存在&#xff1a;无论面试过程如何&#xff0c;最终决定权总是掌握在公司手中&#xff0c;由公…

【Python性能优化】list、array与set

list、array与set 详述测试代码 详述 本文对比 list 与 set 在插入和取值时的性能差异&#xff0c;以提供一条什么时候该选择什么数据类型的建议。先上结果&#xff1a; array 与 list 的不同&#xff1a; 内存方面 array 是 C array 的包装&#xff0c;它直接存储数据&#xf…

Llama 3大模型发布!快速体验推理及微调

Meta&#xff0c;一家全球知名的科技和社交媒体巨头&#xff0c;在其官方网站上正式宣布了一款开源的大型预训练语言模型——Llama-3。 据了解&#xff0c;Llama-3模型提供了两种不同参数规模的版本&#xff0c;分别是80亿参数和700亿参数。这两种版本分别针对基础的预训练任务…

JVM-垃圾收集算法

前言 在 Java 中&#xff0c;垃圾收集&#xff08;Garbage Collection&#xff09;是一种自动管理内存的机制&#xff0c;它负责在运行时识别和释放不再被程序使用的内存&#xff0c;从而避免内存泄漏和悬空引用问题。本篇文章将介绍三种常见的垃圾收集算法。 标记-清除&…

OCT2Former: A retinal OCT-angiography vessel segmentationtransformer论文总结

论文(COMPUT METH PROG BIO)&#xff1a;OCT2Former: A retinal OCT-angiography vessel segmentation transformer 源码&#xff1a;https://github.com/coreeey/OCT2Former 一、摘要 背景与目的&#xff1a;视网膜血管分割在视网膜疾病自动筛查与诊断中起着重要作用。如何分…