Java虚拟机类加载机制探究:生命周期、初始化、使用与验证

一、java虚拟机与程序的生命周期

在如下几种情况之下,java虚拟机将结束生命周期:

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或者错误而异常终止
  • 由于操作系统用出现错误而导致java虚拟机进程终止

二、类的加载,链接,初始化

2.1 加载:查找并加载类的二进制数据

类加载器并不需要某个类被首次主动使用时再加载他。JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)。如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。类被加载后,就进入连接阶段。

2.2 连接:

将已经读入到内存的类的二进制数据合并到虚拟机的运行时环境中去。然后要经过一系列的验证。

2.2.1 验证:确保被加载的类的正确性(验证字节码)
  • 类文件的结构检查:确保类文件遵从java类文件的固定格式。
  • 语义检查:确保类本身符合java语言的语法规定,比如验证final类型的类没有子类,以及final类型的方法没有被覆盖。(虽然编译时就可以发现错误,但不经过编译,手动生成class文件,那么就会发现不了final类型的方法被覆盖,但是语义检查就可以发现)
  • 字节码验证:确保字节码流可以被java虚拟机安全的执行。字节码流代表java方法(报空静态方法和实例方法),它是由被称作操作码的单字节指令组成的序列,每一个操作码后跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
  • 二进制兼容性的验证:确保相互引用的类之间的协调一致,例如在Wroker类的gotoWork()方法中会调用Car类的run()方法。java虚拟机在验证work()类时,会检查在方法区内是否存在Car类的run()方法,假如不存在(当worker类和Car类的版本不兼容,就会出现这种问题),就会抛出NoSuchMethodError方法。
public class Wroker{
	public void gotoWork(){
	   Car car = new Car();	
	   car.run();//这段代码在worker类的二进制数据中表示为符号引用
	}
}
2.2 准备:为类的静态变量分配内存,并将其初始化为默认

在准备阶段,Java虚拟机为类的静态变量分配内存,并设置默认的初始值。例如对于一下Sample类,在准备阶端,将为int类型的静态变量a分配4个字节的内存空间,并且赋予默认值0,为long类型的静态变量b分配8个字节的内存空间,并且赋予默认值0。

public class Sample{
	private static int a=1;
	public static long b;
	static{
		b=2;
	}
}
2.3 解析:把类中的符号引用转换为直接引用

在解析阶段,java虚拟机会把类的二进制数据中的符号引用替换为直接引用。例如在Worker类的gotoWork()方法中会引用Car类的run()方法。

在Worker类中的二进制数据中,包含了一个对Car类的run()方法的符号引用,它由run()方法的全名和相关描述符组成。在解析阶段,Java虚拟机会把这个符号替换为一个指针,该指针指向Car类的run()方法在方法区内的内存位置。这个指针就是直接引用。

public class Wroker{
	public void gotoWork(){
	   Car car = new Car();	
		 car.run();//这段代码在worker类的二进制数据中表示为符号引用
	}
}
2.3 初始化:为类的静态成员变量赋予正确的初始值

在初始化阶段,java虚拟机执行类的初始化语句,为类的静态变量赋予初始值。在程序中,静态变量初始化有两种途径:

  • 在静态变量的声明处进行初始化
  • 在静态代码快中进行初始化。例如在以下代码中,静态变量a和b都被显示初始化,而静态变量c没有被显示初始化,它将保持默认值为0;但是如果要使用c,则必须进行初始化。
public class Sample{
	private static int a=1; //在静态变量声明出进行初始化
	public static long b;
	public static long c;  //但是如果要使用c,则必须进行初始化
	static{
		b=2; //在静态代码块中进行初始化
	}
}

示例:

public class ClassLoaderTest {
	public static void main(String[] args) {
		Singleton singleton=Singleton.getInstance();
		System.out.println("counter1= "+singleton.counter1);
		System.out.println("counter2= "+singleton.counter2);
	}
}

/**

*程序是从上向下顺序执行
* new Singleton()时,counter1,counter2初始值均为0
* 在通过构造方法Singleton(),均加1.则返回的值counter1,counter2均为1
* 然后再程序在继续向下执行,由于counter1没有显示初始化,则值还是为1
* 但是counter2经过显示初始化后,其值为0
* @author coderacademy
*/
class Singleton{

	private static Singleton singleton=new Singleton();//new语句在这是结果为counter1= 1;counter2= 0
	public static int counter1;
	public static int counter2=0;
	//private static Singleton singleton=new Singleton();//new语句在这是结果为counter1= 1;counter2= 1
	private Singleton(){
		counter1++;
		counter2++;
	}
	public static Singleton getInstance(){
		return singleton;
	}
}
  • 静态变量的声明语句,以及静态代码块都被看做类的初始化语句,java虚拟机会按照初始化语句在类文件中的先后顺序来一次执行他们。
  • 类的初始化步骤
  • 假如这个类还没有被加载和连接,那就先进行加载和连接
  • 假如类存在直接的父类,并且这个父类还没有被初始化,那就先初始化直接的父类。
  • 假如父类中存在初始化语句,那就依次执行这些初始化语句。

public class FinalTest {
	public static void main(String[] args) {
	System.err.println(Test.X);
	}
}

/**
* 当X=6/3时,编译时即可算出X=2,即编译时常量,即不需要运行类,所以不打印静态代码块中的内容
*当X=new Random().nextInt(100)时,编译时不能算出X的值,只有运行程序才知道,所以打印结果为:FinalTest static final 2
* @author coderacademy
*/
class Test{
	public static final int X=6/3;//打印结果: 2
	//public static final int X=new Random().nextInt(100);//打印结果为FinalTest static final 2
	static{
		System.err.println("FinalTest static final");
	}
}
  • 类的初始化时机:当java虚拟机初始化一个类时,要求他的所有父类都已经被初始化,但是这条规则并不适用于接口。
  • 在初始化一个类时,并不先初始化它所实现的接口
  • 在初始化一个接口时,并不会先初始化他的父接口
    因此,一个父接口并不会因为他的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态变量时,才会导致该接口的初始化。
public class Test4 {
	static {
		System.err.println("Test4 static block");
	}

	public static void main(String[] args) {
		System.err.println(Child.b);
	}
}

/**
* Test4 static block
* Parent static block
* Child static block
* 4
* @author coderacademy
*/
class Parent{
	public static final int a=3;
	static{
		System.err.println("Parent static block");
	}
}

	class Child extends Parent{
	public static int b=4;
	static{
		System.err.println("Child static block");
	}
}

如以下示例赋值的执行流程:

public class test(){
	private static int a=3;
}

//首先在准备阶段java虚拟在内存中为a分配内存,int的初始值是0,所以此时a的值是0;在初始化阶段,给赋值为3
//相当于:

public class test(){
	private static int a;
	//从上到下执行
	static{
		a=3;
	}
}

image.png

三、java程序对类的使用方式可分为两种:

3.1 主动使用
  • 创建类的实例。比如:new Test()
  • 访问某个类或者接口的静态变量,或者对该静态变量赋值。比如:int b=Test.a
  • 调用类的静态方法。例如:Test.doSomething();
  • 反射(如class.forName("com.jvm.classloader.test"))
  • 初始化一个类的子类(对父类的主动使用)。例如
class Parent {
}

class Child extends Parent{
	public static int a=4;
}
Child.a=8;
  • java虚拟机启动时被表明为启动类的类

程序中对子类的“主动使用”会导致父类被初始化,但对父类的“主动使用”并不会导致子类初始化,不可能说生成一个Object类的对象就导致系统中所有的子类都会被初始化。

public class Test5 {
	static{
		System.err.println("Test5 static block");
	}

	public static void main(String[] args) {
			Parent2 parent;
			System.err.println("-------------");
			parent=new Parent2();
			System.err.println(Parent2.a);
			System.err.println(Child2.b);
		}
}

/**

* Test5 static block
* -------------
* Parent2 static block
* 3
* Child2 static block
* 4
*
*/
class Parent2{
	public static final int a=3;
	static{
		System.err.println("Parent2 static block");
	}

}

class Child2 extends Parent2{
	public static int b=4;
	static{
		System.err.println("Child2 static block");
	}
}

只有当程序访问的静态变量或静态方法确实在当前接口定义时,才可以认为是对类或接口的主动使用。

public class Test6 {
	public static void main(String[] args) {
		System.err.println(Child3.a);
		Child3.doSomething();
	}
}

/**
* Parent3 static block
* 3
* doSomething
* @author coderacademy
*/
class Parent3{
	static int a=3;
	static {
		System.err.println("Parent3 static block");
	}

	static void doSomething(){
		System.err.println("doSomething");
	}

}

class Child3 extends Parent3{
	static{
		System.err.println("Child3 static block");
	}
}

调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

public class Test7 {
  public static void main(String[] args) throws ClassNotFoundException {
		ClassLoader loader=ClassLoader.getSystemClassLoader();
		Class<?> clazz=loader.loadClass("com.jvm.classloader.Z");
		System.err.println("------------------------");
		clazz=Class.forName("com.jvm.classloader.Z");
  }
}

/**
* ------------------------
*Z static block
* @author coderacademy
*/
class Z{
  static{
	  System.err.println("Z static block");
  }
}
3.2 被动使用

除去以上六种主动使用以外的使用都是被动使用,都不会导致类的初始化。所有的java虚拟机实现必须在每个类或接口被java程序首次主动使用时才初始化他们。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其方法存进运行时数据区的方法区内。然后在堆区创建一个Java.lang.Class对象,用来封装在类在方法区内的数据结构。

image.png

四、 加载class文件的方式

4.1 本地系统中直接加载
  • 通过网络下载.class文件(java.net.URLClassLoader(URL[] urls))
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将java源文件动态编译为.class文件。
    类的加载的最终产品是位于堆区中的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
4.2、两种类型的类加载器
4.2.1 Java虚拟机自带的加载器
  • 根类加载器(Bootstrap)。使用C++编写,程序员无法在java代码中获得该类。
  • 扩展类加载器(Extension),使用java代码实现
  • 系统类加载器(System),应用加载器,使用java代码实现
4.2.2 用户自定义的类加载器
  • java.lang.ClassLoader的子类
  • 用户可以定制类的加载方式
    public ClassLoader getClassLoader()方法。针对这个类返回一个个加载器,但是某些实现可能会返回null代表根类加载器。如果使用根类加载器加载类,那么这个方法就会返回null;例:
public class BootStrapTest {
	public static void main(String[] args) throws Exception {
		Class clazz=Class.forName("java.lang.String");
		ClassLoader loader=clazz.getClassLoader();
		/**
		* 打印结果为null
		*/
		System.err.println(loader);
		Class clazz2=Class.forName("com.jvm.classloader.C");
		ClassLoader loader2=clazz2.getClassLoader();
		/**
		* 打印结果为:sun.misc.Launcher$AppClassLoader@54a5f709 应用加载器
		 */
		System.err.println(loader2);
	}
}

class C{

}

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2023年度总结:但行前路,不负韶华

​ &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;Vir2021GKBS &#x…

Pure Mathematics 3-(磨课课件)-反三角函数求导(更新中)

6.6 Differentiating trigonometric functions&#xff08;反三角函数求导&#xff09; Edexcel Pure Mathematics 3(2018版本教材) /-------------------------------------------------------------------------------------------------------------------- Prior Knowledge…

第三十八周周报:文献阅读 +BILSTM+GRU+Seq2seq

目录 摘要 Abstract 文献阅读&#xff1a;耦合时间和非时间序列模型模拟城市洪涝区洪水深度 现有问题 提出方法 创新点 XGBoost和LSTM耦合模型 XGBoost算法 ​编辑 LSTM&#xff08;长短期记忆网络&#xff09; 耦合模型 研究实验 数据集 评估指标 研究目的 洪…

双向冒泡排序的数据结构实验报告

目录 实验目的&#xff1a; 实验内容&#xff08;实验题目与说明&#xff09; 算法设计&#xff08;核心代码或全部代码&#xff09; 运行与测试&#xff08;测试数据和实验结果分析&#xff09; 总结与心得&#xff1a; 实验目的&#xff1a; 理解双向冒泡排序算法的原…

2024年AI红利:抓住AI内容写作、绘画、数字人、等四大变现机遇

2023年见证了人工智能大模型的爆发&#xff0c;其影响力超出了科技界范畴&#xff0c;成为推动社会进步的重要力量。大模型的突破性进展引起了全球关注&#xff0c;被视为科技发展4.0时代的革命性创新。而每一次革命性创新都是一把双刃剑&#xff0c;随之而来的互联网大裁员事件…

「服务器」4.新手小白如何安装服务器环境-宝塔

刚开始初始化好的服务器&#xff0c;使用了阿里云客户端&#xff0c;看着网络脚本乱装&#xff0c;后来决定宝塔环境发现有重复的环境&#xff0c;遂决定重新初始化一下&#xff0c;然后重头干起。 重置服务器 将服务器关闭运行状态后&#xff0c;点击重新初始化云盘即可重新初…

C语言入门教程,C语言学习教程(第三部分:C语言变量和数据类型)一

第三部分&#xff1a;C语言变量和数据类型 本章也是C语言的基础知识&#xff0c;主要讲解变量、数据类型以及运算符&#xff0c;这其中涉及到了数据的存储格式以及不同进制。 一、大话C语言变量和数据类型 在《数据在内存中的存储&#xff08;二进制形式存储&#xff09;》一…

小程序商城搭建:快速入门指南

随着移动互联网的普及&#xff0c;小程序商城逐渐成为了商家们进行线上销售的重要渠道。如果你也想搭建一个小程序商城&#xff0c;那么本文将为你介绍如何使用乔拓云这一第三方小程序搭建平台来轻松搭建自己的小程序商城。 一、选择合适的第三方小程序搭建平台 在选择第三方小…

Java中的网络编程

文章目录 网络基础知识IP 地址端口协议 Java 中网络编程InetAddress&#xff08;静态类&#xff09;UDP 通信原理UDP 发送数据步骤UDP 接收数据步骤UDP 发送接收案例 TCP 通信原理TCP 发送数据步骤TCP 接收数据步骤TCP 发送接收案例 网络基础知识 概述&#xff1a;在网络通信协…

vscode设置python脚本运行参数

1 添加配置文件 点击到你要配置的python文件&#xff0c;然后右上角点击 运行 &#xff0c;再点击 添加配置 再点击 “Pyhton文件” 选项&#xff08;其实就是在选择 当前的python文件 进行配置&#xff09; 接着就生成了配置文件 lanunch.json 2 参数配置 再上面代码的基础上…

206. 反转链表(Java)

题目描述&#xff1a; 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 输入&#xff1a; head [1,2,3,4,5] 输出&#xff1a; [5,4,3,2,1] 代码实现&#xff1a; 1.根据题意创建一个结点类&#xff1a; public class ListNode {int val…

Flask 菜品管理

common/libs/Helper.py getDictFilterField() 方法 用于在web/templates/food/index.html中展示菜品分类 如何能够通过food里面的cat_id获取分类信息呢&#xff1f;只能通过for循环&#xff0c;这样会很麻烦&#xff0c;所以定义了这个方法。 这个方法可以的查询返回结果…

传统 VC 机构,是否还能在 Fair launch 的散户牛市中胜出?

LaunchPad 是代币面向市场的重要一环&#xff0c;将代币推向市场&#xff0c;加密项目将能够通过代币的销售从市场上募集资金&#xff0c;同时生态也开始进入全新的发展阶段。而对于投资者来说&#xff0c;早期打新市场同样充满着机会&#xff0c;参与 LaunchPad 对于每一个投资…

通过iFrame嵌入Grafana页面或pannel

前言 在当前数据驱动的时代&#xff0c;有效地可视化和监控关键性能指标变得至关重要。Grafana&#xff0c;作为一个开源的监控解决方案&#xff0c;提供了强大的功能来呈现和分析数据&#xff0c;从而帮助用户及时洞察和响应各种情况。随着技术的不断发展&#xff0c;将这些信…

地图移动逻辑

主要的一些问题 0. 可能会很久没收到,,或者一下子同时受到很多个同步的包 关于坐标滞后导致的一些游戏逻辑问题,比如攻击命中的判定问题等,一般是以服务器数据为判定依据,逻辑判定还是以服务器为主,客户端主要做表现。 1. 插值 关于坐标上报频率,我采取的是每100ms 或…

RT-Thread基于AT32单片机的485应用开发(二)

在上篇RT-Thread基于AT32单片机的485应用开发&#xff08;一&#xff09;中实现了RS485收发&#xff0c;但总觉得效率不高&#xff0c;函数封装也不完善。考虑到RS485总线应用都是主从式结构&#xff0c;比如工业领域常用的Modbus协议&#xff0c;都是以帧为单位进行收发&#…

【Java集合篇】 ConcurrentHashMap在哪些地方做了并发控制

ConcurrentHashMap在哪些地方做了并发控制 ✅典型解析✅初始化桶阶段&#x1f7e2;桶满了会自动扩容吗&#x1f7e0;自动扩容的时间频率是多少 ✅put元素阶段✅扩容阶段&#x1f7e0; 拓展知识仓&#x1f7e2;ConcurrentSkipListMap和ConcurrentHashMap有什么区别☑️简单介绍一…

Chrome禁用第三方Cookie,有什么影响?

2024年&#xff0c;Chrome将要正式禁用第三方Cookie了&#xff0c;这个变化对Web开发者来说是非常重要的&#xff0c;因为它将改变开发者如何设计网站以及如何搜集和使用用户数据。这是怎么一回事&#xff0c;到底有什么具体影响&#xff1f; 什么是Cookie&#xff1f; 随着互…

支持向量机(Support Vector Machines,SVM)

什么是机器学习 支持向量机&#xff08;Support Vector Machines&#xff0c;SVM&#xff09;是一种强大的机器学习算法&#xff0c;可用于解决分类和回归问题。SVM的目标是找到一个最优的超平面&#xff0c;以在特征空间中有效地划分不同类别的样本。 基本原理 超平面 在二…

【漏洞复现】锐捷EG易网关cli.php后台命令执行漏洞

Nx01 产品简介 锐捷EG易网关是一款综合网关&#xff0c;由锐捷网络完全自主研发。它集成了先进的软硬件体系架构&#xff0c;配备了DPI深入分析引擎、行为分析/管理引擎&#xff0c;可以在保证网络出口高效转发的条件下&#xff0c;提供专业的流控功能、出色的URL过滤以及本地化…