代码编织梦想

今天我们就来看看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"类,并将其加载到内存中。

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

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yangbaggio/article/details/137854189

jvm-爱代码爱编程

目录 一、什么是类加载器 二、类加载器的分类         启动类加载器         Java中的默认类加载器         扩展类加载器         应用程序类加载器 三、双亲委派机制         双亲委派机制,解决的三个问题:         双亲委派机制的作用 四、打破双亲委派机制         自定义类加

cms垃圾回收器为什么被移除-爱代码爱编程

CMS(Concurrent Mark-Sweep)垃圾回收器是Java虚拟机中一种以获取最短回收停顿时间为目标的收集器。CMS的主要特点是它允许垃圾回收线程与应用程序线程同时运行,尽可能减少应用程序的停顿时间。尽管它在多核

tcp超时重传机制-爱代码爱编程

一、TCP超时重传机制简介         TCP超时重传机制是指当发送端发送数据后,如果在一定时间内未收到接收端的确认应答,则会认为数据丢失或损坏,从而触发重传机制。发送端会重新发送数据,并等待确认应答。如果在多次重传后仍未收到确认应答,则会放弃发送,并报告连接异常。 二、Java中的TCP超时重传机制示例         下面通过一个简单的Jav

redis的数据淘汰策略——java全栈知识(19)-爱代码爱编程

Redis的数据淘汰策略 什么是数据淘汰策略 数据过期策略是 redis 中设置了 TTL 的数据过期的时候 Redis 的处理策略。数据淘汰策略是 Redis 内存不够的时候, 数据的淘汰策略:当 Redis 中的内存

ssm115乐购游戏商城系统+vue-爱代码爱编程

毕业生学历证明系统 设计与实现 内容摘要 如今社会上各行各业,都喜欢用自己行业的专属软件工作,互联网发展到这个时候,人们已经发现离不开了互联网。新技术的产生,往往能解决一些老技术的弊端问题。因为传统毕业生学历信息管理难度

jvm的原理与性能-爱代码爱编程

1 JVM 内存结构 1.1 运行时数据区 1.1.1 栈(虚拟机栈) 每个线程在创建时都会创建一个私有的Java虚拟机栈,在执行每个方法时都会打包成一个栈帧,存储了局部变量表、操作数栈、动态链接、方法出口等信息,

jvm类加载器-爱代码爱编程

1、什么是类加载器        类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。类加载器只参与加载过程中的字节码获取并加载到内存这一部分。 1.1、类加载器的作用         类加载器(ClassLoader)负责在类加载过程中的字节码获取并加载到内存这一部分。通过加载字节码数据放入内存

什么是jvm中的程序计数器-爱代码爱编程

在计算机的体系结构中: 程序计数器(Program Counter),通常缩写为 PC,是计算机体系结构中的一个寄存器,用于存储下一条指令的地址。程序计数器是控制单元的一部分,它的作用是确保程序能够按正确的顺序执行指令。 以下是程序计数器的一些关键特性 1.指令定位 程序计数器总是指向CPU中下一条要执行的指令内存地址。 2.顺序执行 在大多数

深入探究 java 虚拟机(jvm)中的栈(stack)和堆(heap)-爱代码爱编程

Java 虚拟机(JVM)是 Java 语言的核心部分,负责将 Java 代码翻译成可在计算机上执行的指令。在 JVM 中,内存管理是一个重要的话题,而栈(Stack)和堆(Heap)是其中两个最重要的内存区域。本文将深入探究 JVM 中的栈和堆,包括其概念、特点、以及在 Java 程序中的应用。 1. 栈(Stack)和堆(Heap)的概念 1.1

【jvm基础篇】类加载器分类介绍-爱代码爱编程

文章目录 类加载器什么是类加载器类加载器的作用是什么应用场景类加载器的分类启动类加载器用户扩展基础jar包 扩展类加载器和应用程序类加载器扩展类加载器通过扩展类加载器去加载用户jar包:

jvm之运行时数据区-爱代码爱编程

 Java虚拟机在运行时管理的内存区域被称为运行时数据区。   程序计数器: 也叫pc寄存器,每个线程会通过程序计数器记录当前要执行的字节码指令的地址。程序计数器在运行时是不会发生内存溢出的,因为每个线程只存储一个固定长度的内存地址。   JAVA虚拟机栈:采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存

【jvm类加载机制】深度剖析jvm类加载机制-爱代码爱编程

深度剖析JVM类加载机制 前言类加载运行全过程loadClass的类加载过程 类加载器和双亲委派机制类加载器的类型类加载器的初始化过程双亲委派机制为什么要设置双亲委派机制?全盘负责委托机制自定义类加载器