类加载子系统之类的生命周期(待完善)

0、前言

文中大量图片来源于 B站 黑马程序员

0.1、类加载子系统在 JVM 中的位置

image-20240429141045222

类加载器负责的事情是:加载、链接、解析

0.2、与类的生命周期相关的虚拟机参数

参数描述
-XX:+TraceClassLoading打印出加载且初始化的类

1、类的生命周期

在这里插入图片描述
在这里插入图片描述

堆上的变量在分配空间的时候隐式设置默认初始值(广义0),其中类变量在准备阶段(Preparation)分配空间,成员变量在使用阶段(Using)分配空间

1.1、加载阶段(懒加载)

懒加载的含义是:并不会加载 jar 包中所有的字节码,使用到才会进行加载

加载阶段流程:

  1. 通过类的全限定名从某个源位置获取定义此类的二进制字节流(内存)
  2. 这个字节流被解析转换为方法区的数据结构(InstanceKlass)
  3. 在堆空间中生成一个代表这个类的 java.lang.Class 对象,java.lang.Class 对象 和 InstanceKlass 对象互相指向。作为方法区中这个类的各种操作的访问入口

static 静态字段在 JDK 8 之后和 java.lang.Class 对象存储在一起,即存放在堆空间中
在这里插入图片描述

什么是 InstanceKlass

InstanceKlass 是 Java 类在 JVM 中的一个快照,JVM 将从字节码文件中解析出来的常量池,类字段,类方法等信息存储到 InstanceKlass 中,这样 JVM 在运行期便能通过 InstanceKlass 来获取Java类的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。这也是Java反射机制的基础,不需要创建对象,就可以查看加载类中的方法,属性等等信息。

Class 对象由 class 字节码 + ClassLoader 共同决定,不同的 ClassLoader 加载同一个 class 字节码得到不同的 Class 对象,即 class 字节码不能够唯一确定 Class 对象。

1.2、链接阶段

子阶段描述
验证校验魔数、版本号等
准备为类变量(static)分配内存空间,并设置默认值(0)
解析将符号引用处理为直接引用

1.3、初始化阶段

见下面的测试案例

1.4、使用阶段

分为主动使用和被动使用两大类,二者区别在于被动使用的情况下,类只会进行加载而不会进行初始化。
(TODO:主动使用、被动使用的情况,添加一个思维导图进行分类)

1.5、卸载阶段

和 GC 垃圾回收相关

2、用于理解类生命周期的测试案例

2.1、不考虑父子类继承的情况

案例一:认识 <clinit><init>

Java 源代码

public class ClassLifeCycleTest01 {public ClassLifeCycleTest01() {System.out.println("<init>...2");}{// 在字节码层面,这些非静态代码块最终被添加到构造函数的最前面System.out.println("<init>...1");}static {System.out.println("<clinit>...");}public static void main(String[] args) {System.out.println("ClassLifeCycleTest01 main...");new ClassLifeCycleTest01();new ClassLifeCycleTest01();}
}

字节码

<init> 方法的字节码

// 成员方法的第一个形参是this(从局部变量表可知),将this压入操作数栈
0 aload_0// 调用父类(Object)的<init>方法,即构造器方法中隐藏在首行的super()
1 invokespecial #1 <java/lang/Object.<init> : ()V>// System.out.println("<init>...1"),先执行构造器方法外面的代码
4 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
7 ldc #3 <<init>...1>
9 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>// System.out.println("<init>...2"),再执行构造器方法里面的代码
12 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
15 ldc #5 <<init>...2>
17 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>20 return

输出结果

<clinit>...
ClassLifeCycleTest01 main...
<init>...1
<init>...2
<init>...1
<init>...2

总结

  • <init> 方法(实例对象初始化)的逻辑:
    1. 构造器方法作为入口
    2. 先执行super()
    3. 再执行构造方法外部的代码逻辑(顺序拼接)
    4. 最后执行构造方法内部的代码逻辑
  • <clinit> 方法是存在线程安全问题的,但虚拟机会对这个过程加锁,不需要程序员处理

案例二:强化理解 <clinit><init> 的生成逻辑

Java 源代码

/*** 目的:通过一些类变量或成员变量的赋值,进一步理解类生命周期的过程* 1. 在变量声明之前的代码块中,该变量只可以作为右值表达式,而不能作为左值* 2. 等价形式为:* 2.1、将变量声明在类的最前面,初始化为0,* 2.2、然后按照再将显式赋值和代码块赋值按照出现顺序,整合为一个init方法或clinit方法*/
public class ClassLifeCycleTest02 {// 变量classVar01定义在静态代码块之前static int classVar01;static {System.out.println("ClassLifeCycleTest02 clinit ...");classVar01 = 20;// System.out.println(classVar01);//正常classVar02 = 10;// classVar02 = classVar01 + 1; //正常,classVar02可以作为右值// classVar02 = classVar02 + 1; //异常,classVar01不可以作为左值// System.out.println(classVar02);//异常}// 变量classVar02定义在静态代码块之后static int classVar02 = 100;public ClassLifeCycleTest02() {System.out.println("ClassLifeCycleTest02 constructor ...");instanceVar = 30;}{System.out.println("ClassLifeCycleTest02 init ...");instanceVar = 10;// instanceVar = instanceVar + 2;//异常// System.out.println(instanceVar);//异常}// instanceVar的值变化过程: 0->10->20->30private int instanceVar = 20;public static void main(String[] args) {int var01 = ClassLifeCycleTest02.classVar01;int var02 = ClassLifeCycleTest02.classVar02;System.out.println(var01);System.out.println(var02);ClassLifeCycleTest02 demo = new ClassLifeCycleTest02();int var = demo.instanceVar;System.out.println(var);}
}

字节码

<clinit> 方法的字节码

0 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
3 ldc #12 <ClassLifeCycleTest02 clinit ...>
5 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>//classVar01 = 20
8 bipush 20
10 putstatic #7 <org/example/lifecycle/ClassLifeCycleTest02.classVar01 : I>//classVar02 = 10
13 bipush 10
15 putstatic #8 <org/example/lifecycle/ClassLifeCycleTest02.classVar02 : I>// classVar02 = 100
18 bipush 100
20 putstatic #8 <org/example/lifecycle/ClassLifeCycleTest02.classVar02 : I>23 return

<init> 方法的字节码

//1、将this压入操作数栈0 aload_0//2、调用父类的<init>方法,这里父类是Object1 invokespecial #1 <java/lang/Object.<init> : ()V>//3、输出字符串
4 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
7 ldc #3 <ClassLifeCycleTest02 init ...>
9 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>//(成员变量在堆上分配空间时会设置默认初始值0,无法通过字节码体现出来)
//4、this.instanceVar = 10
12 aload_0
13 bipush 10
15 putfield #5 <org/example/lifecycle/ClassLifeCycleTest02.instanceVar : I>//5、this.instanceVar = 20
18 aload_0
19 bipush 20
21 putfield #5 <org/example/lifecycle/ClassLifeCycleTest02.instanceVar : I>//6、输出字符串
24 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
27 ldc #6 <ClassLifeCycleTest02 constructor ...>
29 invokevirtual #4 <java/io/PrintStream.println : (Ljava/lang/String;)V>//7、this.instanceVar = 30
32 aload_0
33 bipush 30
35 putfield #5 <org/example/lifecycle/ClassLifeCycleTest02.instanceVar : I>38 return

输出结果

ClassLifeCycleTest02 clinit ...
20
100
ClassLifeCycleTest02 init ...
ClassLifeCycleTest02 constructor ...
30

总结

  • super() 调用的不是父类的构造器,而是父类的 <init> 方法

  • <clinit> 方法和 <init> 方法的生成逻辑是相同的,区别在于前者针对类变量,后者针对成员变量

  • 在变量声明之前的代码块中,如果出现了该变量,那么该变量只能够作为右值表达式,而不能作为左值表达式,例如 classVar02instanceVar 变量

  • 针对下面的代码块,可以进行等价处理

    static{classVar = 10;
    }
    static int classVar = 20;
    
    // 变量声明提前
    static int classVar = 0;
    static{// 顺序添加原来代码块和显式赋值的过程classVar = 10;classVar = 20;
    }
    

2.2、考虑父子类继承的情况

案例三:隐藏的 super() 就是调用父类的 <init> 方法

/*** 特别事项:和Main进行对比,一种类的被动使用导致类没有执行clinit初始化*/
public class ClassLifeCycleTest03 extends ClassLifeCycleTest02 {// 变量classVar01定义在静态代码块之前static int classVar03;static {System.out.println("ClassLifeCycleTest03 clinit ...");classVar03 = 20;}private int instanceVar = -20;{System.out.println("ClassLifeCycleTest03 init ...");}public ClassLifeCycleTest03() {System.out.println("ClassLifeCycleTest03 constructor ...");instanceVar = -30;}public static void main(String[] args) {int var01 = ClassLifeCycleTest03.classVar01;System.out.println(var01);int var02 = ClassLifeCycleTest03.classVar02;System.out.println(var02);ClassLifeCycleTest03 demo = new ClassLifeCycleTest03();int var = demo.instanceVar;System.out.println(var);}
}

字节码

<init> 的字节码

0 aload_0// 这里可以清晰看到调用父类的<init>方法,其它部分在之前的案例中已经介绍
1 invokespecial #1 <org/example/lifecycle/ClassLifeCycleTest02.<init> : ()V>4 aload_0
5 bipush -20
7 putfield #2 <org/example/lifecycle/ClassLifeCycleTest03.instanceVar : I>10 aload_0
11 bipush -30
13 putfield #2 <org/example/lifecycle/ClassLifeCycleTest03.instanceVar : I>16 return

main 的字节码

// 注意这里的类变量,是ClassLifeCycleTest03.classVar01
0 getstatic #3 <org/example/lifecycle/ClassLifeCycleTest03.classVar01 : I>
3 istore_14 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
7 iload_1
8 invokevirtual #5 <java/io/PrintStream.println : (I)V>11 getstatic #6 <org/example/lifecycle/ClassLifeCycleTest03.classVar02 : I>
14 istore_215 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
18 iload_2
19 invokevirtual #5 <java/io/PrintStream.println : (I)V>// ClassLifeCycleTest03 demo = new ClassLifeCycleTest03()的字节码
// new:分配对象空间,设置广义0值,并将对象地址压入操作数栈
// dup:复制栈顶元素
// invokespecial:调用父类的<init>方法(属于字节码层面的方法)
// 将栈顶元素赋值给demo局部变量
22 new #7 <org/example/lifecycle/ClassLifeCycleTest03>
25 dup
26 invokespecial #8 <org/example/lifecycle/ClassLifeCycleTest03.<init> : ()V>
29 astore_330 aload_3
31 getfield #2 <org/example/lifecycle/ClassLifeCycleTest03.instanceVar : I>
34 istore 436 getstatic #4 <java/lang/System.out : Ljava/io/PrintStream;>
39 iload 4
41 invokevirtual #5 <java/io/PrintStream.println : (I)V>44 return

输出结果

ClassLifeCycleTest02 clinit ...
ClassLifeCycleTest03 clinit ...
20
100
ClassLifeCycleTest02 init ...
ClassLifeCycleTest02 constructor ...
ClassLifeCycleTest03 init ...
ClassLifeCycleTest03 constructor ...
-30

总结

  • 父类优先于子类(初始化、类加载)

案例四:类的被动使用(调用父类的静态变量)

在案例三中,我们直接在 ClassLifeCycleTest03 这个类中的 main 方法进行测试,而 main 方法被调用会默认去加载当前类,因此会丢失掉一些现象。因此,我们额外定义一个 Main 类来作为测试入口

import org.junit.jupiter.api.Test;public class Main {/*** 和ClassLifeCycleTest03类中的main()方法进行对比*/@Testpublic void compareClassLifeCycleTest03Test01() {int var01 = ClassLifeCycleTest03.classVar01;System.out.println(var01);int var02 = ClassLifeCycleTest03.classVar02;System.out.println(var02);}@Testpublic void compareClassLifeCycleTest03Test02() {int var03 = ClassLifeCycleTest03.classVar03;System.out.println(var03);}}

输出结果

ClassLifeCycleTest02 clinit ...
20
100
ClassLifeCycleTest02 clinit ...
ClassLifeCycleTest03 clinit ...
20

总结

类(Class)类变量(static)调用示例类加载(Loading)类初始化(Initialization)
子类(ClassLifeCycleTest03)子类(classVar03)ClassLifeCycleTest03.classVar03父类、子类父类、子类
子类(ClassLifeCycleTest03)父类(classVar02)ClassLifeCycleTest03.classVar02父类、子类父类
父类(ClassLifeCycleTest02)父类(classVar02)ClassLifeCycleTest01.classVar02父类父类

注:可以通过添加虚拟机参数 -XX:+TraceClassLoading 查看已经加载的类,再通过 Ctrl + f 来搜索某个类是否被加载

2.3、考虑常量的编译期优化

代码中所有对常量的引用,都会在编译后直接被替换为相应的字面量

在 Java 中什么是常量?

  • 从字节码角度来看,含有 ConstantValue 信息的字段是常量
  • 从 Java 代码角度来看,使用 static final 修饰,且右侧表达式中只包含字面量(1、1.0、“hello” 等)或常量
    // 常量:static final修饰,右侧只包含字面量
    static final int NUM_1 = 100;
    static final int NUM_2 = 200;// 常量:static final修饰,右侧只包含常量
    static final int SUM = NUM_1 + NUM_2;// 字符串同理
    static final String S_1 = "HELLO";
    static final String S_2 = "WORLD";
    static final String S_3 = S_1 + S_2;// 不是常量,右侧出现new对象,这就是static final修饰的变量不一定是常量的原因。
    // 其它类型的引用变量必定是new出来的对象,而String类型却有两种赋值方式
    static final String S_4 = new String(S_1 + S_2);
    

Java 源代码

/*** 常量的编译期优化* * 可以通过反编译看出Demo.VAR被替换为字面量"Hello World",因此不会触发 Demo 的加载和初始化*/
public class ClassLifeCycleTest04 {static class Demo {private static final String VAR = "Hello World!";static {System.out.println("Demo clinit ...");}}public static void main(String[] args) {// 在编译期便完成对常量的替换,所以不会加载 Demo.class,更不会初始化。// 注意这里 main方法并不是 Demo 类的方法System.out.println(Demo.VAR);}
}

反编译后的 Java 代码

public class ClassLifeCycleTest04 {public ClassLifeCycleTest04() {}public static void main(String[] args) {// 可以得出结论,在编译后的字节码文件中,Demo.VAR直接被替换为"Hello World"字面量System.out.println("Hello World!");}static class Demo {private static final String VAR = "Hello World!";Demo() {}static {System.out.println("Demo clinit ...");}}
}

2.4、验证类变量(static)在准备阶段(Preparation)设置默认值

思路:对类变量只进行声明,而不显式赋值。观察字节码中是否有 <clinit> 方法。

public class ClassLifeCycleTest06 {static int num;public static void main(String[] args) {System.out.println(num);}
}

2.5、使用 HSDB 工具来判断 static 变量的存储位置

(TODO:添加过程细节)

注意:inspect 找到的是 InstanceKlass 对象
在这里插入图片描述

在这里插入图片描述

3、补充

  1. 静态变量(static)的存放位置
    • JDK 7 及之前:方法区(InstanceKlass)
    • JDK 8 及之后:堆(java.lang.Class)

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

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

相关文章

【C++STL详解(三)】------vector的介绍与使用

目录 前言 一、关于数组 二、vector的介绍 三、vector的使用 Ⅰ、默认成员函数 1.构造函数 2.赋值重载 3.析构函数 Ⅱ、容量 1.size(&#xff09; 2.capacity() 3.empty() 4.resize() 5.reserve() Ⅲ、遍历操作 1.迭代器 begin() end()&#xff08;正向迭代器…

element 分页切换时:current-page无效 页数不会跟着一起切换

问题回溯&#xff1a;使用el-pagination组件 选择切换当前分页 页数为2 问题结果&#xff1a;el-pagination组件 当前页切换失败 一直都是 1&#xff0c;接口传参分页数据是2&#xff0c;打印当前分页也是2 解决方案1&#xff1a;使用 current-page参数 .sync 修饰符 解决方案2…

rust将json字符串直接转为map对象或者hashmap对象

有些时候我们还真的不清楚返回的json数据里面到底有哪些数据&#xff0c;数据类型是什么等&#xff0c;这个时候就可以使用批处理的方式将json字符串转为一个对象&#xff0c;然后通过这个对象的get方法来获取json里面的数据。 pub async fn test_json(&self) {let json_st…

STM32的TIM输入捕获和PWMI详解

系列文章目录 STM32单片机系列专栏 C语言术语和结构总结专栏 文章目录 1. IC输入捕获 2. 频率测量 3. 主模式、从模式、触发源选择 4. 输入捕获基本结构 5. PWMI模式 6. 代码示例 6.1 PWM.c 6.2 PWM.h 6.3 IC.c 6.4 IC.h 6.5 完整工程文件 输出比较可以看下面这篇…

numpy+matplotlib绘制阿基米德螺线

【第10次课]实验十一数据可视化及应用】 声明&#xff1a;著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 绘制阿基米德螺线&#xff0c;运行效果如图所示。 参数方程为: x icosi yisini 其中半径i和圆心角i变化一致&#xff0c;取值范围为…

#你觉得未来的智慧工地是什么样子的#

#你觉得未来的智慧工地是什么样子的# 有人说“现阶段的智慧工地都是噱头&#xff0c;实际用处不大”&#xff1b;也有人将智慧工地吹嘘上天。那么&#xff0c;随着技术的发展&#xff0c;你觉得未来的智慧工地会是什么样子呢&#xff1f; 随着大数据时代的到来&#xff0c;未来…

Three.js杂记(十五)—— 汽车展览(下)

在上一篇文章Three.js杂记&#xff08;十四&#xff09;—— 汽车展览上 - 掘金 (juejin.cn)中主要对切换相机不同位置和鼠标拖拽移动相机焦点做了简单的应用。 那么现在聊聊该如何实现汽车模型自带的三种动画展示了&#xff0c;实际上可以是两种汽车前后盖打开和汽车4车门打开…

抑郁后的症状表现——XWX-QP大小鼠强迫游泳桶硬件

简单介绍&#xff1a; 大小鼠强迫游泳桶硬件主要用于抗抑郁的研究。适用于大鼠、小鼠或其他实验室动物&#xff0c;通过将实验动物置于一个局限的环境中&#xff0c;动物在该环境中拼命挣扎试图逃跑又无法逃脱&#xff0c;从而提供了一个无可回避的压迫环境&#xff0c;动物的…

如何提取二维码文本信息?文本二维码提取内容的方法

如何分解出二维码中的文本信息呢&#xff1f;很多商家在做活动时会给每个用户生成一个单独的二维码&#xff0c;每一个二维码中有单独的编号信息&#xff0c;那么当我们收集到用户的二维码时&#xff0c;如何操作才能够提取二维码中的编号信息呢&#xff1f;想要解决这个问题可…

双目深度估计原理立体视觉

双目深度估计原理&立体视觉 0. 写在前面1. 双目估计的大致步骤2. 理想双目系统的深度估计公式推导3. 双目标定公式推导4. 极线校正理论推导 0. 写在前面 双目深度估计是通过两个相机的对同一个点的视差来得到给该点的深度。 标准系统的双目深度估计的公式推导需要满足:1)两…

Vue3+ts(day04:watch、watchEffect)

学习源码可以看我的个人前端学习笔记 (github.com):qdxzw/frontlearningNotes 觉得有帮助的同学&#xff0c;可以点心心支持一下哈&#xff08;笔记是根据b站上学习的尚硅谷的前端视频【张天禹老师】&#xff0c;记录一下学习笔记&#xff0c;用于自己复盘&#xff0c;有需要学…

树莓派5用docker运行Ollama3

书接上回&#xff0c;树莓派5使用1panel安装 Ollama 点击终端就可以进入容器 输入以下代码 ollama run llama3Llama3 是市场推崇的版本。您的 树莓派5上必须至少有 4.7GB 的可用空间&#xff0c;因此用树莓派玩机器学习就必须配置大容量的固态硬盘。用1panel部署网络下载速度…

Python | Leetcode Python题解之第58题最后一个单词的长度

题目&#xff1a; 题解&#xff1a; class Solution:def lengthOfLastWord(self, s: str) -> int:ls[]for i in s.split():ls.append(i)return len(ls[-1])

跟TED演讲学英文:The future will be shaped by optimists by Kevin Kelly

The future will be shaped by optimists Link: https://www.ted.com/talks/kevin_kelly_the_future_will_be_shaped_by_optimists Speaker: Kevin Kelly Date: August 2021 文章目录 The future will be shaped by optimistsIntroductionVocabularyTranscriptSummary后记 In…

Verilog基础语法——状态机(类型、写法、状态编码方式)

Verilog基础语法——状态机&#xff08;类型、写法、状态编码方式&#xff09; 写在前面一、状态机类型二、状态机写法2.1 一段式2.2 两段式2.3 三段式 三、状态机状态编码方式写在后面 写在前面 在FPGA设计过程&#xff0c;经常会设计状态机用于控制整个硬件电路的工作进程&am…

基于Vue3的Axios异步请求

基于Vue3的Axios异步请求 1. Axios安装与应用2. Axios网络请求封装3. axios网络请求跨域前端解决方案server.proxy 1. Axios安装与应用 Axios是一个基于promise的网络请求库&#xff0c;Axios.js.中文文档&#xff1a;https://axios.js.cn/ 安装&#xff1a;npm install --sa…

有没有一种可能性,你不投递简历,让HR主动联系你

你是否觉得自己得主动给某个公司投递了简历,他们才会联系你,亦或者是自己得主动在招聘APP上联系那个BOSS,他才会反过来跟你说话,又或者是你千方百计的跟他打招呼了,还是没有回应,这一节有可能让你明白,有时候是可以,你不主动,他也会主动联系你的。 目录 1 简历是如何…

QT:小项目:登录界面 (下一个连接数据库)

一、效果图 登录后&#xff1a; 二、项目工程结构 三、登录界面UI设计 四主界面 四、源码设计 login.h #ifndef LOGIN_H #define LOGIN_H#include <QDialog>namespace Ui { class login; }class login : public QDialog {Q_OBJECTpublic:explicit login(QWidge…

Spark原理之Cache Table的工作原理及实现自动缓存重复表的思考

CACHE TABLE的能力 使用此语法&#xff0c;可以由用户自定义要缓存的结果集&#xff0c;实际上就是一个临时表&#xff0c;不过数据存储在Spark集群内部&#xff0c;由Application所分配的executors管理。 一旦定义了一个缓存表&#xff0c;就可以在SQL脚本中随处引用这个表名…

Ansible自动化运维工具主机清单配置

作者主页&#xff1a;点击&#xff01; Ansible专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月24日12点21分 Ansible主机清单文件用于定义要管理的主机及其相关信息。它是Ansible的核心配置文件之一&#xff0c;用于Ansible识别目标主机并与其建立连接。 …