【JVM】深入理解类加载机制(一)

深入理解类加载机制

Klass模型

在这里插入图片描述

Java的每个类,在JVM中都有一个对应的Klass类实例与之对应,存储类的元信息如:常量池、属性信息、方法信息…从继承关系上也能看出来,类的元信息是存储在元空间的。普通的Java类在JVM中对应的是InstanceKlass(C++)类的实例,再来说下它的三个子类:

  • 1.InstanceMirrorKlass:用于表示java.lang.Class,Java代码中获取到的Class对象,实际上就是这个C++类的实例,存储在堆区,学名镜相类
  • 2.InstanceRefKlass:用于表示java/lang/ref/Reference类的子类
  • 3.InstanceClassLoaderKlass:用于遍历某个加载器的类

Java中的数组不是静态数据类型,而是动态数据类型,即是运行期生成的,Java数组的元信息用ArrayKlass的子类来表示:

  • 1.TypeArrayKlass:用于表示基本类型的数组
  • 2.ObjArrayKlass:用于表示引用类型的数组

总结:
非数组:
InstanceKlass -> 普通的类在JVM中对应的C++类 方法区
InstanceMirrorKlass -> 对应的是Class对象 镜像类 堆区
数组:
基本类型数组
boolean、byte、char、short、int、float、long、double -> TypeArrayKlass
引用类型数组: ObjArrayKlass

为什么还要有镜像类?
是为了安全,由JVM控制可以将哪些参数返回给用户

实操:

public class Hello {public static void main(String[] args) {int[] a = new int[] {1,2,3};Hello[] hello = new Hello[2];Hello h = new Hello();while(true);}
}

利用HSDB查看main线程的调用栈,由于栈的规则是先进后出,也就是说意味着,当前方法栈的栈底存放的是当前方法的参数args,其次是int数组,Hello对象数组,我们可以查看它们的内存地址中都包含了哪些内容
在这里插入图片描述
在这里插入图片描述
基本数据类型的klass模型,还可以看到数组的内容
在这里插入图片描述
引用类型数组的klass模型,我们在代码中创建的Hello数组对象引用都是空的
在这里插入图片描述
也就是_java_mirror,这里c++上的注解也是说明了这个InstanceMirroKlass的存在

类加载的过程

类的加载由7个步骤完成,如图所示。类的加载说的是前5个阶段。

加载

  • 1.通过类的全限定名获取存储该类的class文件(没有指明必须从哪获取)
  • 2.解析成运行时数据,即instanceKlass实例,存放在方法去
  • 3.在堆区生成该类的Class对象,即instanceMirrorKlass实例

程序随便你怎么写,随便你用什么语言,只要能达到这个效果即可。就是说你可以改写openjdk源码,你写的程序能达到这三个效果即可。
预加载:包装类、String、Thread
因为没有指明必须从哪获取class文件,脑洞大开的工程师们开发了这些:

  • 1.从压缩包中读取。如jar、war
  • 2.从网络中获取,如Web Applet
  • 3.动态生成,如动态代理、CGLIB
  • 4.由其他文件生成,如JSP
  • 5.从数据库读取
  • 6.从加密文件中读取

验证

  • 1.文件格式验证。如验证class文件中是否包含魔数(CAFE BABE)、主次版本号是否在当前虚拟机处理范围之内
  • 2.元数据验证。如这个类是否有父类、这个类的父类是否继承了不允许被继承的类(如被final修饰的类)
  • 3.字节码验证。整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析确定程序语义是合法的,如保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载如本地变量表中
  • 4.符号引用验证。最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看作是堆类自身以外的(常量池中的各种符号引用)的信息匹配性校验,如符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用中的类和字段一级方法的访问性是否可以被当前类访问(比如调用静态方法,检查调用的方法是否可以被当前类调用)

准备

为静态变量分配内存、赋初值。实例变量是在创建对象的时候完成赋值的,没有赋初值这一说。如果是被final修饰,在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步
在这里插入图片描述

public class MyClassLoadHello {public static int v = 10;public static final int b = 11;public static void main(String[] args) {int a = 1;int b = 2;System.out.println(a + b);}
}

在这里插入图片描述

可以看到变量b多出了一个ConstantValue的属性,这个属性指向了常量池中11这个数值。准备阶段就会直接赋值
在这里插入图片描述
反观变量v则是在类的初始化方法块中

为什么要在准备阶段赋初值?为何不直接赋值?
(C++对象)InstanceMirrorKlass对象只是创建出来,并没有属性,把这个变量写入到Class对象中去,如果这个静态变量没有使用到,也没有赋初值,字节码指令中将不包含该变量.
如图所示,变量m并没有在字节码指令中,因为没有赋初值也没有进行使用
在这里插入图片描述
在这里插入图片描述

通过HSDB可以发现InstanceMirrorKlass对象是有变量m这个属性的,但是InstanceKlass对象却显示只有两个静态属性。是不是很奇怪?字节码指令中都没有这个变量,InstanceMirrorKlass对象中却有这个属性。其实也不难理解,InstsanceKlass对象是存储在方法区中的,可以表示类的静态属性信息。由于这个属性没有赋值也没有使用,字节码层面就直接优化掉了,我们知道反射的时候可以获取到这个类的所有信息所有属性以及所有方法不管其作用域的范围是什么,如果不给InstanceMirrorKlass对象赋值这个属性,那么在反射的时候就会拿不到,这其实违背了反射的规则。所以要有静态属性赋初值这个动作,来给InstasnceMirrorKlass对象赋上这个属性。

解析

在这里插入图片描述

将常量池中的符号引用转为直接引用。解析后的信息存储在ConstantPoolCache类实例中。其中会涉及到如下:

  • 1.类或接口的解析
  • 2.字段解析
  • 3.方法解析
  • 4.接口方法解析

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。可以理解为静态常量池的索引
直接引用(Direct References): 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的.某个变量的内存地址

解析的时机?
1.加载阶段解析常量池时(类加载以后马上解析 resolve的参数需要改为 => true)
2.用的时候
解析什么?只要是直接引用都需要解析
+1.继承的类、实现的接口

  • 2.属性
  • 3.方法

如何避免重复解析:
借助缓存,ConstantPoolCache(运行时常量池的缓存) if (klass -> is_resolved()) {}如图所示
常量池缓存:
key: 常量池的索引 2
value: String -> ConstantPoolEntry
静态属性是存储在堆区中的,静态属性的访问:

  • 1.去缓存中去找,如果有直接返回
  • 2.如果没有就触发解析
    底层实现:
  • 1.会找到直接引用
  • 2.会存储到常量池缓存中
    openjdk是第二种思路,在执行特定的字节码指令之前进行解析:anewarray、checkcase、getfield、instanceof、invokeddynamic、invokeinterface、invokesepcial、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield.
拓展知识:编译时常量池和运行时常量池

在Java中,常量池是class文件的一部分,它用于存储关于类和接口的常量以及一些符号引用。常量池分为两种:编译时常量池和运行时常量池。

  • 1.编译时常量池(Constant Pool)
    编译时常量池是在编译器生成的,它包含了类文件中的字面量(Literal)和符号引用(Symbolic References).
    字面量:如文本字符串、final常量值等
    符号引用:包括类和接口的全限定名、字段名称和描述符、方法名称和描述符。这些符号引用在类加载阶段或第一次使用时会被解析为直接引用。
    编译时常量池时.class文件的一部分,它随着类文件的生成而生成,每个.class文件都有一个自己的编译时常量池
  • 2.运行时常量池(Runtime Constant Pool)
    运行时常量池是类或接口在JVM运行时的一部分,当类被JVM加载时,JVM会根据.class文件中的编译时常量池来创建运行时常量池。运行时常量池是方法区中的一部分。
    动态性:运行时常量池具有动态性,它可以在运行期间想其中添加新的常量。例如,String的intern()方法可以将字符串常量添加到运行时常量池中
    解析:运行时常量池中的符号引用会在类加载过程中或第一次使用时被解析为直接引用。
    简而言之,编译时常量池是静态的,是.class文件的一部分,而运行时常量池是动态的,是JVM运行时数据区的一部分。运行时常量池在JVM的规范中是方法区的一部分,但在不同的JVM实现中可能会有所不同,如在HotSpot虚拟机中,它被放在了堆(Heap)中。

初始化

在这里插入图片描述

执行静态代码块,完成静态变量的赋值。类初始化阶段时类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度表达:初始化阶段时执行类构造器()方法的过程。

  • 1.()方法时由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,可以在前面的静态语句块中赋值,但是不能访问,如图所示
  • 2.()方法与类的构造函数(或者说实例构造器()方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类地()方法执行之前,父类的()方法已经执行完毕。因此在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object
  • 3.由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
  • 4.()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成()方法
  • 5.虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,知道活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法的那条线程退出()方法后,其他线程唤醒之后不会再次进入()方法。同一个类加载器下,一个类型只会初始化一次)。

何时初始化?主动使用时

  • 1.new、getstatic、putstatic、invokestatic
  • 2.反射
  • 3.初始化一个类的子类会去加载其父类
  • 4.启动类(main函数所在类)
  • 5.当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic、REF_put_static、REF_invoke_Static的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先触发其初始化。
实例
  • < clinit>()方法执行死锁示例1:
public class DeadLoopClass {static {if (true) {System.out.println(Thread.currentThread() + "init DeadLoopClass");while (true) {}}}public static void main(String[] args) {Runnable script = new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread() + "start");DeadLoopClass dlc = new DeadLoopClass();System.out.println(Thread.currentThread() + "end");}};Thread thread1 = new Thread(script);Thread thread2 = new Thread(script);thread1.start();thread2.start();}
}
Thread[main,5,main]init DeadLoopClass

一条线程在死循环模拟长时间操作,另外一条线程在阻塞等待执行clinit方法执行完毕后触发唤醒,但是一直等不到,所以就发生了死锁

  • < clinit >()方法执行死锁示例2:
public class InitDeadLock {public static void main(String[] args) throws InterruptedException {new Thread(() -> new A()).start();new Thread(() -> new B()).start();}
}class A {static {System.out.println("class A init");new B();}
}class B {static {System.out.println("class B init");new A();}
}

一个线程创建A对象,进而触发A的初始化,但是A的clinit方法中又创建B,又触发B的初始化,另一个线程的初始化则反过来,资源获取顺序不当造成了死锁

卸载

判定一个类是否是"无用的类"的条件相对一个实例对象或者"废弃常量"要苛刻很多。类需要同时满足下面3个条件才能算是"无用的类":

  • 1.该类的所有实例都已经被回收,也就是Java堆中不存在该类的任何实例。
  • 2.加载该类的ClassLoader已经被回收
  • 3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
    虚拟机可以堆满足上述3个条件的无用类进行回收,这里说的仅仅是"可以",而并不是和对象一样,不使用了,就必然会回收。

这也造成了很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中确实说过可以不要求虚拟机在方法区中实现垃圾收集,而且在方法区中进行垃圾收集"性价比"一般比较低:在队中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70~95%的空间,而永久代的垃圾收集效率远低于此

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

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

相关文章

4款AI 生成 PPT的工具,帮你赶上演示文稿的新趋势!

AI 生成 PPT 最大的优势就在于它能够帮助我们提高效率。如果我们自己制作的话就需要花费大量的时间去收集资料、构思布局、设计排版。而现在&#xff0c;有了AI工具&#xff0c;一切就迎刃而解&#xff0c;如果大家需要这样的工具&#xff0c;可以看看这4款。 1、笔灵办公 直通…

RabbitMQ 的工作原理

下面是rabbitmq 的工作原理图解 1、客户端连接到消息队列服务器&#xff0c;打开一个channel。 2、客户端声明一个exchange&#xff0c;并设置相关属性。 3、客户端声明一个queue&#xff0c;并设置相关属性。 4、客户端使用routing key&#xff0c; 在exchange和queue 之间…

为什么说网络安全行业是IT行业最后的红利?

前言 2023年网络安全行业的前景看起来非常乐观。根据当前的趋势和发展&#xff0c;一些趋势和发展可能对2023年网络安全行业产生影响&#xff1a; 5G技术的广泛应用&#xff1a;5G技术的普及将会使互联网的速度更快&#xff0c;同时也将带来更多的网络威胁和安全挑战。网络安全…

《向量数据库 Faiss 搭建与使用全攻略》

一、Faiss 概述 Faiss 是由 Facebook AI 团队开发的一款强大工具&#xff0c;在大规模数据处理和相似性搜索领域占据着重要地位。 在当今信息爆炸的时代&#xff0c;数据规模呈指数级增长&#xff0c;如何从海量数据中快速准确地找到相似的数据成为了关键挑战。Faiss 应运而生…

《黑神话.悟空》:一场跨越神话与现实的深度探索

《黑神话.悟空》&#xff1a;一场跨越神话与现实的深度探索 在国产游戏日益崛起的今天&#xff0c;《黑神话.悟空》以其独特的剧情、丰富的人物设定和深刻的主题&#xff0c;成为了无数玩家翘首以盼的国产3A大作。这款游戏不仅是一次对传统故事的创新演绎&#xff0c;更是一场对…

AIoTedge边缘计算平台V1.0版本发布

AIoTedge边缘计算平台V1.0&#xff0c;一款创新的AIoT解决方案&#xff0c;现已正式发布。该产品集成了NodeRED软网关、边缘物联网平台和边缘AI能力&#xff0c;为企业提供强大的边云协同能力。它支持设备管理和泛协议接入&#xff0c;确保不同设备间的无缝连接。AIoTedgeV1.0还…

[C#]winform基于深度学习算法MVANet部署高精度二分类图像分割onnx模型高精度图像二值化

【训练源码】 https://github.com/qianyu-dlut/MVANet 【参考源码】 https://github.com/hpc203/MVANet-onnxrun 【算法介绍】 二分图像分割&#xff08;DIS&#xff09;最近出现在从高分辨率自然图像中进行高精度对象分割方面。在设计有效的DIS模型时&#xff0c;主要的挑战是…

SDCS-IOE-2C 3ADT220090R007模块控制器

SDCS-IOE-2C 3ADT220090R007模块控制器 SDCS-IOE-2C 3ADT220090R007模块控制器 SDCS-IOE-2C 3ADT220090R007模块控制器 SDCS-IOE-2C 3ADT220090R007模块控制器引脚线 SDCS-IOE-2C 3ADT220090R007模块控制器说明书 SDCS-IOE-2C 3ADT220090R007模块控制器线路图 SDCS-IOE-2…

Snipaste 的一款替代工具 PixPin,支持 gif 截图、长截图和 OCR 文字识别,功能不是一点点强!

Snipaste 的一款替代工具 PixPin&#xff0c;支持 gif 截图、长截图和 OCR 文字识别&#xff0c;功能不是一点点强&#xff01; PixPin 的名字来源于“Pixel Pin”&#xff0c;简单来说是一个截图、贴图的工具&#xff0c;但是 PixPin 以截图和贴图两大功能为核心做了大量的优…

mysql速起架子

wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.21-linux-glibc2.12-x86_64.tar.xz 下载mysql tar xvJf mysql-8.0.21-linux-glibc2.12-x86_64.tar.xz 解压 mv mysql-8.0.21-linux-glibc2.12-x86_64 mysql-8.0 改名 去到bin目录 cd bin mkdir data gr…

Python(PyTorch)硅光电倍增管和量化感知训练亚光子算法验证

&#x1f3af;要点 &#x1f3af;亚光子光神经网络矩阵计算 | &#x1f3af;光学扇入计算向量点积 | &#x1f3af;表征测量确定不同光子数量下计算准确度 | &#x1f3af;训练全连接多层感知器基准测试光神经网络算法数字识别 | &#x1f3af;物理验证光学设备设置 | &#x…

Controller中接收数组参数 post请求中在body中传+post请求中通过表单形式传(x-www-form-urlencoded)

1、场景 需要根据用户id集合批量删除用户数据&#xff0c;前端使用post请求&#xff0c;controller中参数接收数组参数并根据用户id删除用户基本信息 2、分析处理&#xff1a; 2.1、前端请求类型contentType:application/json 请求体中为json字符串&#xff0c;后端新建一个Us…

2024年8月13日~8月19日周报

目录 一、前言 二、完成情况 2.1 遇到的问题及解决 2.1.1 盐数据网络情况与损失函数不下降 2.1.2 其他问题 2.2 损失函数与介绍部分讨论 三、下周计划 一、前言 上周主要完成&#xff1a; ①对比实验执行&#xff1a;InversionNet、DD-Net70②消融实验执行&#xff1a;…

如何选择较为安全的第三方依赖版本?

如何选择较为安全的第三方依赖版本&#xff1f; 本文概览1.1 前言1.1.1 学会看第三方开源库的版本发布说明1.1.2 尽可能使用 starer 匹配的第三方开源库1.1.3 参考Maven 中心仓库的安全警告信息 本文概览 本篇博文分享如何选择较为安全的第三方依赖版本的方法。 1.1 前言 众…

【 亿邦动力网-注册安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞 …

python语言day7 函数式编程 面向对象编程

Java 函数式编程_java函数式编程-CSDN博客 25.Java函数式编程-CSDN博客 函数式编程&#xff1a; 通过调用函数send_email()&#xff0c;完成业务需求。将具体的业务需求封装成一个函数这样的一种解决问题的思想称它为函数式编程。 在java中本来没有函数的概念&#xff0c;因为…

【开源分享】CommLite 跨平台文本UI串口调试助手

文章目录 1. 简介2. 编译3. 使用4. 借鉴&思考参考 1. 简介 CommLite是一款基于CSerialPort的文本UI串口调试助手。 gitee仓库 2. 编译 编译非常简单&#xff0c;按照文档操作即可&#xff1a; $ git clone --depth1 https://github.com/itas109/CommLite.git $ cd Comm…

网易云音乐故障 2 小时,这次到底谁背锅?(今天记得领补偿)

大家好&#xff0c;我是程序员鱼皮&#xff0c;8 月 19 日下午&#xff0c;网易云音乐突发严重故障&#xff0c;并登顶微博热搜&#xff0c;跟黑神话悟空抢了热度。 根据用户的反馈&#xff0c;故障的具体表现为&#xff1a;用户无法登录、歌单加载失败、播放信息获取失败、无法…

聊聊适配器模式

目录 适配器模式概念 主要实现方式 主要组成 UML用例图 代码示例 生活场景 应用场景 适配器模式概念 适配器模式属于结构型设计模式&#xff0c;它的主要目的是将一个类的接口转换成客户端所期望的另一种接口形式&#xff0c;使得原本接口不兼容的类可以一起工作。 主…

暑假算法刷题日记 Day 10

目录 重点整理 054、 拼数 题目描述 输入格式 输出格式 输入输出样例 核心思路 代码 055、 求第k小的数 题目描述 输入格式 输出格式 输入输出样例 核心思路 代码 总结 这几天我们主要刷了洛谷上排序算法对应的一些题目&#xff0c;相对来说比较简单 一共是13道…