JVM详解:类的加载过程

news2024/11/19 15:41:25

JVM中类的加载主要分为三个部分,分别为加载(loading),链接(linking),初始化(initing)。其中加载负责的主要是讲类文件加载到内存中变为类对象,不过此时只有基本结构,没有实际数据。链接阶段还会被细分为验证,准备和解析,其中验证时对字节码文件进行格式校验(实际上所有阶段在执行前都会对自己需要的数据进行校验),准备阶段则是对静态属性进行默认赋值以及类型校验,而解析阶段则是这个环节最重要的,主要是将加载阶段没有加载的结构进行加载,这就相当于加载阶段是加载了一个和简单的骨架,而链接阶段则是将剩余的骨架补齐。最终的初始化阶段则是最终的赋值,为这具骨头附加血肉,这三个阶段的具体实现如下:

1. 加载(loading)

JVM通过加载java服务的启动类来开启运行java服务,任何非启动类的加载都是由另一个类的的的调用,触发加载的。一个类调用另一个类的逻辑在编译后的字节码文件中的体现是符号引用,当需要使用那个类时,才会通过符号引用,将其加载进内存中的。不过此时内存中保存的对象信息,包括方法属性常量等等,都只是符号引用,并不是真实引用。

那么什么是符号引用呢?具体我也不清楚,但可以知道的是,在没加载类或方法之前,其是保存在字节码文件中,而符号引用其中保存了可以找到目标类或方法的字节码文件的信息(不仅仅是这个作用,符号饮用记录了很多类或方法等,其表示的东西的信息)。

1.1 加载前的检查

JVM的类加载前需要进行大量的检查,大致包括如下内容:

  • 判断是否已经加载过这类了,如果出现类名相同,内容不同的情况,直接报错LinkageError。
  • 检查类文件格式是否正确,可能会抛出ClassFormatError(文件格式不正确)和ClassFormatError(版本不对)的错误。
  • 检查找到的类文件和符号饮用的描述是否一致,如果不是目标类,则报错NoClassDefFoundError。
  • 加载其父类的符号引用,如果父类是接口(IncompatibleClassChangeError)或自身(ClassCircularityError),则会报错。

一旦检查通过,就会将当前类记录在其对应的类加载器中,表示类已经加载过了。

类加载器:见名知意,就是讲类加载进内存的东西,不过随着类的分布位置不同,也需要不同的类加载器,JVM内置了三种类加载器,其中包括:

  • 引导类加载器(Bootstrap ClassLoader):这是 JVM 中最底层的类加载器,负责加载 Java 核心库(如 java.lang.*java.util.* 等),这些类位于 JDK 中的 jre/lib/rt.jar 或类似的系统路径下。它由 JVM 提供并且是不可见的,无法直接实例化。
  • 扩展类加载器(Extension ClassLoader):负责加载 JDK 扩展库(即 jre/lib/ext 目录下的类,或者 java.ext.dirs 配置的路径下的类)。它也是由 JVM 提供的,但与引导类加载器不同,它加载的是 Java 扩展库。
  • 应用类加载器(Application ClassLoader):负责加载用户应用程序的类路径(classpath)中的类,通常就是你在命令行中通过 java -cpjava -classpath 指定的类路径。这是最常用的类加载器,用于加载 Java 程序中定义的类。

但是创建这三个类加载器的目的是什么呢?为什么不直接把类放在一起加载呢?

首先这一定和性能无关,因为我与其使用三种加载器,不如将一种加载器复制三份。那么作用就一定是功能性的了,大概率是为了解藕不同功能的类,防止在一起互相影响,引发不可预料的事件。

除了JVM内置的加载器外,JVM还允许开发者自定义加载器,这样就可以让开发者自己处理一些刁钻的类的位置,比如在另一个服务器,或数据库中的类等等。

1.2 类的加载过程

JVM类的加载过程,大概可以将类分为两种,一种是数组类,一种是实例类(也就是非数组类)。

实例类

加载实例类时,JVM会检查类是否加载过,如果加载过,则认为其已经在内存中,跳过加载过程,如果不在,则将符号引用交给类加载器,让其寻找并加载对应的类。

当类加载或解析失败时,类加载器会抛出LinkageError或其子类的实例,当第一次加载,没有找到类的字节码文件时,则会跑出LinkageError的子类ClassNotFoundException的实例。

同样的NoClassDefFoundError也是LinkageError的子类实例,不同的是其通常是类在某个时刻加载过,但后续没有找到。JVM规定两次查找相同的类必须返回同一类对象,所以也可以说NoClassDefFoundError是在JVM的堆中没有找到类对象,而ClassNotFoundException是在磁盘中没有找到类的字节码文件。

数组类

而对于数组类,其没有二进制表示,是由JVM拿到相应参数来创建的。JVM会优先检查数组类内部元素是否为引用类型,如果是,其类是否在之前加载过,如果没有则会先加载器内部元素的类(如果元素的类还包含其他的类,则递归这一过程)。最终在由JVM创建数组类。如果不是,该数组类会被标记为引到类加载器加载的类(防止下一次重复检查,直接加载即可)。

1.3 类的加载约束

假设两个类加载器,加载了两个同名内容却不同的类,由于JVM规定一个类只允许加载一次,当一个类加载完成后,另一个加载时会被当成已经加载过了,那么在使用这类时,就会出现类的实际行为与预期不一致的问题。

为了防止这种情况的发生,JVM采用了一种名为类加载约束的方式。JVM要求加载类之前,如果发现了同名的类,必须保证其符号引用中记录的类格式一样(符号饮用的另一个作用,在编译期间生成的符号饮用会记录类的格式)。如果不同,则会返回LinkageError的错误。

实际上述情况还有另一种说明,就是根本不会发生类加载前的格式检查,而是同名类会直接拒绝加载,使用内存中的,如果与预期的类不一致,则会直接抛出LinkageError的错误,具体实现会在不同版本的JVM中有所差异,不过归根到底都是遇到同类名不同内容时,会返回错误。

那么如果真的出现了同名的类,哪个类会被加载进内存呢?这就和类加载器的执行顺序有关了,jvm的类加载器按照向上检查,向下加载的顺序来决定有哪个类加载器来加载这个类,这是什么意思呢?

类加载器之间也是有继承关系的(这个在java代码中可见),其中引导类加载器(Bootstrap ClassLoader)为所有类加载器的顶级父类,其子类为扩展类加载器(Extension ClassLoader),然后是应用类加载器(Application ClassLoader),自定义加载器为应用类加载器的子类。向上检查则是从自定义加载器开始检查,是否加载过这个类,如果加载过则无需重复加载。这一过程是从自定义加载器开始,逐渐顺着继承链向上检查。当检查到**引导类加载器(Bootstrap ClassLoader)时则证明没有加载过这个类,再从引导类加载器(Bootstrap ClassLoader)**开始判断是否有记载这个类的能力,如果有,则进行加载,如果没有则判断其子类,以此类推。大概过程如下图
在这里插入图片描述

当所有的类加载器都不能加载这个类时,由于类加载器的区别时根本加载类文件所处位置位置不同,所以没有类加载器能够加载这个类实际上就是没有在对应位置找到对应类文件,所以会返回ClassNotFoundException报错。

2. 连接(linking)

连接分为三个阶段,分别是验证,准备,以及解析

2.1 验证

在验证阶段,JVM会对文件再次进行检查,检查二进制文件(字节码文件)是否符合预期,如果不符合则会抛出异常LinkageError或其子类。

2.2 准备

在准备阶段,JVM会对加载类中的静态字段,并赋予其默认值(不是实际的值,实际的值会在解析阶段使其赋值)。并且还会对其实现或重写的接口或父类方法之前的类型进行对比检查(这会导致接口和父类的提前加载)。

2.3 解析

在类的加载阶段(loading),大量类的父类、字段、方法、接口等被作为符号引用,存在常量池中。而在linking的解析阶段,这些符号引用将被转换为真实引用,并将方法和属性加载到方法区中。但是对于反射和动态方法,这些无法在编译及加载期间确定其具体值的,必须在实际执行时,才能够进行符号引用到实际引用的变换操作。

JVM在解析阶段对方法和字段进行解析时,如果当前类寻找不到,则会在当前类的继承链或实现接口进行查找。如果还是没有找到,则会抛出错误。

在当前被加载的类中,未被使用的方法里保存的类,会被作为符号引用存在常量池中,不会被解析为实际引用,。不过正在加载类的整个继承链和接口,都会从子类的linking阶段的准备阶段开始初始化,并且需要在子类初始化完成之前初始化完成。父类和接口虽然需要提前被初始化完成,不过这也并不代表优先于子类初始化,而是会卡在连接阶段,只有在子类初始化时,父类和接口等才会被递归初始化,这些后面初始化阶段会说。

3. 初始化

由于JVM是多线程的,并且规定类不能被加载多个,所以又可能会出现多个线程使用一个类创建实例,或递归请求初始化某个类或接口的情况。为了解决这个问题,JVM使用了初始化锁来确保线程间初始化的安全。

在当前线程得到初始化锁后,JVM会首先会初始化全部的常量和静态变量(连接期间赋予的是默认值,现在才会赋予真实值),然后判断自己的父类是否已经初始化(如果父类在之前使用过,就是已经初始化的状态),如果没有则开始对自己的父类或接口等进行递归初始化。

父类和初始化完毕后,此时会开始执行静态代码块的代码(从最高级的祖父类开始,逐渐向下级执行,因为子类的初始化完成,依赖于父类和接口的初始化完成)。静态代码一旦执行成功,那么就代表整个初始化环节结束,线程会释放初始化锁。

类初始化后,所有的方法都已经实际存在于内存中,此时还需要将本地代码(也就是其他语言的代码),和已经加载的方法进行绑定操作,让本地方法能够正常调用。到此一个类的加载就完成了。

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

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

相关文章

FPGA开发流程

注:开发板:小梅哥的ACX720。本实验可直接运行在小梅哥的ACX720开发板上,后续的实验都可直接运行在小梅哥的ACX720上。 一、打开VIVADO并创建工程 1、双击VIVADO图标,打开vivado。 2、打开vivado界面打,点击有 Create …

免费开源!DBdoctor推出开源版系统诊断工具systool

​前言 在开发和运维过程中,经常会遇到难以定位的应用问题,我们通常需要借助Linux系统资源监控工具来辅助诊断。然而,系统的IO、网络、CPU使用率以及文件句柄等信息通常需要通过多个独立的命令工具来获取。在没有部署如Prometheus这样的综合…

Restful API接⼝简介及为什么要进⾏接⼝压测

一、RESTful API简介 在现代Web开发中,RESTful API已经成为一种标准的设计模式,用于构建和交互网络应用程序。本文将详细介绍RESTful API的基本概念、特点以及如何使用它来设计高效的API接口。 1. 基于协议 HTTP 或 HTTPS RESTful API通常使用HTTP&am…

R语言统计分析与MATLAB数学建模书籍推荐

文章目录 一、《R语言统计分析与可视化》1.1 内容核心1.2 内容简介 二、《MATLAB数学建模从入门到精通》2.1 关键点2.2 内容简介2.3 作者简介 一、《R语言统计分析与可视化》 R语言统计分析与可视化从入门到精通。学R语言、练语法、取数据、预处理、可视化、回归分析、方差分析…

智慧社区平台系统提升物业管理效率与居民生活质量

内容概要 智慧社区平台系统是为应对现代城市管理挑战而诞生的重要工具。随着城市化进程的加快,传统的物业管理方式已经难以满足日益增长的居民需求和管理复杂性。因此,引入智能化管理手段显得尤为重要。这个系统不仅仅是一个简单的软件,它是…

【ASR技术】WhisperX安装使用

介绍 WhisperX 是一个开源的自动语音识别(ASR)项目,由 m-bain 开发。该项目基于 OpenAI 的 Whisper 模型,通过引入批量推理、强制音素对齐和语音活动检测等技术。提供快速自动语音识别(large-v2 为 70 倍实时&#xf…

STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组

STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组 STM32CUBE开发环境集成了STM32 HAL库进行FreeRTOS配置和开发的组件,不需要用户自己进行FreeRTOS的移植。这里介绍最简化的用户操作类应用教程。以STM32F401RCT6开发板为…

力扣(leetcode)题目总结——动态规划篇

leetcode 经典题分类 链表数组字符串哈希表二分法双指针滑动窗口递归/回溯动态规划二叉树辅助栈 本系列专栏:点击进入 leetcode题目分类 关注走一波 前言:本系列文章初衷是为了按类别整理出力扣(leetcode)最经典题目&#xff0c…

计算器的实现

计算器的实现 计算器实现思路 我们⽇常写的计算表达式都是中缀表达式,也就是运算符在中间,运算数在两边,但是直接读取⽆ 法⻢上进⾏运算因为⼀个计算表达式还涉及运算符优先级问题。如: 都⽆法运算,因为后⾯还有括号优…

Python蓝桥杯刷题1

1.确定字符串是否包含唯一字符 题解:调用count函数计算每一个字符出现的次数,如果不等于1就输出no,并且结束循环,如果等于1就一直循环直到计算到最后一个字符,若最后一个字符也满足条件,则输出yes import…

《基于 PySpark 的电影推荐系统分析及问题解决》

以下是一篇关于上述代码的博客文章: 基于PySpark的电影推荐系统实现与分析 在当今数字化时代,个性化推荐系统在各个领域中都发挥着至关重要的作用,尤其是在娱乐行业,如电影推荐。本文将详细介绍如何使用PySpark构建一个简单的电…

每天五分钟深度学习pytorch:批归一化全连接网络完成手写字体识别

本文重点 前面我们学习了普通的全连接神经网络,后面我们学习了带有激活层的全连接神经网络,本文我们继续进一步升级,我们学习带有批归一化的全连接神经网络,批归一化可以加快神经网络的训练速度,减少过拟合,具体它的原理,大家可以看我们的《每天五分钟深度学习》专栏,…

JavaWeb后端开发知识储备1

目录 1.DTO/VO/PO 2.MVC架构/微服务架构 3.JWT令牌流程 4.ThreadLocal 5.接口路径/路径参数 6.自定义注解 1.DTO/VO/PO 1.1 DTO DTO 即 Data Transfer Object—— 数据传输对象,是用于传输数据的对象,通常在服务层与表现层之间传递数据&#xff…

什么是SMARC?模块电脑(核心板)规范标准简介三

1. 概念 SMARC(Smart Mobility ARChitecture,智能移动架构)是一种通用的小型计算机模块定义,基于ARM和X86技术的模块化计算机低功耗嵌入式架构平台,旨在满足低功耗、低成本和高性能的应用需求。这些模块通常使用与平板…

Filebeat升级秘籍:解锁日志收集新境界

文章目录 一、什么是filebeat二、Filebeat的工作原理2.1 filebeat的构成2.1.1 Prospector 组件2.1.2 Harvester 组件 2.2 filebeat如何保存文件的状态2.3 filebeat何如保证至少一次数据消费 三、Filebeat配置文件四、filebeat对比fluented五、Filebeat的部署安装5.1裸金属安装5…

C++小白实习日记——Day 4 将本地项目上传到gitee

生活就像一坨狗屎 我跑的代码老板说耗时太长了,不知道要怎么做才能耗时小一点 老板把我加到企业gitee里了,让我将代码上传到个人仓库: 新建一个文件夹当做库文件,点git bash here——> git init——>git config --global…

qiankun主应用(vue2+element-ui)子应用(vue3+element-plus)不同版本element框架css样式相互影响的问题

背景:qiankun微前端架构实现多应用集成 主应用框架:vue2 & element-ui 子应用框架:vue3 & element-plus >> 问题现象和分析 登录页面是主应用的,在登录之后才能打开子应用的菜单页面,即加载子应用。 首…

云渲染,解决houdini特效缓存太大上传太慢的问题

对于从事 Houdini 创作的艺术家和设计师们来说,使用云渲染的朋友,缓存太大导致云渲染上传慢一直是一个令人头疼的问题。然而,现在有了成都渲染 101 云渲染,这个难题迎刃而解。Houdini 以其强大的功能能够创建极为复杂和逼真的特效…

前端开发迈向全栈之路:规划与技能

一、前端开发与全栈开发的差异 前端开发主要负责构建和实现网页、Web 应用程序和移动应用的用户界面。其工作重点在于网页设计和布局,使用 HTML 和 CSS 技术定义页面的结构、样式和布局,同时运用前端框架和库如 React、Angular 或 Vue.js 等构建交互式和…