JVM系列(二) -类的加载过程

news2025/2/24 15:12:56

一、背景介绍

我们知道 Java 是先通过编译器将.java类文件转成.class字节码文件,然后再通过虚拟机将.class字节码文件加载到内存中来实现应用程序的运行。

那么虚拟机是什么时候加载class文件?如何加载class文件?class文件进入到虚拟机后发生了哪些变化?

今天我们就一起来了解一下,虚拟机是如何加载类文件的。

二、类加载的时机

经常有面试官问,“类什么时候加载”和“类什么时候初始化”,从内容上来说,似乎都在问同一个问题:class文件是什么时候被虚拟机加载到内存中,并进入可以使用的状态?

从虚拟机角度来说,加载初始化是类的加载过程中的两个阶段。

对于“什么时候加载”,Java 虚拟机规范中并没有约束,每个虚拟机实例都可以按自身需要来自由实现。但基本上都遵循类在进行初始化之前,需要先进行加载class文件。

对于“什么时候初始化”,Java 虚拟机规范有明确的规定,当符合以下条件时(包括但不限),并且虚拟机在内存中没有找到对应的类信息,必须对类进行“初始化”操作:

  • 使用new实例化对象时,读取或者设置一个类的静态字段或方法时
  • 反射调用时,例如Class.forName("com.xxx.Test")
  • 初始化一个类的子类,会首先初始化子类的父类
  • Java 虚拟机启动时标明的启动类,比如main方法所在的类
  • JDK8 之后,接口中存在default方法,这个接口的实现类初始化时,接口会在它之前进行初始化

类在初始化开始之前,需要先经历加载、验证、准备、解析这四个阶段的操作。

下面我们一起来看看类的加载过程。

三、类的加载过程

当一个类需要被加载到虚拟机中执行时,虚拟机会通过类加载器,将其.class文件中的字节码信息在内存中转化成一个具体的java.lang.Class对象,以便被调用执行。

类从被加载到虚拟机内存中开始,到卸载出内存,整个生命周期包括七个阶段:加载、验证、准备、解析、初始化、使用和卸载,可以用如下图来简要概括。

其中类加载的过程,可以用三个步骤(五个阶段)来简要描述:加载 -> 连接(验证、准备、解析)-> 初始化。(验证、准备、解析这3个阶段统称为连接

其次加载、验证、准备和初始化这四个阶段发生的顺序是确定的,必须按照这种顺序按部就班的开始,而解析阶段则不一定。在某些情况下解析阶段可以在初始化阶段之后开始,这是为了支持 Java 语言的运行时绑定,也称为动态绑定晚期绑定

同时,这五个阶段并不是严格意义上的按顺序完成,在类加载的过程中,这些阶段会互相混合,可能有些阶段完成了,有些阶段没有完成,会交叉运行,最终完成类的加载和初始化。

接下来依此分解一下加载、验证、准备、解析、初始化这五个步骤,这五个步骤组成了一个完整的类加载过程。使用没什么好说的,卸载通常属于 GC 的工作,当一个类没有被任何地方引用并且类加载器已被 GC 回收,GC 会将当前类进行卸载,在后续的文章我们会介绍 GC 的工作机制。

3.1、加载

加载是类加载的过程的第一个阶段,这个阶段的主要工作是查找并加载类的二进制数据,在虚拟机中,类的加载有两种触发方式:

  • 预先加载:指的是虚拟机启动时加载,例如JAVA_HOME/lib/下的rt.jar下的.class文件,这个jar包里面包含了程序运行时常用的文件内容,例如java.lang.*java.util.*java.io.*等等,因此会随着虚拟机启动时一起加载到内存中。要证明这一点很简单,自己可以写一个空的main函数,设置虚拟机参数为-XX:+TraceClassLoading,运行程序就可以获取类加载的全部信息
  • 运行时加载:虚拟机在用到一个.class文件的时候,会先去内存中查看一下这个.class文件有没有被加载,如果没有,就会按照类的全限定名来加载这个类;如果有,就不会加载。

无论是哪种触发方式,虚拟机在加载.class文件时,都会做以下三件事情:

  • 1.通过类的全限定名定位.class文件,并获取其二进制字节流
  • 2.将类信息、静态变量、字节码、常量这些.class文件中的内容放入运行时数据区的方法区
  • 3.在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口,一般这个java.lang.Class对象会存在 Java 堆中

虚拟机规范对这三点的要求并不具体,因此具体虚拟机实现的灵活度都很大。比如第一条,没有指明二进制字节流要从哪里来,单单就这一条,就能变出许多花样来,比如下面几种加载方式:

  • 从 zip、jar、ear、war 等归档文件中加载.class文件
  • 通过网络下载并加载.class文件,典型应用就是 Applet
  • Java源文件动态编译为.class文件,典型应用就是动态代理技术
  • 从数据库中提取.class文件并进行加载

总的来说,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)对于开发者来说是可控性最强的一个阶段。因为开发者既可以使用系统提供的类加载器来完成加载,也可以自定义类加载器来完成加载。

3.2、验证

验证是连接阶段的第一步,这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

Java 语言本身是比较安全的语言,但是正如上面说到的.class文件未必是从 Java 源码编译而来,可以使用任何途径来生成并加载。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

验证阶段大致会完成 4 项检验工作:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型等
  • 元数据验证:对字节码描述的元数据信息进行语义分析,要符合 Java 语言规范,例如:是否继承了不允许被继承的类(例如 final 修饰过的)、类中的字段、方法是否和父类产生矛盾等等
  • 字节码验证:对类的方法体进行校验分析,确保这些方法在运行时是合法的、符合逻辑的
  • 符号引用验证:确保解析动作能正确执行,例如:确保符号引用的全限定名能找到对应的类,符号引用中的类、字段、方法允许被当前类所访问等等

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

3.3、准备

准备是连接阶段的第二步,这个阶段的主要工作是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配

不过这个阶段,有几个知识点需要注意一下:

  • 1.这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在 Java 堆中
  • 2.这个阶段会设置变量的初始值,值为数据类型默认的零值(如 0、0L、null、false 等),不是在代码中被显式地赋予的值;但是当字段被final修饰时,这个初始值就是代码中显式地赋予的值
  • 3.在 JDK1.8 取消永久代后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在 Java 堆中的,跟 JDK1.7 及以前的版本稍有不同

关于第二个知识点,我们举个简单的例子进行讲解,比如public static int value = 123value在准备阶段过后是0而不是123

因为这时候尚未开始执行任何 Java 方法,把value赋值为123public static指令是在程序编译后存放于类构造器<clinit>()方法之中的,因此把value赋值为123的动作将在初始化阶段才会执行。

假如被final修饰,比如public static final int value = 123就不一样了,编译时Javac将会为value生成ConstantValue属性,在准备阶段,虚拟机就会给value赋值为123,因为这个变量无法被修改,会存入类的常量池中。

各个数据类型的零值如下图:

数据类型零值
byte0
short0
int0
long0L
float0.0f
double0.0d
booleanfalse
char\u0000
referencenull
3.4、解析

解析是连接阶段的第三步,这个阶段的主要工作是虚拟机会把这个.class文件中常量池内的符号引用转换为直接引用

主要解析的是类或接口、字段、方法等符号引用,我们可以把解析阶段中符号引用转换为直接引用的过程,理解为当前加载的这个类和它所引用的类,正式进行“连接“的过程。

我们先来了解一下符号引用直接引用有什么区别:

  • 符号引用:这个其实是属于编译原理方面的概念,Java 代码在编译期间,是不知道最终引用的类型,具体指向内存中哪个位置的,这时候会使用一个符号引用来表示具体引用的目标是"谁",符号引用和虚拟机的内存布局是没有关系的
  • 直接引用:指的是可以直接或间接指向目标内存位置的指针或句柄,直接引用和虚拟机实现的内存布局是有关系的

符号引用转换为直接引用,可以理解成将某个符号与虚拟机中的内存位置建立连接,通过指针或句柄来直接访问目标。

与此同时,同一个符号引用在不同的虚拟机实现上翻译出来的直接引用一般不会相同。

3.5、初始化

初始化是类加载的过程的最后一步,这个阶段的主要工作是执行类构造器 <clinit>()方法的过程

简单的说,初始化阶段做的事就是给static变量赋予用户指定的值,同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。

初始化阶段,虚拟机大致依此会进行如下几个步骤的操作:

  • 1.检查这个类是否被加载和连接,如果没有,则程序先加载并连接该类
  • 2.检查该类的直接父类有没有被初始化,如果没有,则先初始化其直接父类
  • 3.类中如果有多个初始化语句,比如多个static代码块,则依次执行这些初始化语句

有个地方需要注意的是:虚拟机会保证类的初始化在多线程环境中被正确地加锁、同步执行,所以无需担心是否会出现变量初始化时线程不安全的问题

如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都会阻塞等待,直到<clinit>()方法执行完毕。同时,同一个类加载器下,一个类只会初始化一次,如果检查到当前类没有初始化,执行初始化;反之,不会执行初始化。

与此同时,只有当对类的主动使用的时候才会触发类的初始化,触发时机主要有以下几种场景:

  • 1.创建类的实例对象,比如new一个对象操作
  • 2.访问某个类或接口的静态变量,或者对该静态变量赋值
  • 3.调用类的静态方法
  • 4.反射操作,比如Class.forName("xxx")
  • 5.初始化某个类的子类,则其父类也会被初始化,并且父类具有优先被初始化的优势
  • 6.Java 虚拟机启动时被标明为启动类的类,比如SpringBootApplication入口类

最后,<clinit>()方法和<init>()方法是不同的,一个是类构造器初始化,一个是实例构造器初始化,千万别搞混淆了啊。

四、小结

当一个符合 Java 虚拟机规范的.class字节码文件,经历加载、验证、准备、解析、初始化这些 5 个阶段相互协作执行完成之后,虚拟机会将此文件的二进制数据导入运行时数据区的方法区内,然后在堆内存中,创建一个java.lang.Class类的对象,这个对象描述了这个类所有的信息,同时提供了这个类在方法区的访问入口。

可以用如下图来简要描述。

与此同时,在方法区中,使用同一加载器的情况下,每个类只会有一份Class字节流信息;在堆内存中,使用同一加载器的情况下,每个类也只会有一份java.lang.Class类的对象。

写到最后

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

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

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

相关文章

ARCH和GARCH模型★★

该博客为个人学习清风建模的学习笔记&#xff0c;部分课程可以在B站&#xff1a;【强烈推荐】清风&#xff1a;数学建模算法、编程和写作培训的视频课程以及Matlab等软件教学_哔哩哔哩_bilibili 该节是针对时间序列分析中对证券指数分析不能使用传统时间序列模型做出的模型&a…

C++ 基础(类和对象下)

目录 一. 再探构造函数 1.1. 初始化列表&#xff08;尽量使用列表初始化&#xff09; 二. static成员 2.1static成员初始化 三.友元 3.1友元&#xff1a;提供了⼀种 突破类访问限定符封装的方式. 四.内部类 4.1如果⼀个类定义在另⼀个类的内部&#xff0c;这个内部类就叫…

数据结构第三讲:单链表的实现

数据结构第三讲&#xff1a;单链表的实现 1.什么是单链表2. 节点3.单链表的实现3.1节点的结构3.2打印单链表3.3申请一个新节点3.4单链表尾部插入3.5单链表头部插入3.6单链表的尾部删除3.7单链表头部删除3.8查找3.9在指定位置之前插入数据3.10在指定位置之后插入数据3.11删除pos…

全国区块链职业技能大赛样题第9套前端源码

后端源码地址:https://blog.csdn.net/Qhx20040819/article/details/140746050 前端源码地址:https://blog.csdn.net/Qhx20040819/article/details/140746216 智能合约+数据库表设计:https://blog.csdn.net/Qhx20040819/article/details/140746646 登录 ​ 用户管理

Java httpclient请求form-data格式,并设置boundary代码实现

文章目录 form-data 数据请求格式样例报错信息: **MissingServletRequestParameterException**解决方法报错信息: **no multipart boundary was found** 解决方法Java代码实现【错误】使用 UrlEncodedFormEntity 、BasicNameValuePair 请求失败&#xff08;error&#xff09;【…

【计算机毕业设计】850汽车售后服务信息管理系统

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

《Windows API每日一练》20.1 动态链接库

本节将介绍如何编写动态链接库&#xff0c;并更全面地探讨动态链接库的使用方法&#xff0c;包括以不同的方法装入动态链接库和以不同的方法调用其中的函数等。 本节必须掌握的知识点&#xff1a; 动态链接库的概念 第157练&#xff1a;编写动态链接库DLL 第158练&#xff1a;…

Java从入门到精通(十二)~ 动态代理

晚上好&#xff0c;愿这深深的夜色给你带来安宁&#xff0c;让温馨的夜晚抚平你一天的疲惫&#xff0c;美好的梦想在这个寂静的夜晚悄悄成长。 文章目录 目录 前言 主要作用和功能&#xff1a; 应用场景&#xff1a; 二、代理概念 1.静态代理 2.动态代理 2.1 概念介绍 …

MSPM0G3507将系统时钟改为80M

这款板卡手册上写着最高80M的主频&#xff0c;不过要达到80M需要自己配置时钟树分频倍频。 第一步&#xff1a;打开SysConfig工具&#xff0c;那个方框原本是没有勾选的&#xff0c;这里我们把它勾选 第二步&#xff1a; 点击时钟树配置 第三步&#xff1a; 主要是改变这几个…

nodejs - MongoDB 学习笔记

一、简介 1、MongoDB 是什么 MongoDB 是一个基于分布式文件存储的数据库&#xff0c;官方地址 https://www.mongodb.com/ 2、数据看是什么 数据库&#xff08;DataBase&#xff09;是按照数据结构来组织、存储和管理数据的应用程序。 3、数据库的作用 主要作用是 管理数据…

docker命令大全(新手必备)-my write

一、命令图谱 二、基本命令 docker version #显示版本信息 docker info #显示系统信息,包括镜像、容器数量 docker 命令 --help #帮助 三、镜像命令 3.1 docker images 查看本地主机上的镜像 docker images [OPTIONS] [REPOSITORY[:TAG]] 选项:-a …

【JavaEE初阶】Thread类及常见方法

目录 &#x1f4d5; Thread类的概念 &#x1f4d5; Thread 的常见构造方法 &#x1f4d5; Thread 的几个常见属性 &#x1f4d5; start()-启动一个线程 &#x1f4d5; 中断一个线程 &#x1f6a9; 实例一 &#x1f6a9; 实例二 &#x1f6a9; 实例三 &#x1f4d5; jo…

Pytorch深度学习实践(8)多分类任务

多分类问题 多分类问题主要是利用了Softmax分类器&#xff0c;数据集采用MNIST手写数据集 设计方法&#xff1a; 把每一个类别看成一个二分类的问题&#xff0c;分别输出10个概率 但是这种方法存在一种问题&#xff1a;不存在抑制问题&#xff0c;即按照常规来讲&#xff0c…

Python的文件操作介绍

一、编码格式介绍 1.1、常见的字符编码格式 1.2、Python字符编码格式 Python的解释器使用的是Unicode&#xff08;内存&#xff09; .py文件在磁盘上使用UTF-8存储&#xff08;外存&#xff09; #encodinggbkprint(你好&#xff0c;中国) 二、文件的读写原理 2.1、文件的…

Servlet1-Servlet程序、请求处理、继承体系

目录 什么是Servlet 手动实现Servlet程序 ​编辑url地址如何定位到Servlet程序去访问 Servlet的生命周期 ​编辑GET和POST请求的分发处理 通过继承HttpServlet类实现Servlet程序 IDEA菜单生成Servlet程序 Servlet类的继承体系 ServletConfig类 ServletContext类 什么…

Docker学习与实战

一、Docker安装 移除旧版本docker sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine配置docker yum源 sudo yum install -y yum-utils配置阿里云docker仓库 sudo y…

甄选范文“论软件测试中缺陷管理及其应用”软考高级论文,系统架构设计师论文

论文真题 软件缺陷指的是计算机软件或程序中存在的某种破坏正常运行能力的问题、错误,或者隐藏的功能缺陷。缺陷的存在会导致软件产品在某种程度上不能满足用户的需要。在目前的软件开发过程中,缺陷是不可避免的。软件测试是发现缺陷的主要手段,其核心目标就是尽可能多地找…

Nvidia GPU驱动安装报错显卡与驱动不兼容(本身兼容)

最近在公司服务器上遇到了一个特别离谱的问题&#xff0c;就是在本身在nividia官网上面下载的匹配的显卡驱动&#xff0c;安装之后采用下面命令查看驱动显示&#xff1a; $ nvidia-smiNVIDIA-SMI has failed because it couldnt communicate with the NVIDIA driver. Make su…

exe4j 使用jar包 打包exe程序,并且自带jre

1. 准备工作 1) 程序jar包一个 2) jdk自带jre文件夹一个 3&#xff09;exe4j 程序 &#xff0c;自行搜索 我用的5.1 4&#xff09;图标&#xff08;icon&#xff09;文件一个&#xff0c;用来作为exe程序的图标 5&#xff09; 图片&#xff08;png/jpg等&#xff09;用来打开…

LeetCode 118.杨辉三角 C++写法

LeetCode 118.杨辉三角 C写法 思路&#x1f9d0;&#xff1a; 我们使用vector来进行解答&#xff0c;该题规律简单&#xff0c;观察一下就可以发现&#xff0c;下一行的非1部分是上一行相同下标与上一行相同下标的前一个数据的和。难一点的是数据存储方式&#xff0c;C语言需要…