一文看懂Java的类加载机制

news2024/11/20 11:24:39

前言

当我们运行Java程序时,Java虚拟机(JVM)需要加载各种类文件,以执行程序中的代码。Java的类加载机制是Java语言的一个关键特性,它负责在运行时将类加载到内存中,并确保类的正确性。
类是在运行期间第一次使用时,被类加载器动态加载至JVM。JVM不会一次性加载所有类。因为如果一次性加载,那么会占用很多的内存。


一、类的生命周期

类的生命周期包括以下 7 个阶段:
● 加载(Loading)
● 验证(Verification)
● 准备(Preparation)
● 解析(Resolution)
● 初始化(Initialization)
● 使用(Using)
● 卸载(Unloading)
其中,前五个阶段是最重要的,它们也是类加载过程,可以通过一句谐音来记忆“家宴准备了西式菜” = 家 (加载) 宴 (验证) 准备 (准备) 了西 (解析) 式 (初始化) 菜

Step1:加载

加载是类加载的第一个阶段,加载过程完成以下3件事:

1. 通过类的完全限定名称获取定义该类的二进制字节流。
2. 将该字节流表示的静态存储结构转换为Metaspace元空间区的运行时存储结构。
3. 在内存中生成一个代表该类的 Class 对象,作为元空间区中该类各种数据的访问入口。

Step2:验证

  • 确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

Step3:准备

  • 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是元空间区的内存。
  • 实例变量不会在这阶段分配内存,它会在对象实例化时,随着对象一起被分配在堆中。应该注意到,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
  • 初始值一般为 0 值。例如:下面的类变量 value 被初始化为 0 而不是 123。
public static int value = 123;
  • 如果类变量是常量,那么它将初始化为表达式所定义的值而不是 0;例如:下面的常量 value 被初始化为 123 而不是 0。
public static final int value = 123;

Step4:解析

  • 将常量池的符号引用替换为直接引用的过程。其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

Step5:初始化

  • 初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器
    clinit()方法的过程
    。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
  • clinit()是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。所以,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。

接口的类加载

接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 clinit() 方法。但接口与类不同的是,执行接口的 clinit() 方法不需要先执行父接口的 clinit() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 clinit() 方法。
虚拟机会保证一个类的 clinit() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 clinit() 方法,其它线程都会阻塞等待,直到活动线程执行 clinit() 方法完毕。如果在一个类的 clinit() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中,该阻塞非常隐蔽,几乎不会被察觉。


二、类加载的时机

主动引用

虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了只有下列几种情况必须对类进行加载:

  1. 当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条字节码指令时:
public class Demo01 {
	public static void main(String[] args){
		//执行以下字节代码,类会被加载
		//1.new指令
		Parent parent = new Parent();
		//getStatic指令
		System.out.println(Parent.i);
		//putStatic指令
    	Parent.i=4;
		//invokeStatic 指令
		Parent.dosth();
		
		//如果是访问常量,会到常量池中获取,不会触发类加载
		System.out.println(Parent.k);
	}
}
class Parent{
	static int i = 3;
	static final int k = 3;
	static {
		System.out.println("Parent类被加载");
	}
	public static void dosth() {	
	}
}
  1. 使用 java.lang.reflect包的方法对类进行反射调用时如 Class.forname(“…”), 或newInstance() 等等。如果类没初始化,需要触发类的加载。
	//1.Class.forname("...")
	try {
		Class.forName("com.apesource.demo3.Parent");
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	}
	//仅获取Class对象,不会被加载
	Class cls = Parent.class;//
	//使用newInstance() ,会被加载
	cls.newInstance();
  1. 子类被加载时,父类也会被加载
public class Demo02 {
	public static void main(String[] args){
		Son son  = new Son();
	}
}
class Father{
	static{
		System.out.println("father类被加载");	
	}
}
class Son extends Father{
	static {
		System.out.println("son类被加载");
	}	
}
  1. 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了加载,则该接口要在实现类之前被加载。
interface A{
	default void dosth() {
		System.out.println("实现了该接口的实现类被加载之前会先加载该接口");
	}
}
  1. main方法执行时,main方法所在的类也会被加载
public class Demo02 {
	static{
		System.out.println("Demo02被加载");
	}
	public static void main(String[] args){
	}
}

被动引用

除主动引用之外,所有引用类的方式都不会触发加载,称为被动引用。

  1. 通过子类引用父类的静态字段,不会导致子类加载。

  2. 通过数组定义来引用类,不会触发此类的加载。该过程会对数组类进行加载,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。

  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的加载。

//被动引用,不会被加载
public class Demo02 {
	public static void main(String[] args) throws InstantiationException, IllegalAccessException {
		//1.通过子类调用父类的静态变量,只会触发父类的加载,不会触发子类的类加载
		System.out.println(Son.value);	
		//2.通过类定义的数组,不会触发类加载
		Father[] array = new Father[16];
		System.out.println(array);
		//3.访问类的静态常量,不会触发类加载
		System.out.println(Father.i);
	}
}
class Father{
	static final int i = 3;
	static int value = 3;
	static{
		System.out.println("father类被加载");
	}
}
class Son extends Father{
	static {
		System.out.println("son类被加载");
	}	
}

三、类与类加载器

在Java中,类加载机制与类加载器(ClassLoader)密切相关。类加载器负责加载类文件并构建类的表示。

类加载器分类

  1. 启动类加载器(Bootstrap ClassLoader)
    该类加载器负责将存放在 <JRE_HOME>\lib 目录中的革新类库加载到虚拟机内存中,启动类加载器无法被 Java 程序直接引用

  2. 扩展类加载器(Extension ClassLoader)
    该类加载器负责将存放在 <JRE_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,扩展类库都是由扩展类加载器加载,开发者可以直接使用扩展类加载器

  3. 应用程序类加载器(Application ClassLoader)
    该类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现,负责加载自定义类或第三方jar包。开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。


四、双亲委派模型

应用程序是由三种类加载器互相配合,从而实现类加载,除此之外还可以加入自己定义的类加载器。
类加载器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)。
在这里插入图片描述

双亲委派工作机制

一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。

双亲委派的作用

  • 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一,避免冲突;

例如:java.lang.Object 存放在 rt.jar 中,如果我们在类路径ClassPath下也编写一个java.lang.Object,程序可以编译通过,但是由于双亲委派模型的存在,在 rt.jar 中被启动类加载器加载的 Object 比在 ClassPath 中被应用程序类加载器加载的 Object 优先级更高,那么程序中使用的所有的 Object 都是由启动类加载器所加载的 Object

  • 实现热加载,比如Spring Boot DevTools;

五、对象的创建过程

以上是Java对一个类加载的过程,到现在位置,我们只是将Class类加载的流程熟悉了一边,但是仅仅将类加载到初始化的完成,也只是类声明周期的前五步,在类使用的过程出,除了静态资源访问、反射操作之外,还有一个关键的使用,就是对象的实例化,也就是对象的创建过程:

Step1:类加载检查

虚拟机遇到一条 new 指令时,首先检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

Step2:分配内存

在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。内存分配的查找方式有 “指针碰撞” 和 “空闲列表” 两种。
指针碰撞:

  • 使用场景:适用于堆内存规整(没有内存碎片的情况);
  • 原理:用过的内存全部整合到一边,没有过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方法将该指针移动对象内存大小的位置即可;
  • GC收集器:Serial、ParNew

空闲列表:

  • 使用场景:堆内存不规整的情况;
  • 原理:虚拟机会维护一个列表,用来记录哪些内存是可用的,在分配的时候,找一个足够这个对象使用的内存分配给对象实例,最后更新列表记录;
  • GC收集器:CMS

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"。

Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置请求头

初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例如何才能找到类的元数据信息对象的哈希码对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5:执行init构造方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java 程序的视角来看,对象创建才刚开始,init 构造方法还没有执行,目前所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 init构造方法,把对象按照程序逻辑的意愿进行初始化,这样一个真正可用的对象才算完整创建出来。


总结

Java的类加载机制是Java虚拟机的一个重要组成部分,它负责将类加载到内存中,并确保类的正确性。了解这个机制有助于开发者更好地理解Java程序的运行方式,并能够更好地应对类加载相关的问题。

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

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

相关文章

算法竞赛入门【码蹄集新手村600题】(MT1260-1280)C语言

算法竞赛入门【码蹄集新手村600题】(MT1260-1280&#xff09;C语言 目录MT1260 袋鼠躲猫猫MT1261 留下来的才是幸运数MT1262 约数MT1263 最大的三位约数MT1264 完数MT1265 区间完数MT1266 完数与因子MT1267 亏数MT1268 因数的因数MT1269 区间素数MT1270 素数计算MT1271 三生质数…

开开心心带你学习MySQL数据库之节尾篇

Java的JDBC编程 各种数据库,MySQL, Oracle, SQL Server在开发的时候,就会提供一组编程接口(API) API ~~ Application Programming Interface ~~ 应用程序编程接口 计算机领域里面的一个非常常见的概念, 给你个软件,你能对他干啥(从代码层次上的) 基于它提供的这些功能,就可以写…

Python 图形化界面基础篇:创建你的第一个 Tkinter 窗口

Python 图形化界面基础篇&#xff1a;创建你的第一个 Tkinter 窗口 引言准备工作步骤1&#xff1a;导入 Tkinter 模块步骤2&#xff1a;创建主窗口步骤3&#xff1a;设置窗口标题步骤4&#xff1a;启动主事件循环 完整的示例代码代码解释结论 引言 欢迎来到 Python 图形化界面…

台积电、博通、英特尔等巨头积极进军硅光子技术领域 | 百能云芯

据传&#xff0c;台积电与博通、英伟达等大客户密切合作&#xff0c;共同致力于新一代超高速运算芯片的开发&#xff0c;预计明年下半年将开始迎来大规模订单。为此&#xff0c;台积电已投入逾200名研发人员&#xff0c;成立专门的先遣研发团队&#xff0c;以抓住基于硅光子制程…

windows环境下node安装教程(超详细)

安装node.js 1、下载node: 下载地址&#xff1a;下载 | Node.js 中文网 node.js的zip包安装时是直接解压缩后就可以了, node.js的msi包是傻瓜式一路next就可以了 选择一中方式就可以 2、解压后的目录,或者mis安装后的目录如下: 3、安装完后&#xff0c;可以在命令行中输入…

电池的健康状态 SOH 估计

电池的健康状态 SOH 估计 SOH&#xff08;State of Health&#xff09;估计通常用于描述电池的健康状态&#xff0c;即电池当前容量与初始容量的比值。 一种常见的SOH估计方法是基于经验的电池寿命预测方法&#xff0c;包括循环周期数法、安时法与加权安时法、面向事件的老化…

华为云云服务器云耀L实例评测 | 从零到一:华为云云耀云服务器L实例上手体验

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

学习记忆——记忆宫殿——编码——数字编码——数字声母

https://www.bilibili.com/video/BV1gb411x7ic?p7&vd_source5021bbafad5e5afcf1984c99432f8353 0—D&#xff1a;0象形D。 1—y&#xff1a;1的发音首字母是y。 2—Z&#xff1a;象形。 3—S&#xff1a;3的手音首字母是S。 4—h&#xff1a;倒象形。 5—w&#xff1a;5的…

ES6中的Promise对象

1. Promise是什么 Promise简单来说就是一个容器&#xff0c;里面保存着未来才会结束的事件的结果&#xff08;这个事件就是异步操作&#xff09;。Promise是一个对象&#xff08;构造函数&#xff09;&#xff0c;可以获取异步操作的结果。 特点&#xff1a; 对象的状态不受外…

k8s优雅停服

在应用程序的整个生命周期中&#xff0c;正在运行的 pod 会由于多种原因而终止。在某些情况下&#xff0c;Kubernetes 会因用户输入&#xff08;例如更新或删除 Deployment 时&#xff09;而终止 pod。在其他情况下&#xff0c;Kubernetes 需要释放给定节点上的资源时会终止 po…

轻松省下大笔费用!5个你不得不知道的云渲染省钱攻略

&#xfeff; 在今天的数字化时代&#xff0c;云渲染正以其强大的计算能力和高效的渲染速度成为许多设计师和创意工作者的首选。然而&#xff0c;使用云渲染服务也可能意味着额外的费用开销。幸运的是&#xff0c;本文将为您揭示5个轻松省下大笔费用的云渲染省钱攻略&#xff…

巨人互动|Facebook海外户Facebook客户反馈分数

Facebook客户反馈分数是一项用于衡量用户对Facebook产品和服务满意度的指标。该指标被广泛应用于各种调研和评估活动&#xff0c;帮助Facebook了解用户对其平台和功能的意见和建议&#xff0c;并从中识别出改进的机会。 巨人互动|Facebook海外户&Facebook新闻提要的算法&am…

门阀-bitlocker

一、bitlocker&#xff0c;可给C盘&#xff0c;D盘其他盘&#xff0c;&U盘加密&#xff1b; 1.1此处只涉及D盘加密 网址&#xff1a;如何开启BitLocker加密 保存恢复码 数据解密 基础篇【夻白咏技 057期】 - YouTube 步骤须知&#xff1a; D盘操作步骤&#xff1a; 1&am…

2023年锂行业研究报告

第一章 行业概况 1.1 定义 锂行业&#xff0c;作为有色金属行业中稀有金属子行业的重要组成部分&#xff0c;近年来受到了广泛的关注和研究。锂矿经过冶炼加工&#xff0c;可以得到多种锂盐产品。这些锂盐产品在传统工业中有着广泛的应用&#xff0c;尤其是在玻璃和陶瓷制造、…

勒索病毒最新变种.halo勒索病毒来袭,如何恢复受感染的数据?

摘要&#xff1a; .halo勒索病毒已成为数字世界中的威胁&#xff0c;通过高级加密技术将文件锁定&#xff0c;并要求支付赎金。本文91数据恢复将深入介绍.halo勒索病毒的工作原理&#xff0c;提供解锁被感染文件的方法&#xff0c;以及探讨如何有效预防这一威胁。如果您正在经…

串行通信协议

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、UART二、SPI二、IIC 前言 UART为异步串行通信&#xff0c;使用各自的时钟控制数据的发送和接受过程&#xff0c;不使用同步时钟&#xff0c;而是使用一些特…

苏宁API接口解析,实现按关键字搜索suning商品

苏宁API接口提供了多种搜索商品的方式&#xff0c;其中包括按关键字搜索。下面是一个简单的示例&#xff0c;演示如何使用苏宁API接口实现按关键字搜索商品&#xff1a; 点击获取key和secret 苏宁易购按关键字搜索suning商品 API 返回值说明 请求参数 请求参数&#xff1a;q…

UG\NX二次开发 计算一个向量的反向向量UF_VEC3_negate

文章作者:里海 来源网站:王牌飞行员_里海_里海NX二次开发3000例,里海BlockUI专栏,C\C++-CSDN博客 简介: UG\NX二次开发 计算一个向量的反向向量UF_VEC3_negate 效果: 代码: #include "me.hpp"void ufusr(char* param, int* retcode, int paramLen) {UF…

腾讯云4核8G服务器CVM S5性能测评及优惠价格表

腾讯云4核8G服务器CVM标准型S5实例性能测评&#xff0c;包括CPU型号、内存、系统盘、CVM实例规格性能测评&#xff0c;腾讯云4核8G租用优惠价格表&#xff0c;腾讯云服务器网分享腾讯云4核8G服务器CVM S5性能测评和租用费用&#xff1a; 目录 腾讯云4核8G服务器CVM S5性能测评…

单片机之硬件记录

一、概念 VBAT 当使用电池或其他电源连接到VBAT脚上时&#xff0c;当VDD断电时&#xff0c;可以保存备份寄存器的内容和维持RTC的功能。如果应用中没有使用外部电池&#xff0c;VBAT引脚应接到VDD引脚上。 VCC&#xff1a;Ccircuit 表示电路的意思,即接入电路的电压&#x…