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

news2024/9/19 9:25:22

深入理解类加载机制

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() {
            @Override
            public 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.coloradmin.cn/o/2056467.html

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

相关文章

便利店(超市)管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图详细视频演示技术栈系统测试为什么选择我官方认证玩家&#xff0c;服务很多代码文档&#xff0c;百分百好评&#xff0c;战绩可查&#xff01;&#xff01;入职于互联网大厂&#xff0c;可以交流&#xff0c;共同进步。有保障的售后 代码参考数据库参…

层次聚类算法原理及Python实现

层次聚类算法&#xff08;Hierarchical Clustering Method&#xff09;是一种基于簇间相似度在不同层次上分析数据&#xff0c;从而形成树形聚类结构的算法。它主要分为两种形式&#xff1a;凝聚层次聚类&#xff08;自下而上&#xff09;和分裂层次聚类&#xff08;自上而下&a…

ansible --------拓展

编辑 hosts 配置文件 [rootmo ~]# vim /etc/ansible/hosts # 创建目录 [rootmo ~]# mkdir /etc/ansible/playbook # 编辑配置文件 [rootmo ~]# vim /etc/ansible/playbook/nginx.yml # 执行测试 [rootmo ~]# ansible-playbook /etc/ansible/playbook/nginx.yml roles 修…

C# asnyc和await

asnyc和await是什么? 异步编程是一种编程范式&#xff0c;C#中的异步编程可以通过Thread,TheadPool,Task,async/await等来实现。 await能等待什么? 不能等待同步代码&#xff0c;只能等待Task或异步方法&#xff0c;且异步方法必须有返回值&#xff0c; async/await的出现…

遇到BUG怎么分析,全方位带你分析

软件测试的目的是尽可能早地找出软件产品中潜藏的缺陷&#xff0c;并确保其得以修复。所以缺陷的分析就会变得很关键&#xff0c;那么如何来分析缺陷呢&#xff1f; 根据缺陷的定义描述准则&#xff1a; 所有不满足需求或超出需求的都是缺陷。缺陷的判定主要的依赖点在于产品…

配置oss cdn加速静态资源访问 阿里云

效果对比 配置cdn下载速度对比 步骤 1: 登录阿里云控制台控制台主页&#xff0c;找到并点击“对象存储 OSS” 创建存储空间&#xff08;Bucket&#xff09; 设置权限 步骤 2: 获取外网访问地址 步骤 3 在 CDN 中使用该地址 复制该外网访问地址 打开全站加速 DCDN/域名管理 添…

【LeetCode热题100】双指针

class Solution { public:void moveZeroes(vector<int>& nums) {int dst -1,cur 0;while(cur<nums.size()){if(nums[cur] 0){cur;}else{swap(nums[dst1],nums[cur]);cur;dst;}}} }; 题目分析&#xff1a;对于数组分块/数组划分的问题&#xff0c;我们可以使用双…

Jmeter请求发送加密参数详解

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 最近在做http加密接口&#xff0c;请求头的uid参数及body的请求json参数都经过加密再发送请求&#xff0c;加密方式为&#xff1a;ase256。所以&#xff0c;jmeter…

社区志愿者服务系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图详细视频演示技术栈系统测试为什么选择我官方认证玩家&#xff0c;服务很多代码文档&#xff0c;百分百好评&#xff0c;战绩可查&#xff01;&#xff01;入职于互联网大厂&#xff0c;可以交流&#xff0c;共同进步。有保障的售后 代码参考数据库参…

【AI落地应用实战】Amazon SageMaker JumpStart 体验零一万物的 Yi 1.5 模型

目录 一、前言1.1、Amazon SageMaker JumpStart1.2、Yi-1.5 9B Chat 模型 二、Yi 1.5实践流程2.1、准备SageMaker Studio环境2.2、部署Yi-1.5-9B模型2.3、运行推理Yi-1.5-9B模型 三、体验总结 中国大模型公司零一万物发布开源大模型Yi-1.5&#xff0c;包含多个版本。同时发布多…

YOLOv8轻量化改进之slimneck

目录 一、原理 二、代码 三、修改到YOLOv8中 四、yaml文件修改 一、原理 论文地址:2206.02424 (arxiv.org) 主要模块的网络结构 二、代码 slimneck的代码如下,slimneck主要由GSConv和VoVGSCSPC两部分组成。 class GSConv(nn.Module):# GSConv https://github.com/Alan…

分布式高可用架构设计

一、限流 1、单机限流 如图&#xff0c;应用C的资源c/x被上游的应用A和应用C并发访问&#xff0c;应用C的系统能力支持c/x资源最高5000/qps的访问量&#xff1b;为了不让高并发流量或尖峰流量压垮应用C&#xff0c;可以针对应用C的资源c/x做限流&#xff1b;比如设置限流4500…

为什么会有浮毛猫毛?这些不容忽视的危害宠物空气净化器能解决!

很多人看到朋友家可爱的猫猫狗狗&#xff0c;自己也心痒痒想要养一只。但一想到宠物可能会带来的掉毛、体臭等问题&#xff0c;又犹豫不决。诚然&#xff0c;养宠物确实会对室内空气造成一定影响&#xff0c;但养宠人的共识是&#xff1a;那份与宠物共度的快乐时光&#xff0c;…

mysql索引探索

《令人困惑的mysql索引》在这篇文章中&#xff0c;遗留了几个困惑。为了解决这些困惑&#xff0c;我又进行了一些测试。 本文中索引的建立&#xff0c;并不一定科学&#xff0c;查询的SQL也不一定科学&#xff0c;只是为了理解mysql索引建的一些测试数据。 CREATE TABLE cms_…

加密与安全_双向RSA+AES加密及Code实现

文章目录 AES / RSA流程Code生成AES密钥 和 生成RSA密钥对AES工具类&#xff0c;提供AES加密和解密功能RSA加密工具类测试类 总结 AES / RSA AES&#xff1a;一种对称加密算法&#xff0c;意味着加密和解密使用相同的密钥。速度快&#xff0c;适合加密大量数据。RSA&#xff1…

Tina-Linux Bootloaer简述

Tina-Linux Bootloaer简述 目录介绍 ubuntuubuntu1804:~/tina-v2.0-sdk/lichee/brandy-2.0$ tree -L 1 . ├── build.sh ├── opensbi ├── spl //boot0 ├── spl-pub //boot0 ├── tools └── u-boot-2018 /ubootTina-Linux 启动流程简述

ant design pro v6 如何做好角色管理

先上图&#xff1a; 整个角色管理是如何做的吗&#xff1f; 首先你要处理后端&#xff0c;要先把角色存到用户那。 这是用户管理部分的内容&#xff1a; 可以看到一个用户是有多个角色的。 看到没有&#xff0c;存的是数组 数组的是一个 role 对象 role 对象是这样&#xf…

reactive 和 ref 的区别和联系

在 Vue3 的组合式 API &#xff08;Composition API&#xff09;中&#xff0c;ref 和 reactive 是用于创建响应式数据的两个核心函数。尽管二者都用于实现响应式数据&#xff0c;但在使用方式和适用场景上存在一些区别。 1. 基本概念 1.1 ref 用途&#xff1a;用于定义 基本…

深入探讨视频美颜SDK:直播美颜工具的核心技术与实现

本篇文章&#xff0c;笔者将深入探讨视频美颜SDK的核心技术及其在直播美颜工具中的实现。 一、视频美颜SDK的核心技术 视频美颜SDK通过一系列复杂的算法和技术&#xff0c;实时处理视频流并应用各种美颜效果。这些核心技术主要包括以下几个方面&#xff1a; 1.人脸识别与追踪…

【Hot100】LeetCode—24. 两两交换链表中的节点

目录 1- 思路四指针 2- 实现⭐24. 两两交换链表中的节点——题解思路 3- ACM 实现 原题连接&#xff1a;24. 两两交换链表中的节点 1- 思路 四指针 定义 dummyHead&#xff1a;便于处理头结点① cur 指针&#xff0c;记录两个交换节点的前 前一个结点② 第一个指针 first③ 第…