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

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

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

相关文章

C++从入门到精通——入门知识

1. C关键字(C98) C总计63个关键字&#xff0c;C语言32个关键字 2. 命名空间 在C/C中&#xff0c;变量、函数和后面要学到的类都是大量存在的&#xff0c;这些变量、函数和类的名称都将存在于全局作用域中&#xff0c;可能会导致很多冲突。使用命名空间的目的就是对标识符的名…

ST表---算法

相当于二分的思想&#xff0c;一直比较最值 ST的创建 现在创建成功&#xff0c;是应该如何查询的问题 ST表的查询 虽然这两区间有重叠&#xff0c;但是可以一个往前数&#xff0c;一个往后数&#xff0c;互不影响 时间复杂度 创建st表的复杂度为n*logn 使用时的复杂度为O(…

【机器学习】K-近邻算法(KNN)介绍、应用及文本分类实现

一、引言 1.1 K-近邻算法&#xff08;KNN&#xff09;的基本概念 K-近邻算法&#xff08;K-Nearest Neighbors&#xff0c;简称KNN&#xff09;是一种基于实例的学习算法&#xff0c;它利用训练数据集中与待分类样本最相似的K个样本的类别来判断待分类样本所属的类别。KNN算法…

Golang 哈希表底层实现原理

1、本文讨论Golang的哈希表 Golang哈希表的实现&#xff0c;底层数据结构是数组单链表&#xff0c;链表节点由8个key、value和键的高八位组成的。为了方便理解&#xff0c;先简单看一个图快速理解。 我们来看一下Golang哈希表的结构体定义 简单介绍一下结构体中几个关键的…

.NET CORE 分布式事务(四) CAP实现最终一致性

目录 引言&#xff1a; 1.0 最终一致性介绍 2.0 CAP 2.0 架构预览 3.0 .NET CORE 结合CAP实现最终一致性分布式事务 3.1 准备工作(数据库&#xff0c;本文使用的是MySql) 3.1.1 数据模型 3.1.2 DbContext 3.1.3 数据库最终生成 3.2 Nuget引入 3.3 appsettings.json …

【漏洞复现】极简云 download.php 接口处存在任意文件读取漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

OpenHarmony实战:小型系统平台驱动移植

在这一步&#xff0c;我们会在源码目录//device/vendor_name/soc_name/drivers目录下创建平台驱动。 建议的目录结构&#xff1a; device ├── vendor_name │ ├── drivers │ │ │ ├── common │ │ │ ├── Kconfig # 厂商驱动内核菜单入口 │ …

武汉星起航电子商务公司领航跨境电商新纪元,助力品牌走向全球

在全球经济一体化的时代背景下&#xff0c;跨境电商正成为推动国际贸易增长的重要力量。武汉星起航电子商务有限公司&#xff0c;作为一家专注于提供一站式解决方案的跨境电商服务商&#xff0c;凭借其丰富的实战经验和专业团队&#xff0c;在行业中取得了令人瞩目的成绩。 自…

前端学习<四>JavaScript基础——02-JavaScript入门:hello world

开始写第一行 JavaScript&#xff1a;hello world JS 代码的书写位置在哪里呢&#xff1f;这个问题&#xff0c;也可以理解成&#xff1a;引入 JS 代码&#xff0c;有哪几种方式&#xff1f;有三种方式&#xff1a;&#xff08;和 CSS 的引入方式类似&#xff09; 行内式&…

前端(动态雪景背景+动态蝴蝶)

1.CSS样式 <style>html, body, a, div, span, table, tr, td, strong, ul, ol, li, h1, h2, h3, p, input {font-weight: inherit;font-size: inherit;list-style: none;border-spacing: 0;border: 0;border-collapse: collapse;text-decoration: none;padding: 0;margi…

014——超声波模块驱动开发Plus(基于I.MX6uLL、SR04和poll机制)

目录 一、基础知识 二、分析为什么打印会影响中断 三、驱动程序 四、应用程序 五、验证及其它 一、基础知识 013——超声波模块驱动开发&#xff08;基于I.MX6uLL与SR04&#xff09;-CSDN博客 二、分析为什么打印会影响中断 asmlinkage __visible int printk(const ch…

戴尔电脑Dell SupportAssist占用内存高,卸载Dell SupportAssist

咨询戴尔客服了解到&#xff0c;SupportAssist是机器出厂自带的一款应用&#xff0c;主要的功能是可以检查驱动更新以及做一些硬件方面的健康检测&#xff0c;有时候后台运行可能会导致进程占用内存比较大&#xff0c;导致访问被的应用崩溃。 咨询卸载不影响之后&#xff0c;然…

【Python系列】 yaml中写入数据

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

使用pytorch构建一个初级的无监督的GAN网络模型

在这个系列中将系统的构建GAN及其相关的一些变种模型&#xff0c;来了解GAN的基本原理。本片为此系列的第一篇&#xff0c;实现起来很简单&#xff0c;所以不要期待有很好的效果出来。 第一篇我们搭建一个无监督的可以生成数字 (0-9) 手写图像的 GAN&#xff0c;使用MINIST数据…

BugKu:Simple SSTI

1.进入此题 2.查看源代码 可以知道要传入一个名为flag的参数&#xff0c;又说我们经常设置一个secret_key 3.flask模版注入 /?flag{{config.SECRET_KEY}} 4.学有所思 4.1 什么是flask&#xff1f; flask是用python编写的一个轻量web开发框架 4.2 SSTI成因&#xff08;SST…

RIP协议(路由信息协议)

一、RIP协议概述 RIP协议&#xff08;Routing Information Protocol&#xff0c;路由信息协议&#xff09;是一种基于距离矢量的内部网关协议&#xff0c;即根据跳数来度量路由开销&#xff0c;进行路由选择。 相比于其它路由协议&#xff08;如OSPF、ISIS等&#xff09;&#…

Spring Cloud微服务入门(二)

微服务的技术栈 服务治理&#xff1a; 服务注册、发现、调用。 负载均衡&#xff1a; 高可用、集群部署。 容错&#xff1a; 避免雪崩、削峰、服务降级。 消息总线&#xff1a; 消息队列、异步通信&#xff0c;数据一致性。 网关&#xff1a; 校验路径、请求转发、服务集成…

使用Python获取红某书笔记详情并批量无水印下载

根据红某手最新版 请求接口必须要携带x-s x-s-c x-t,而调用官方接口又必须携带cookie,缺一不可,获取笔记详情可以通过爬取网页的形式获取&#xff0c;虽然也是无水印&#xff0c;但是一些详情信息只能获取大概&#xff0c;并不是详细的数值&#xff0c;因此既不想自己破解x-s x…

UE5 C++ LevelSequence

前言 最近在用UE C做一些功能&#xff0c;用到了Level Sequence功能&#xff0c;但是看了下UE官方论坛包括一些文章基本没有关于C 处理Level Sequence 这块内容&#xff0c;有的也是一些修改或者源码原理的一些内容分析&#xff0c;接下来我就把我新建Sequence包括一些库的调用…

# 达梦数据库知识点

达梦数据库知识点 测试数据 -- SYSDBA.TABLE_CLASS_TEST definitionCREATE TABLE SYSDBA.TABLE_CLASS_TEST (ID VARCHAR(100) NOT NULL,NAME VARCHAR(100) NULL,CODE VARCHAR(100) NULL,TITLE VARCHAR(100) NULL,CREATETIME TIMESTAMP NULL,COLUMN1 VARCHAR(100) NULL,COLUMN…