一文看懂Java的类加载机制


前言

当我们运行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.rhkb.cn/news/129526.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络安全—0基础入门学习手册

前言 一、什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防…

用python实现基本数据结构【01/4】

说明 如果需要用到这些知识却没有掌握&#xff0c;则会让人感到沮丧&#xff0c;也可能导致面试被拒。无论是花几天时间“突击”&#xff0c;还是利用零碎的时间持续学习&#xff0c;在数据结构上下点功夫都是值得的。那么Python 中有哪些数据结构呢&#xff1f;列表、字典、集…

Kubernetes (K8s) 解读:微服务与容器编排的未来

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f405;&#x1f43e;猫头虎建议程序员必备技术栈一览表&#x1f4d6;&#xff1a; &#x1f6e0;️ 全栈技术 Full Stack: &#x1f4da…

设计模式篇(Java):装饰者模式

&#x1f468;‍&#x1f4bb;本文专栏&#xff1a;设计模式篇-装饰者模式 &#x1f468;‍&#x1f4bb;本文简述&#xff1a;装饰者模式的详解以及jdk中的应用 &#x1f468;‍&#x1f4bb;上一篇文章&#xff1a; 设计模式篇(Java)&#xff1a;桥接模式 &#x1f468;‍&am…

串行数据发送器

框图 THR&#xff1a;发送保持寄存器 定义了两种状态&#xff1a;空&#xff0c;满数据写入端口地址&#xff1a;00H状态读出端口地址&#xff1a;00H当THR不满时&#xff0c;可以向THR写入数据 TSR&#xff1a;发送移位寄存器 一旦TSR空而THR中有数据时&#xff0c;THR中的数…

shell中分支语句,循环语句,函数

实现对一个数组求和的函数&#xff0c;将数组作为实参传给函数 #!/bin/bash sum() {for i in $do((sumi))doneecho $sum} read -p "请输入一组数字: " -a arr sum ${arr[*]}2 调用函数&#xff0c;输出当前用户的uid gid 并使用变量接收结果 #!/bin/bashget() {uid…

2023 年高教社杯全国大学生数学建模竞赛题目 B 题 多波束测线问题

B 题 多波束测线问题 单波束测深是利用声波在水中的传播特性来测量水体深度的技术。声波在均匀介质中作匀速直线传播&#xff0c;在不同界面上产生反射&#xff0c;利用这一原理&#xff0c;从测量船换能器垂直向海底发射声波信号&#xff0c;并记录从声波发射到信号接收的传播…

VSCode自动分析代码的插件

今天来给大伙介绍一款非常好用的插件&#xff0c;它能够自动分析代码&#xff0c;并帮你完成代码的编写 效果如下图 首先我们用的是VSCode&#xff0c;&#xff08;免费随便下&#xff09; 找到扩展&#xff0c;搜索CodeGeeX&#xff0c;将它下载好&#xff0c;就可以实现了 到…

由Qt::BlockingQueuedConnection引起的关闭Qt主页面而后台仍有进程残留

BUG&#xff1a;由Qt::BlockingQueuedConnection引起的关闭Qt主页面而后台仍有进程残留 1、错误代码示例 首先我们看下下面的代码&#xff0c;可以思考一下代码的错误之处 /** BlockingQueueDeadLock.h **/ #pragma once#include <QtWidgets/QMainWindow> #include &q…

深度学习Tensorflow: CUDA_ERROR_OUT_OF_MEMORY解决办法

目前在用深度学习训练&#xff0c;训练中设置batch size后可以正常跑通&#xff0c;但是在训练一轮save_model时&#xff0c;总出现这个错误&#xff0c;即使我调batch size到1也依旧会报错。 发现是在 调用logger时出现问题。 查询后了解到是因为TensorFlow中的eager_executi…

模电课程设计

主要内容跟本科实验关系很大&#xff0c;可以用来借鉴。 包含文件有&#xff1a;实验报告、Multisim仿真文件&#xff0c;资料很全&#xff0c;有问题可以私信 目录 1、模电课设&#xff1a;用Multisim简单了解二极管 2、模电课设&#xff1a;用Multisim简析三极管与场效应…

开发者的商业智慧:产品立项策划你知道多少?

文章目录 想法的萌芽&#x1f31f;初步评估产品可行性&#x1f34a;分析核心功能和特点以及竞争对手&#x1f4dd;大健康监测&#x1f4dd;时尚新科技产品&#x1f4dd;准确性&#x1f4dd;多功能&#x1f4dd;品牌口碑&#x1f4dd;数据分析与个性化建议&#x1f4dd;社交互动…

go的iris框架进行本地资源映射到服务端

我这里使用的是HandleDirapi,有其他的请补充 package mainimport ("github.com/kataras/iris/v12" )type Hello struct{Status int json:"status"Message string json:"message" }func main(){app : iris.New()//第一个api:相当于首页app.Get(&q…

【多线程】volatile 关键字

volatile 关键字 1. 保证内存可见性2. 禁止指令重排序3. 不保证原子性 1. 保证内存可见性 内存可见性问题: 一个线程针对一个变量进行读取操作&#xff0c;另一个线程针对这个变量进行修改操作&#xff0c; 此时读到的值&#xff0c;不一定是修改后的值&#xff0c;即这个读线…

C++学习之list的实现

在了解学习list实现之前我们首先了解一下关于迭代器的分类&#xff1a; 按功能分类&#xff1a; 正向迭代器 反向迭代器 const正向迭代器 const反向迭代器 按性质分类&#xff1a; 单向迭代器 只能 例如单链表 双向迭代器 可&#xff0c;也可-- 例如双…

服务端 TCP 连接的 TIME_WAIT 过多问题的分析与解决

https://blog.csdn.net/zxlyx/article/details/120397006 本文给出一个 TIME_WAIT 状态的 TCP 连接过多的问题的解决思路&#xff0c;非常典型&#xff0c;大家可以好好看看&#xff0c;以后遇到这个问题就不会束手无策了。 问题描述 模拟高并发的场景&#xff0c;会出现批量…

《Python深度学习-Keras》精华笔记3:解决深度学习多分类问题

公众号&#xff1a;机器学习杂货店作者&#xff1a;Peter编辑&#xff1a;Peter 持续更新《Python深度学习》一书的精华内容&#xff0c;仅作为学习笔记分享。 本文是第三篇&#xff1a;介绍如何使用Keras解决Python深度学习中的多分类问题。 多分类问题和二分类问题的区别注意…

Linux dup dup2函数

/*#include <unistd.h>int dup2(int oldfd, int newfd);作用&#xff1a;重定向文件描述符oldfd 指向 a.txt, newfd 指向b.txt,调用函数之后&#xff0c;newfd和b.txt close&#xff0c;newfd指向a.txtoldfd必须是一个有效的文件描述符 */ #include <unistd.h> #i…

Vue 2 条件渲染

条件渲染相关的指令有哪些&#xff1f; v-if、v-else、v-else-ifv-show v-if 的作用 <div v-if"expression"></div>v-if 根据表达式 expression 返回的值是否为 truthy 来决定其内容是否被渲染。 Vue还实现了 v-else 和 v-else-if&#xff1a; <d…

排序算法:快速排序(三种排序方式、递归和非递归)

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关排序算法的相关知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通…