理解Java类加载的步骤

2023-11-13

前言

与在编译时需要进行“连接”工作的语言不同,在Java语言里,类的加载、连接、初始化过程都是在程序运行期间完成的,这种策略虽然牺牲了一小部分性能,但是大大增加了Java的灵活性,Java里天生可以动态拓展的语言特性就是依赖运行期动态加载和动态连接这个特点来实现的。一些热修复框架(如Tinker)、插件化框架也是运用了Java这种灵活的类加载机制来完成设计。

1.类加载的时机

1.一个类从加载到JVM中开始,到卸载出内存为止,一共会经历7个过程,分别是:加载、验证、准备、解析、初始化、使用、卸载。
其中,从加载到初始化会经历三个部分:加载、连接、初始化。
类的生命周期
2.加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,但也只是按顺序开始,什么时候完成是不确定的,因此,这几个阶段很多时候实在交叉混合地运行。而解析阶段开始的时机则不是确定的,有时候是在初始化之后开始,有时候是在初始化之前。
3.加载阶段什么时候开始JVM规范并没有强制规定,这可以有具体的JVM决定,但是什么时候初始化则是有要求的。
JVM规范规定了有且只有5种时机需要立即初始化:
(1)遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成着4条字节码指令最常见java代码场景是:1.使用new关键字实例化对象时候。2.读取或设置一个类的静态字段的时候(被finnal修饰、已在编译期把结果放入常量池的静态字段除外)。3.调用一个对象的静态方法的时候。
(2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
(3)当初始化一个类的时候,如果其父类没有被初始化,则需要先初始化父类。
(4)当虚拟机启动时,需要制定一个类作为主类(包含main方法那个类),则要先初始化这个主类。
(5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、Invoke_static的方法句柄,并且这个句柄对应的类没有进行过初始化,则需要先触发其初始化。

2.类加载步骤

整体过程如下图所示:
类加载的过程

2.1 加载

在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下三件事情:
  (1). 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,比如:压缩包(Jar等)、网络、动态生成、数据库等);
  (2). 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;(即将第一步的二进制字节流,转化为虚拟机所需的格式存储在方法区中)
  (3). 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

2.2 验证

属于连接过程的第一阶段,目的是为了确保Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

2.2.1 验证文件格式

首先是段验证字节流是否符合Class文件格式的规范,并且能被当前版本虚拟机处理,比如:验证Class文件是否以魔数开通;验证主次版本号是否在当前虚拟机处理的范围之内;验证常量池中是否有不被支持的常量类型等。

2.2.2 元数据验证

然后是验证字节码的描述信息是否符合Java语言规范的要求,比如:是否有父类;是否继承了不被允许继承的类;如果不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法。

2.2.3 字节码验证

再然后主要是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在上一步对元数据信息中的数据类型进行校验后,这个一步会对类中的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。比如:保证跳转指令不会跳转到方法体以外的字节码指令上;保证方法体中的数据类型转换是有效的。(JDK1.6为了优化检查的性能,在给方法体的Code属性表中增加了一项名为“StackMapStack”的属性,用来描述方法体中所有的基本块(按照控制流拆分成的代码块)开始时本地变量表盒操作栈应有的状态,将字节码验证的类型推导转变成了类型检查,从而节省一些时间)。

2.2.4 符号引用验证

最后校验JVM将符号引用转换成直接引用的时候是否存在问题。(注意符号引用转化成直接引用发生在“解析”阶段)
符号引用的验证可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要检验下列内容:符号引用中通过字符串描述的全限定名是否能够找到对应的类;在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段;符号引用中的类、字段、方法的访问性(被访问修饰修饰)是否可以被当前类访问。

2.3 准备

准备阶段是正式为类变量分配内存并设置初始值的阶段,这些变量所使用的内存都将在方法区分配。不过这个需要注意的是分配内存的对象仅仅限于类变量(被static修饰的变量),不包括实例变脸,实例变量是在new的时候才会在堆中分配内存。
并且,这里所谓的初始值指的是JVM为类变量指定的默认值并不是我们手动分配的值。如:
static int a = 666;
在准备阶段a的值会被指定为int的默认值0,热不是666。因为这时候尚未执行任何的Java方法,而把a赋值为666的操作的指令putstatic是在程序被编译后,存放于类的构造器<clinit>()方法之中的,所以只有在类初始化阶段才会执行。
而如果是被final修饰的常量,则在准备阶段就会被赋值,如:
static final int a = 666
在这种情况下,a的值在准备阶段就会被赋值为666。

2.4 解析

解析阶段是虚拟机将常量池内的符号引用替换成直接引用的过程,符号引用在Class文件中是以CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等类型常量出现,它与直接引用的关联如下:
符号引用:符号引用是以一组符号类描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义地定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标并不一定已经加载到内存中。虽然各种虚拟机实现的内存布局不同,但他们接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在了Java迅即规范的Class文件格式中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用的实现是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机中翻译出来的直接引用一般是不一样的。如果有了直接引用,那么引用的目标必然已经在内存中存在了。
需要注意的是,在解析过一个符号引用过后,虚拟机会进行一定的缓存操作,即如果解析成功,将记录对象的直接引用,避免重复解析。但遇到invokedynamic指令时,则不会有这条规则,因为invokedynamic指令的作用就是用于动态语言支持,动态的含义就是等程序执行到这条指令时才进行解析工作。相对来说,其余的指令都是非动态的,即在完成加载阶段的时候就进行解析。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

2.4.1 类或接口的解析

假设:

class D{
    C c = new C();
    //C[] c = new C[];
}

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用时,那虚拟机完成整个解析过程需要以下3个步骤:
1.如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载的过程中,由于元数据验证、字节码验证的需要,有可能触发其他相关的类加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析就以失败告终。
2.如果加载的类C是数组类型,并且数组的元素类型为对象,将会按照1的规则加载数组的元素类型,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
3.如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或接口了,但在解析完成之前还要进行符号引用的验证,确认D是否具备对C的访问权限,如果不具备访问权限,将抛出IllegalAcessError异常。

2.4.2 字段解析

在解析字段之前,首先要对字段所属的类或接口的符号引用进行解析,如果在此过程中遇到任何异常,到会导致字段符号引用解析失败。如果解析成功,则会按照以下步骤解析字段(将这个字段所属的类或接口用C来表示):
1.如果C本身就包含了“简单的名称”和“字段描述符都与目标相匹配的字段”,则返回这个字段的直接引用,查找结束。
2.否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索个个接口和它的父接口,如果接口中包含了“简单的名称”和“字段描述符都与目标相匹配的字段”,则返回这个目标字段的直接引用,查找结束。
3.否则,如果类不是Object类的话,将会按照继承关系从下往上递归搜索其父类,如果父类包含了“简单的名称”和“字段描述符都与目标相匹配的字段”,则返回这个目标字段的直接引用,查找结束。
4.否则,查找失败,抛出java.lang.NoSuchFileError

如果最后成功返回了直接引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将会抛出java.lang.IllegalAccessError异常。

2.4.3 类方法解析

在解析类方法之前,同样也需要解析出方法所属的类或者接口的符号引用,如果解析成功,将会按照如下步骤来解析:
1.由于类方法和接口方法的符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中的索引C是个接口,那就直接抛出java.lang.IncompatibleClassChangerError。
2.如果通过了第一步,在类C中查找是否有简单“简单名称”和“描述符都与目标相匹配的方法”,如果有,则返回这个方法的直接引用,查找结束。
3.否则,将会按照继承关系从下往上递归搜索其父类,如果父类包含了“简单的名称”和“描述符都与目标相匹配的方法”,则返回这个方法的直接引用,查找结束。
4.否则,将会按照继承关系从下往上递归搜索个个接口和它的父接口,如果接口中包含了“简单的名称”和“描述符都与目标相匹配的方法”,说明C是一个抽象类,查找结束。抛出java.lang.AbstractMethodError异常。
5.否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。
最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError。

2.4.4 接口方法的解析

在解析接口方法的时候也是需要先解析出接口方法表中class_index项中索引的方法所属类或接口的符号引用,如果解析成功,依然采用C表示这个接口,虚拟机将会按照如下步骤进行后续的接口方法搜索:
1.如果接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncampatibleClassChangeError异常。
2.否则,否则在接口C中查找是否有”简单方法名“和”描述符都与目标方法相匹配的方法“,如果有,则返回这个方法的直接引用,查找结束。
3.在接口C的父接口中递归查找,直到java.lang.Object类为止,看是否有“简单方法名”和“描述符都与目标相匹配的方法”,如果有,则返回方法的引用,查找结束。
4.否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。

2.5 初始化

初始化是类加载的最后一步,在这个阶段JVM才就开始真正执行Java的字节码了。
初始化阶段就是执行类加载器<clinit>()方法的过程,<clinit>()方法主要的任务是为静态变量赋实际的值,并且执行静态代码块。它具有以下特性:
1.<clinit>()方法与实例的构造函数<init>()初始化的时机是不同的,类构造器<clinit>()是会比 实例的构造函数<init>()先执行的,并且类构造器不需要手动调用父类的构造器,因为JVM会保证父类的类构造器先于子类执行。
2.由于父类的类构造器先于子类执行,所以父类的静态代码块也会先于子类执行。
3.<clinit>()对于类(包括抽象类)或者接口来说并不是必须的,如果类或者接口中没有静态代码块或者静态变量赋值的操作,JVM也就不会为这个类生成类构造器。
4.接口中不能有静态语句块,但是也会有静态变量赋值的操作,所以也会生成<clinit>()方法,但是不必先执行父接口的<clinit>()方法,只有当父接口的方法的变量使用时,才会调用,有点“按需执行”的意思。
5.JVM会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待。如果在其中执行长时间的耗时操作,会造成多个进程阻塞。但需要注意,在同一个类加载器下,一个类型只会被初始化一次。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

理解Java类加载的步骤 的相关文章

随机推荐

  • 开源License的类型

    如今 Stallman率先推出的GPL已经进入到第三个版本 GNU GPLv3 且这只是几十种开源License类型中的一种 开源组织OSI 是一个在1998年成立的 为了推广开源程序和规范术语使用的组织 它已经批准了80多种开源许可证 这
  • JS数组方法&&es5数组新增方法

    1 unshift 给数组的开头添加一个或多个元素 数组名 unshift 一个值或多个值 返回添加以后的新数组的长度 2 push 给数组的末尾添加一个或多个元素 数组 push 一个值或多个值 返回新数组的长度 3 shift 给数组的
  • C++的Json库的简单实现

    我的Json库实现 Json 实现Json 我的源码 点这里 https github com jo qzy MyJson 和效果图 Json库中的类实现 JSON Value类 JSON Reader JSON Writer FastWr
  • 第十二章 Ambari二次开发之集成Alluxio

    1 Alluxio高可用部署 生产环境 使用具有高可用性的模式来运行Alluxio masters 1 1 Alluxio架构 Alluxio可以被分为三个部分 masters workers以及clients 一个典型的设置由一个主服务器
  • MySQL 过滤重复数据

    方法1 加关键字 DISTINCT 在mysql中 可以利用 SELECT 语句和 DISTINCT 关键字来进行去重查询 过滤掉重复的数据 语法 SELECT DISTINCT 字段名 FROM 数据表名 DISTINCT 关键字的语法格
  • HTML+CSS+JavaScript写计算器

    思维导图 代码 HTML div div div div div div
  • TestNg多线程—— 并行执行测试

    多线程并行执行测试 可以通过参数设置来实现不同级别的多线程配置测试 1 test级别的多线程测试 每个
  • 第二章:25+ Python 数据操作教程(第十八节如何使用 Matplotlib 库在 python 中执行绘图和数据可视化)持续更新中

    本教程概述了如何使用 Matplotlib 库在 python 中执行绘图和数据可视化 这篇文章的目的是让您熟悉该库的基础知识和高级绘图功能 它包含几个示例 将为您提供使用 Python 生成绘图的实践经验 目录 什么是 Matplotli
  • phpshe v1.7漏洞复现(Sql injection+XXE)

    前几天研究了一下xss的绕过 这两天准备深入研究下sql注入的审计 首先自动审计一波 看到疑似的一个变量覆盖点 点进去看原来是 register globals的隐患消除 简单来说如果这个配置设置为On的话 从客户端传输过来的任意参数值会被
  • jboss 热部署

    文章目录 JBoss EAP 6 4 0 GA AS 7 5 0 Final redhat 21 JBoss EAP 6 4 0 GA AS 7 5 0 Final redhat 21
  • 一文学会动态规划

    系列文章目录 注意 在学习理论之前 希望读者能看如下几个例子 有助于理解 算法导论 学习 十七 动态规划之钢条切割 C语言 算法导论 学习 十八 动态规划之矩阵链乘 C语言 算法导论 学习 十九 动态规划之最长公共子序列 C语言 文章目录
  • 浅谈数据挖掘——频繁模式、序列挖掘与搜索优化算法

    本系列将从下面几方面谈谈最近的一点点收获 令声明 本文主要是对我找到的一个莫名其妙国外英文pdf文件的学习与解读 因为我也没有找到他的出处 也没作者也没学校 所以我仅以此段文字向这个未知的作者致敬 本文主要处于科普类的理解 列出的主要算法并
  • SpringBoot学习之单点登录

    SpringBoot学习之单点登录 单点登录 登录 注销 部署 实现 主要功能 重要步骤 sso client拦截未登录请求 sso server拦截未登录请求 sso server验证用户登录信息 sso server创建授权令牌 sso
  • 直播运营岗

    一 理论知识 直播基础知识 直播团队构成 直播工作流程 中控工作流程 中控话术 二 界面操作 电商罗盘 巨量百应 直播伴侣 发单打单 产品链接 三 进阶学习 千川投放 直播复盘 四 面试规划 简历制作 职业生涯规划 1 直播基础知识 1 1
  • 银河麒麟桌面操作系统安装 postgreSQL13(源码安装)

    1 首先下载源码postgres wget http ftp postgresql org pub source v13 1 postgresql 13 1 tar bz2 2 解压 tar xjvf postgresql 13 1 tar
  • visual studio 2017 installer 安装包制作过程出现的问题---无法注册模块 HRESULT -2147024769 请与您的技术支持人员联系...

    使用visual studio 2017 installer制作打包程序时如果用到了外部控件需要按以下方式操作 1 将应用程序及应用程序所用到的所有DLL拷贝到打包目录 加入打包程序之中 2 将应用程序的XXX dll或XXX ocx的Re
  • STM32 C++编程系列一:STM32 C++编程介绍

    一 STM32及其他单片机开发现状 在目前绝大部分的单片机开发当中 C语言占据着主流的地位 但由于C语言本身是一种面向过程的语言 因此在当前利用面向对象思想构建可复用代码为主流的今天显得比较麻烦 很多人写单片机程序时都会遇到一个问题 明明写
  • 大数据采集概述

    文章目录 大数据采集概述 1 互联网大数据与采集 1 1互联网大数据来源 1 社交媒体 2 社交网络 3 百科知识库 4 新闻网站 5 评论信息 6 位置型信息 1 2 互联网大数据的特征 1 大数据类型和语义更加丰富 2 数据的规范化程度
  • 内存溢出问题解决思路

    内存溢出问题解决 一 常规解决思路 首先 在JVM参数配置时需要配置内存溢出后dump出内存的快照来 配置如下 XX HeapDumpOnOutOfMemoryError XX HeapDumpPath 内存快照 hprof输出路径 然后
  • 理解Java类加载的步骤

    前言 与在编译时需要进行 连接 工作的语言不同 在Java语言里 类的加载 连接 初始化过程都是在程序运行期间完成的 这种策略虽然牺牲了一小部分性能 但是大大增加了Java的灵活性 Java里天生可以动态拓展的语言特性就是依赖运行期动态加载