JVM专题——类文件加载

本文部分内容节选自Java Guide和《深入理解Java虚拟机》, Java Guide地址: https://javaguide.cn/java/jvm/class-loading-process.html

🚀 基础(上) → 🚀 基础(中) → 🚀基础(下) → 🤩集合(上) → 🤩集合(下) → 🤗JVM专题1 → 🤗JVM专题2

类加载过程

一个类从被加载到 JVM 内存开始, 到卸载出内存位置, 它的整个生命周期会经历 加载 , 验证 , 准备 , 解析 , 初始化 , 使用卸载 七个阶段, 其中, 验证 , 准备 , 解析 这三个阶段统称为 连接

加载, 验证, 准备, 初始化, 卸载这五个阶段的顺序是确定的, 类型的加载过程必须按照这种顺序按部就班地开始, 而解析顺序不一定

加载

类加载过程的第一步, 主要完成下面三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的 java.lang.Class 对象, 作为方法区这个类的各种数据的访问入口

加载这一步主要是通过 类加载器 完成的, 具体是由哪个类加载器加载由 双亲委派模型 决定

每个 Java 类都有一个引用指向加载它的 ClassLoader , 不过, 数组类不是由 ClassLoader 加载的, 而是 JVM 在需要的时候自动创建的, 数组类通过 getClassLoader() 方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的

一个非数组类的加载阶段 (加载阶段获取类的二进制字节流的动作) 是可控性最强的阶段, 这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式 (重写一个类加载器的 loadClass() 方法)

加载阶段与连接阶段的部分动作 (如一部分字节码文件格式验证动作) 是交叉进行的, 加载阶段尚未完成, 连接阶段可能已经开始, 但这些夹在加载阶段之中进行的动作, 仍然属于连接阶段的一部分, 这两个阶段的开始时间仍然保持着固定的先后顺序

验证

验证是连接阶段的第一步, 这一阶段的目的是为了确保 Class 文件的字节流中的信息符合 Java 虚拟机规范的全部约束要求, 保证这些信息被当作代码运行后不会危害虚拟机的安全

Java 虚拟机如果不检查输入的字节流, 对其完全信任的话, 很可能会因为载入了有错误或者有恶意企图的字节码流导致整个系统受攻击甚至崩溃, 所以验证字节码是 Java 虚拟机保护自身的必要措施

验证阶段大致上会完成下面四个阶段的检验动作: 文件格式验证, 元数据验证, 字节码验证和符号引用验证

文件格式验证

验证点如下:

  1. 是否以魔数 0xCAFEBABE 开头
  2. 主, 次版本号是否在当前 Java 虚拟机的可接受范围之内
  3. 常量池中是否有不被支持的常量类型
  4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
  5. CONSTANT_Utf8_info型的常量中是否有不符合 UTF-8 编码的数据
  6. Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息

主要目的是保证输入的字节流能够正确地解析并存储于方法区内, 格式上符合描述一个 Java 类型信息的要求.

这阶段的验证是基于二进制字节流进行的, 只有通过了这个阶段的验证之后, 这段字节流才被允许进入 Java 虚拟机内存的方法区中进行存储, 所以后面的三个验证阶段全部是基于方法区的存储结构上进行的, 不会再读取, 操作字节流了

元数据验证

验证点如下:

  1. 这个类是否有父类 (除了 java.lang.Object 之外, 所有的类都应当有父类)
  2. 这个类的父类是否继承了不允许被继承的类
  3. 如果这个类不是抽象类, 是否实现了其父类或接口之中要求实现的所有方法
  4. 类中的字段, 方法是否与父类产生矛盾

主要是对字节码描述的信息进行语义分析, 保证不存在与 Java语言规范 定义相悖的元数据信息

字节码验证

验证点如下:

  1. 保证任意时刻操作数栈的数据类型和指令代码序列都能配合工作, 例如不能出现类似于 “在操作栈中放置了一个int类型的数据, 使用时却按照long类型加载入本地变量表中” 这种情况
  2. 保证任何跳转指令都不会跳转到方法体之外的字节码指令上
  3. 保证方法体中的类型转换总是有效的, 例如可以把一个子类对象赋给父类数据类型, 这是安全的, 但是把父类对象赋值给子类数据类型甚至一个毫无继承关系的数据类型, 则是不合法的

符号引用验证

验证点如下:

  1. 符号引用中通过字符串描述的全限定名是否能找到对应的类
  2. 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
  3. 符号引用中类, 字段, 方法的可访问性是否可被当前类访问

符号引用验证的目的是确保解析行为可以正常执行, 如果无法通过符号引用验证, Java 虚拟机会抛出一个 java.lang.IncompatibleClassChangeError 的子类异常, 典型的有: java.lang.IllegalAccessError , java.lang.NoSuchFieldError , java.lang.NoSuchMethodError

准备

准备阶段是正式为类中定义的变量 (即静态变量, 被static修饰的变量) 分配内存并设置类变量初始值的阶段.

需要注意的几点

  1. 这时候进行内存分配的仅包括类变量, 而不包括实例变量, 实例变量会在对象实例化时随着对象一起分配在 Java 堆中
  2. 这里所说的初始值 “通常情况下” 是数据类型的零值(如 0, 0L, null, false 等)
  3. 类变量使用的内存都应当在方法区中分配, 不过需要注意的是, 在 JDK7 以前, HotSpot 使用永久代实现方法区时, 是符合这种逻辑概念的, 但是在 JDK7 之后, HotSpot已经把原本放在永久代的字符串常量池, 静态变量等移动到堆中, 这时候类变量则会随着 Class 对象一起存放在 Java 堆中

解析

解析阶段是 JVM 将常量池中的符号引用直接替换为直接引用的过程

解析动作主要针对类或接口, 字段, 类方法, 接口方法, 方法类型, 方法句柄和调用点限定符这7种符号引用进行

初始化

初始化阶段是执行初始化方法 <clinit>() 方法的过程, 是类加载的最后一步, 这一步 JVM 才开始真正执行类中定义的 Java 程序代码

对于 <clinit>() 方法的调用, 虚拟机会自己确保其在多线程环境中的安全性, 因为 <clinit>() 方法是带锁线程安全, 所以在多线程环境下进行类初始化的话可能会引起多个线程阻塞, 并且这种阻塞很难被发现

对于初始化阶段, 虚拟机严格规范了有且只有6种情况, 必须对类进行初始化

  1. 当遇到 new , getstatic , putstaticinvokestatic 这4条字节码指令时, 比如 new 一个类, 读取一个静态字段, 或调用一个类的静态方法
    • 当 JVM 执行 new 指令时会初始化类, 即当程序创建一个类的实例对象
    • 当 JVM 执行 getstatic 指令时会初始化类, 即程序访问类的静态变量
    • 当 JVM 执行 putstatic 指令时会初始化类, 即程序给类的静态变量赋值
    • 当 JVM 执行 invokestatic 指令时会初始化类, 即程序调用类的静态方法
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forName("...") , newInstance() 等. 如果类没有初始化, 需要触发其初始化
  3. 初始化一个类, 如果其父类没有初始化, 则先触发父类的初始化
  4. 当虚拟机启动时, 用户需要定义一个要执行的主类, 虚拟机会先初始化这个类
  5. MethodHandleVarhandle 可以看作是轻量级的反射调用机制, 而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类
  6. 当一个接口中定义了 JDK8 新加入的默认方法时, 如果有这个接口的实现类发生了初始化, 那该接口要在其之前被初始化

类卸载

卸载类即该类的 Class 对象被GC

卸载类需要满足 3 个要求:

  1. 该类的所有实例对象都已被 GC, 也就是说堆不存在该类的实例对象
  2. 该类没有在其他地方被引用
  3. 该类的类加载器实例已被GC

类加载器

类与类加载器

类加载器的主要作用就是加载 Java 类的字节码 (.class 文件) 到 JVM 中 (在内存中生成一个代表该类的 Class 对象)

对于任意一个类, 都必须由加载它的类加载器和这个类本身一起共同确定其在 JVM 中的唯一性, 每个类加载器都有一个独立的类名称空间

也就是说, 比较两个类是否 “相等”, 只有在这两个类是由同一个类加载器加载的前提下才有意义, 否则, 即使这两个类来源于同一个 Class 文件, 被同一个 JVM 加载, 只要加载它们的类加载器不同, 那么这两个类必定不相等

此处的相等, 包括 Class 对象的 equals() 方法, isAssignableFrom() 方法, isInstance 方法的返回结果, 也包括了使用 instanceof 关键字做对象所属关系判断等各种情况

加载规则

JVM 启动时, 并非会一次性加载所有的类, 而是根据需要动态加载类. 也就是说大部分类都是在具体用到的时候才会去加载, 这样对内存更友好

对于已经加载的类会放在 ClassLoader 中, 在类加载时, 系统会先判断这个类是否被加载过, 已经被加载过的类会直接返回, 否则会尝试加载. 也就是说, 对于一个类加载器来说, 相同二进制名称的类只会被加载一次

类加载器总结

JVM 中内置了3个重要的 ClassLoader

  1. BootstrapClassLoader (启动类加载器): 最顶层加载类, 由C++实现, 通常表示为null, 且没有父级, 主要用来加载 JDK 内部的核心类库以及被 -Xbootclasspath 参数指定的路径下的所有类
  2. ExtensionClassLoader (扩展类加载器): 主要负责加载 %JRE_HOME%/lib/ext 目录下的jar包和类以及被 java.ext.dirs 系统变量所指定的路径下所有的类
  3. AppClassLoader (应用程序类): 负责加载当前应用 classpath 下所有的jar包和类

除了上面三个类加载器, 用户还可以自定义类加载器

除了 BootstrapClassLoader 是 JVM 自身的一部分之外, 其他所有的类加载器都是在 JVM 外部实现的, 并且全部继承自 ClassLoader 抽象类. 这样的好处是用户可以自定义类加载器, 以便让应用程序自己决定如何去获取所需的类

每个 ClassLoader 都可以通过 getParent() 获取其父加载器, 如果获取到的加载器为 null 的话, 说明该类是通过 BootstrapClassLoader 加载到

自定义类加载器

如果需要自定义类加载器, 需要继承 ClassLoader 抽象类

ClassLoader 中有两个关键的方法

  • protected Class loadClass(String name, boolean resolve) : 加载指定二进制名称的类, 实现双亲委派机制
  • protected Class findClass(String name) : 根据类的二进制名称查找类, 默认实现是空方法

如果不想打破双亲委派机制, 就重写 ClassLoader 中的 findClass() 方法. 但是, 如果想打破双亲委派机制就需要重写 loadClass() 方法

双亲委派模型

在这里插入图片描述

如图所示的各种类加载器之间的层次关系被称为类加载器的 “双亲委派模型”. 双亲委派模型要求除了顶层的启动类加载器之外, 其他类加载器都必须有自己的父类加载器. 这里类加载器之间的父子关系不是以继承的关系来实现的, 而是通常使用组合关系复用父加载器的代码

双亲委派模型的执行流程:

  • 在类加载时候, 系统会判断这个类是否被加载过, 已经被加载过的类会直接返回, 否则会尝试加载
  • 类加载器在进行类加载的时候, 他首先不会自己尝试加载这个类, 而是把这个请求委派给父类加载器去完成. 这样的话, 所有的请求都会传送到顶层的启动类加载器 BootstrapClassLoader
  • 只有当父加载器反馈自己无法完成这个加载请求, 子加载器才会尝试自己去加载
  • 如果子加载器也无法加载, 抛出 ClassNotFoundException 异常

双亲委派模型保证了 Java 程序的稳定运行, 也避免类的重复加载, 也保证了 Java 的核心 API 不被篡改

打破双亲委派模型

双亲委派模型并非是一个具有强制性约束的模型, 而是 Java 设计者推荐给开发者的类加载器实现方式

为了打破双亲委派模型, 需要继承 ClassLoader , 如果不想打破双亲委派模型, 就重写 ClassLoader 中的 findClass() 方法, 如果要打破双亲加载机制就需要重写 loadClass() 方法

重写 loadClass()方法之后, 我们就可以改变传统双亲委派模型的执行流程.例如, 子类加载器可以在委派给父类加载器之前, 先自己尝试加载这个类, 或者在父类加载器返回之后, 再尝试从其他地方加载这个类. 具体的规则由我们自己实现,根据项目需求定制化

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

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

相关文章

利用AI结合无极低码(免费版)快速实现接口开发教程,会sql即可,不需要编写编译代码

无极低码无代码写服务+AI实践 本次演示最简单的单表无代码增删改查发布服务功能,更复杂的多表操作,安全验证,多接口调用,自自动生成接口服务,生成二开代码,生成调用接口测试,一键生成管理界面多条件检索、修改、删除、查看、通用公共接口调用、通用无限级字典调用等后续…

【Linux】Ubuntu 文件权限管理

Linux 系统对文件的权限有着严格的控制&#xff0c;用于如果相对某个文件执行某种操作&#xff0c;必须具有对应的权限方可执行成功&#xff0c;这也是Linux有别于Windows的机制&#xff0c;也是基于这个权限机制&#xff0c;Linux可以有效防止病毒自我运行。因为运行的条件是必…

第二十三章 Git

一、Git Git 是一个开源的分布式版本控制系统&#xff0c;用于敏捷高效地处理任何或小或大的项目。 Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。 Git 与常用的版本控制工具 CVS, Subversion 等不同&#xff0c;它采用了分布式版…

前端三剑客 —— CSS ( 坐标问题 、定位问题和图片居中 )

前期内容回顾&#xff1a; 1.常见样式 text-shadow x轴 y轴 阴影的模糊程度 阴影的颜色 box-shadow border-radio 实现圆角 margin 内边距 padding 外边距 background 2.特殊样式 媒体查询&#xff1a;media 自定义字体&#xff1a;font-face { font-family:自定义名称&#…

代码随想录算法训练营第四十四天 |卡码网52. 携带研究材料 、518. 零钱兑换 II、377. 组合总和 Ⅳ

代码随想录算法训练营第四十四天 |卡码网52. 携带研究材料 、518. 零钱兑换 II、377. 组合总和 Ⅳ 卡码网52. 携带研究材料题目解法 518. 零钱兑换 II题目解法 377. 组合总和 Ⅳ题目解法 感悟 卡码网52. 携带研究材料 题目 解法 题解链接 1. #include <iostream> #inc…

生成式人工智能与 LangChain(预览)(上)

原文&#xff1a;Generative AI with LangChain 译者&#xff1a;飞龙 协议&#xff1a;CC BY-NC-SA 4.0 一、生成模型是什么&#xff1f; 人工智能&#xff08;AI&#xff09;取得了重大进展&#xff0c;影响着企业、社会和个人。在过去的十年左右&#xff0c;深度学习已经发…

【接口】HTTP(1)|请求|响应

1、概念 Hyper Text Transfer Protocol&#xff08;超文本传输协议&#xff09;用于从万维网&#xff08;就是www&#xff09;服务器传输超文本到本地浏览器的传送协议。 HTTP协议是基于TCP的应用层协议&#xff0c;它不关心数据传输的细节&#xff0c;主要是用来规定客户端和…

单元测试 mockito(二)

1.返回指定值 2.void返回值指定插桩 3.插桩的两种方式 when(obj.someMethod()).thenXxx():其中obj可以是mock对象 doXxx().wien(obj).someMethod():其中obj可以是mock/spy对象 spy对象在没有插桩时是调用真实方法的,写在when中会导致先执行一次原方法,达不到mock的目的&#x…

RobotFramework测试框架(2)-测试用例

创建测试数据 测试数据语法 这里的测试数据就是指的测试用例。 测试文件组织 测试用例的组织层次结构如下&#xff1a; 在测试用例文件&#xff08; test case file &#xff09;中建立测试用例 一个测试文件自动的建成一个包含了这些测试用例的测试集&#xff08; test s…

vulhub中Apache Solr 远程命令执行漏洞复现(CVE-2019-0193)

Apache Solr 是一个开源的搜索服务器。Solr 使用 Java 语言开发&#xff0c;主要基于 HTTP 和 Apache Lucene 实现。此次漏洞出现在Apache Solr的DataImportHandler&#xff0c;该模块是一个可选但常用的模块&#xff0c;用于从数据库和其他源中提取数据。它具有一个功能&#…

【Android Studio】上位机-安卓系统手机-蓝牙调试助手

【Android Studio】上位机-安卓系统手机-蓝牙调试助手 文章目录 前言AS官网一、手机配置二、移植工程三、配置总结 前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 AS官网 AS官网 一、手机配置 Android Studio 下真机调试 二、移植工程 Anro…

【Linux】第二个小程序--简易shell

请看上面的shell&#xff0c;其本质就是一个字符串&#xff0c;我们知道bash本质上就是一个进程&#xff0c;只不过命令行就是一个输出的字符串&#xff0c; 我们输入的命令“ls -a -l”实际上是我们在输入行输入的字符串&#xff0c;所以&#xff0c;如果我们想要做一个简易的…

通用开发技能系列:SQL基础学习

云原生学习路线导航页&#xff08;持续更新中&#xff09; 本文是 通用开发技能系列 文章&#xff0c;主要对编程通用技能 SQL基础 进行学习 1.数据库简介 1.1.数据库中的一些名称 DataBase&#xff1a;数据库 程序员只负责怎么维护存取数据&#xff0c;不管数据库是什么 DBA…

c#程序报错引用无效解决办法之一:检查引用的文件路径

直接右键然后打开本地 打开这个.csproj文件&#xff0c;直接对着路径看看里面的路径对不对。 一般是很多人一起开发&#xff0c;然后这个文件路径被推送上来的问题

考研经验与科目学习建议

前言 24考研刚刚结束&#xff0c;成功上岸&#xff0c;回想起刚开始的时候的迷茫&#xff0c;加上因为迷茫而被卖书的坑的几百块钱。感慨万千&#xff0c;所以决定写下这篇文章。回想当时&#xff0c;因为笔者零基础&#xff0c;加上作为一名专升本的学生&#xff0c;惶恐因为…

第十四届省赛大学B组(C/C++)子串简写

原题链接&#xff1a;子串简写 程序猿圈子里正在流行一种很新的简写方法&#xff1a; 对于一个字符串&#xff0c;只保留首尾字符&#xff0c;将首尾字符之间的所有字符用这部分的长度代替。 例如 internationalization 简写成 i18n&#xff0c;Kubernetes 简写成 K8s&#…

目标检测——车牌数据集

一、重要性及意义 交通安全与管理&#xff1a;车牌检测和识别技术有助于交通管理部门快速、准确地获取车辆信息&#xff0c;从而更有效地进行交通监控和执法。例如&#xff0c;在违规停车、超速行驶等交通违法行为中&#xff0c;该技术可以帮助交警迅速锁定违规车辆&#xff0…

机器学习模型:决策树笔记

第一章&#xff1a;决策树原理 1-决策树算法概述_哔哩哔哩_bilibili 根节点的选择应该用哪个特征&#xff1f;接下来选什么&#xff1f;如何切分&#xff1f; 决策树判断顺序比较重要。可以使用信息增益、信息增益率、 在划分数据集前后信息发生的变化称为信息增益&#xff0c…

MySQL 主从复制架构搭建及其原理

前言 系统的性能瓶颈一般出现在数据库上&#xff0c;以 mysql 为例&#xff0c;如果存在高并发的写请求&#xff0c;势必会有锁表&#xff0c;锁数据行的情况发生&#xff0c;这时候如果有读请求刚好访问到被锁的数据&#xff0c;那么读请求会阻塞&#xff0c;直到写请求处理完…

【C++ STL迭代器】iterator

文章目录 【 1. 迭代器的属性 】【 2. 不同容器支持的迭代器 】【 3. 迭代器的定义方式 】【 4. 实例 】4.1 定义方式&#xff1a;正向迭代器和反向迭代器4.2 迭代器属性&#xff1a;前向迭代、双向迭代、随机迭代4.2 迭代器的遍历方法4.3 auto关键字 自动指定迭代器定义类型 背…