JVM006_类加载的过程

类加载

类加载时机

类加载的过程

新术语

类加载器 简单的理解为将类转换为二进制流的类或接口。

数组的元素类型 数组去掉所有维度的类型。

数组的组件类型 数组去掉一个维度的类型。

基本块 按照控制流拆分的代码块。


1. 加载

加载是类加载过程的一个阶段。加载阶段主要完成三件事情:

  1. 根据类的全限定名,获取定义此类的二进制字节流。
  2. 将这个二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  3. 在内存中生成一个代表该类的java.lang.Class对象,作为访问方法区中该类各种数据的外部接口。

在上述的1中,没有限定此类的格式,所以可以是一个class文件,可以是一个jar包,也可以是运行时生成等等。我们可以通过重写一个类加载器的findclass()方法或者loadClass方法来自定义字节流的获取方法。

数组与类加载器

数组类本身是由JVM在内存中直接构造的,但是又与类加载器紧密联系,其遵循规则如下:

  1. 如果数组的组件类型是一个引用类型,那么会递归的使用加载过程去加载该组件类型,数组将被标识在加载该组件类型的类加载器的类名称空间上。

  2. 若组件类型是基本类型,JVM会将该数组标记为与BootStrapClassLoader关联

  3. 数组类的可访问性与其组件类型的可访问性一致。

2. 验证

验证是连接的第一步,器目的是确保Class文件的字节流中包含的信息完全符合《JVM规范》中的全部约束条件,保证这些信息在运行时不会威胁到JVM的安全。其大致可分为四个阶段:文件格式验证,元数据验证,字节码验证和符号引用验证。

文件格式验证

这一阶段要验证字节流是否符合Class文件格式的规范,以保证输入的字节流能够正确的解析并存储到方法区内。格式上要符合一个Java类型信息的要求。只有文件格式验证通过后,才能将字节流中的信息存储到方法区中,所有后面的是三个验证,都是基于方法区的存储结构进行的,而不是字节流。

元数据验证

这个阶段要求对字节码的描述信息进行语义分析,也就是保证其描述的信息符合《Java语言规范》的要求。

字节码验证

该验证的目的是通过数据流分析和控制流分析,确定程序语义的合法性、合逻辑性。在元数据验证通过后,该阶段对类的方法体(也就是Class文件中的Code属性)进行校验分析,保证类的方法不会在运行时危害到JVM。

在JDK6后,将尽可能多的校验辅助措施挪到javac编译器中,具体的做法是在Code属性中增加了一个StackMapTable属性,该属性描述了方法体所有的基本块开始时本地变量和操作栈应有的状态。在字节码验证时,只需要检查StackMapTable中记录是否合法即可,而不用根据程序推导这些状态的合法性。

符号引用验证

该阶段发生在JVM将符号引用转为直接引用的时候,其在连接的第三阶段解析阶段才发生。是对类自身以外的各类信息进行匹配性校验,比如说该类是否缺少或禁止访问它依赖的某些额外部类等,若无法通过验证,会排除java.lang.IncompatibleClassChangeError的子类异常(java.lang.NoSuchFieldError等)。

3. 准备

准备阶段是正式为被static修饰的变量(类变量)分配内存并设置初始值的阶段。

特别注意:

  • 此阶段的内存分配,仅仅包括类变量,不包括实例变量。

  • 若类变量同时被final修饰(也就是通常说的常量),那么其赋值不会是基本类型的零值,而是指定的值。

    例如public staitc final int INIT_VALUE=99,那么在准备阶段INIT_VALUE会被赋值为99,而不是0,这是因为被static final同时修饰是,在javac编译时,字段属性表中会有一个ConstantValue属性,在准备阶段,该变量值就会被初始化为ConstantValue属性所指定的初始值。

基本数据的零值

数据类型零值
int0
long0L
short(short)0
char‘\u0000’
byte(byte)0
booleanfalse
float0.0f
double0.0d
referencenull

4. 解析

解析是JVM将符号引用转换为直接引用的过程。

符号引用 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要在使用时能够无歧义的定义到目标即可。符号引用与JVM的内存布局无关。

直接引用 是可以直接指向目标的指针、相对偏移量或者一个能够间接定位到目标的句柄。其与JVM内存布局直接相关。

句柄 是由系统所管理的引用标识,该标识可以被系统重新定位到一个内存地址上。

解析动作主要针对类或结构、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号医用进行,其对应着8种常量类型。

4.1 类或者接口的解析

假设当前的类为D,要将其中未解析过的符号引用N解析为一个类或者接口C的直接引用。大概过程如下:

在这里插入图片描述

  1. 若C不是一个数组,那么JVM会将代表N的全限定类名交个D的类加载器,由其去加载这个类C。若在C的加载过程中出现异常,那么解析过程宣告失败。
  2. 若C是一个数组,并且数组的元素类型是对象,那么会按照上一步的步骤去加载元素类型,再由JVM生成数组对象。
  3. 若1、2都没问题,则检查D对C的访问权限。若没有权限则会抛出java.lang.IllegalAccessError

4.2 字段解析

若要对一个没有经过解析的字段进行解析,首先我们需要看字段表内的class_index项中的CONSTANT_class_info符号引用进行解析(参考类文件结构),也就是对字段对应的类或者接口的引用的解析。

假设字段对应的类或接口为C,那么在解析类或接口成功后,会根据《JVM规范》对字段进行搜索:

  1. 若C本身包含的简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,搜索结束。
  2. 否则,若C实现了接口,那么将会按照继承关系从下向上递归搜索各个接口及父接口,若接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回该字段的直接引用,搜索结束。
  3. 否则,若C不是java.lang.Object的话,就会按照继承关系从下向上递归搜索其父类,若在父类中包含了简单名称和字段描述符与之相匹配的字段,则返回该字段的直接引用,搜索结束。
  4. 否则查找失败,抛出java.lang.NoSuchFieldError异常。
  5. 若查找过程成功返回了引用,则进行访问权限验证,若无权限抛出java.lang.IllegalAccessError异常。

4.3 方法解析

方法解析的第一步也是对方法表内的class_index项中的CONSTANT_class_info符号引用进行解析(参考类文件结构)。若解析成功则会按照如下的规则来搜寻对应的方法。

  1. Class文件格式中类的方法和接口的方法的符号引用时分开存储的,所以若类的方法表中发现class_index对应的是一个接口的,则抛出java.lang.IncompatibleChangeError
  2. 若1通过,若类C中有简单名称和描述符都与目标匹配的,则返回这个方法的直接引用,搜索结束。
  3. 否则,在类C的父类中递归查找简单名称和描述符与目标都匹配的方法,若有则返回这个方法的直接引用,搜索结束。
  4. 否则,在类C的接口列表及他们的父接口中递归查询简单名称和描述符与目标都匹配的方法,若有,则表示C是一个抽象类,搜索结束,抛出java.lang.AbstractMethodError
  5. 否则,查找失败,抛出java.lang.NoSuchMethodError
  6. 若查找过程成功返回了引用,则进行访问权限验证,若无权限抛出java.lang.IllegalAccessError异常。

4.4 接口方法解析

基本同方法解析。若接口解析成功,接下来的方法搜索规则如下:

  1. 若方法表中class_index对应的是一个类,java.lang.IncompatibleChangeError排除异常。
  2. 否则在C中查找是否有简单名称和描述符都与目标相配的方法,则返回该方法的直接引用,搜索结束。
  3. 否则,在接口C的父类中递归查找,直到java.lang.Object,有简单名称和描述符都与目标相配的方法,则返回该方法的直接引用,搜索结束。
  4. 对于3,由于接口运行多继承,所以存在在多个接口中都有简单名称和描述符都与目标相配的情况,那么会从这些方法中返回一个,并结束查找。
  5. 否则,查找失败,抛出java.lang.NoSuchMethodError
  6. 若查找过程成功返回了引用,则进行访问权限验证,若无权限抛出java.lang.IllegalAccessError异常。(JDK9的影响)

5. 初始化

类初始化是类加载过程的最后一个阶段。在这个阶段JVM才真正开始执行类中编写的程序代码,将主导权交给程序。在准备阶段,已经对类变量赋了零值,在这一阶段,将会根据程序编码去初始化类变量和其它资源。也可以说初始化就是执行类构造器()方法的过程。

接下来我们对<clinit>()方法做一些说明。

  • <clinit>()方法是由javac编译器自动生成的,是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序觉得的,静态语句块中只能访问到在静态语句块之前的变量,定义在其之后的变量,在该语句块中只能赋值不能访问。
  • <clinit>()方法与<init>()方法(类的构造函数)不同,它不需要显示的调用父类构造器,JVM会保证在子类的<clinit>()执行前,父类的<clinit>()已经执行完毕。可以推论出,JVM中第一个被执行的<clinit>()方法是java.lang.Object的。
  • 由于父类的<clinit>()先执行,所以父类的静态语句块要优先于子类的变量赋值操作。
  • <clinit>()对于接口来说是非必要的。
  • 接口中不能使用静态代码块,但是可能存在类变量的赋值操作,因而接口也会生成<clinit>()方法。但是当接口的<clinit>()方法执行时,不要求父接口的<clinit>()方法先执行,只有当父类中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也不会执行接口的<clinit>()方法。
  • JVM必须保证一个类的<clinit>()方法在多线程环境下被正确的加锁同步。若一个类的<clinit>()方法中有耗时很长的操作,那就可能造成阻塞。

类加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起确立其在JVM中的唯一性。

双亲委派模式

从虚拟机的视角类加载器可以分为:

  1. BootStrapClassLoader启动类加载器,用C++实现,是JVM的一部分。
  2. 其他类加载器,用Java实现,都继承了抽象类java.lang.ClassLoader,独立存在于JVM之外。

从使用这角度可分为:

  1. BootStrapClassloader

    负责加载在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,并且是能够被JVM所识别的类库加载到虚拟机内存中。

  2. Extension Class Loader 这个类是在sum.mis.Launcher$ExtClassLoader中以Java代码实现的。它负责加载<JAVA_HOME>\ext\目录中,或者被java.ext.dirs系统变量所指定的路径中的所有类库。

  3. Application Class Loaser这个类由``sum.mis.Launcher$AppClassLoader`来实现,有时也被称为“系统类加载器”,它用来加载用户类路径上所有的类库。

JDK9之前的Java应用都是由这三类加载器来相互配合完成加载。通常这些类加载器按照下图的协作关系来完成加载:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ztTqTNW1-1617897204791)(https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg4.mukewang.com%2F5bdf01aa0001a43210380303.jpg&refer=http%3A%2F%2Fimg4.mukewang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1620482748&t=d3df08dd58dfb0a1299f9f0207afff4c)]

这样的模型被称为双亲委派模型,其工作过程如下:

若一个类加载器收到了类加载请求,它首先不会自己去加载,而是将这个请求委托给父类加载器去完成,每一层次的类加载器都是如此。因此所有的加载请求最终都应该传送到最顶层的BootStrapClassloader ,只有当父加载器反馈自己无法完成这个加载请求(及在它的搜索范围类没有找到所需的类),子需求才会去尝试自己完成加载。

注意:双亲委派中的父加载器,不是继承关系中的父子关系,而是通过组合关系来复用父加载器的代码。

破坏双亲委派

在上一点中我们提到通常情况下,加载是按照双亲委派模型执行,意味着存在这其它方法,也就是双亲委派模型被破坏。按历史反正可以分为下面三种情况:

第一次:

由于双亲委派模型是JDK1.2引入的,ClassLoader是在第一个版本就存在了的,并且加载的核心代码在loadClass中(可参考《JVM》P284),所以为了兼容用户已经自定义类加载器的情况,双亲委派在实现中做出了妥协,在loadClass方法中加了一个protected修饰的findClass方法,并引导用户使用findClass

第二次:

第二次破坏是基于双亲委派的模型自身的缺陷,双亲委派很好的解决了基础类型一致性的问题,但是对于基础类型需要回调用户的代码,双亲委派无能为力。这个时候引入线程上下文类加载器!待深入研究

第三次:

这次破坏基于对代码热替换,模块热部署的追求。例如OSGi。!待深入研究


参考资料:

《深入理解Java虚拟机》

Tomcat类加载器破坏双亲委派

从JDBC看“破坏”双亲委派模型

真正理解线程上下文类加载器

服务发现机制


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

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

相关文章

服务端开发——云服务器的端口转发设置(SSH隧道)

引言 本篇博客介绍端口转发的知识&#xff0c;并详细阐述操作和设置步骤。这是因为在实际工作中&#xff0c;会有很多企业从安全的角度考虑&#xff0c;为线上或重要的服务器设置一个跳板机&#xff08;堡垒机&#xff09;&#xff0c;避免远程开发人员直接操作&#xff0c;是…

Shiro————核心设计思想

引言 以此篇博客为引&#xff0c;开启一个新的专栏分类——Shiro。 之前在工作中有比较快速的学习过Shiro安全框架&#xff0c;但经过一年的荒废&#xff0c;已经不是很熟悉了&#xff0c;通过这个系列&#xff0c;深入研究和学习Shiro的一些知识&#xff0c;填补安全管理方面…

Web应用安全————账号冻结与 Session 实时失效

引言 开篇时说些题外话&#xff0c;最近刚刚被公司CY&#xff0c;不过很快找到了下家&#xff0c;也同时拿到了三家公司的Offer。一周面试下来&#xff0c;总体感觉面试题少了&#xff0c;不过多了上机程序题。新公司是做外包&#xff0c;不过相比于上一家公司&#xff0c;也算…

Web应用安全————Shiro 解决会话固定漏洞

引言 承接上一篇《Web应用安全————账号冻结与 Session 实时失效》关于 session 的学习&#xff0c;本篇博客聚焦如何通过 shiro 解决会话固定导致的漏洞问题。 首先&#xff0c;没怎么接触过应用安全方面的小伙伴可能会发起疑问 - 什么是会话固定&#xff1f; 简单来说&…

Web应用安全————多点登录互斥

引言 在实际生活中&#xff0c;很多网站都做了多点登录互斥的操作&#xff0c;简单来说就是同一个账号&#xff0c;只能在一台电脑上登录&#xff0c;如果有人在其他地方登录&#xff0c;那么原来登录的地方就会自动下线&#xff0c;再进行操作就会弹出登录界面。 实现思路 …

Linux进阶之路————磁盘查询

引言 承接《Linux进阶之路————Linux磁盘分区与挂载》&#xff0c;本文介绍实际生产中对于磁盘的监控和查询。 一、查询磁盘整体使用情况 基本语法&#xff1a; df -h 该命令会显示包括我们手动挂载的磁盘&#xff0c;如果使用 umount 卸载磁盘&#xff0c;那么将不会显示…

Linux进阶之路————CentOS网络配置

引言 Linux在装机后&#xff0c;如果没有特殊配置&#xff0c;会使用动态获取 IP 地址的策略。本文描述了&#xff0c;虚拟机使用网络的拓扑图&#xff0c;以及如何通过配置&#xff0c;将 IP 地址固定下来&#xff0c;不会因为重启而失效。同时可以访问外网地址。 一、NAT模…

Linux进阶之路————进程与服务管理

引言 在Linux 中&#xff0c;每个执行的程序&#xff08;代码&#xff09;都成为一个进程&#xff0c;Linux 为每一个进程分配了一个唯一的 id 号 - PID。 每个进程都会对应一个父进程&#xff0c;而这个父进程可以复制多个子进程&#xff0c;例如 www 服务器。 每个进程都可…

Linux进阶之路———— RPM 与 YUM 包管理

引言 rpm 是一种用于互联网下载的打包及安装工具&#xff0c;它包含在某些 Linux 发行版中&#xff0c;生成具有 .rpm 扩展名的文件。rpm 是 redhat package manager&#xff08;RedHat 软件包管理器&#xff09;的缩写&#xff0c;类似 Windows 下的 setup.exe 文件。这一文件…

Linux进阶之路———Shell 编程入门

引言 通过 Shell 编程的学习&#xff0c;铺平架构师道路上的一块大砖。 Shell 在Linux 系统中的定位如下所示&#xff1a; 一、第一个 Shell 脚本 我们通过一个简单的 Shell 脚本来感受一下。 在 Shell 中不需要加 “;” 结尾&#xff0c;通过 vim 可以进行 shell 的编程工…

Linux 实操———CentOS 6 安装配置 Oracle JDK 1.8

引言 本篇博客也属于Linux进阶系列&#xff0c;主要讲解如何在CentOS 6 下安装并配置 JDK 8。由于通过 yum 搜索的结果都是 openjdk&#xff0c;而目前企业中还是以 Oracle jdk 为主&#xff0c;因此&#xff0c;操作步骤这样的。 在Oracle 官网把 jdk 1.8 下载下来&#xff…

Linux 实操———CentOS 6 安装配置 Tomcat

引言 Linux下安装Tomcat。 一、下载、传输与解压 同《Linux 实操———CentOS 6 安装配置 Oracle JDK 1.8》一样&#xff0c;前期都是先在远程机上下载压缩包&#xff0c;然后通过远程终端&#xff0c;将压缩包放在 Linux 的 opt 目录下&#xff0c;然后解压。 下载地址是T…

Spring Boot 实用开发技巧————Eclipse 远程调试

引言 在之前的开发当中&#xff0c;都会进行本地项目启动&#xff0c;然后向本地服务发起请求来进行 Debug 调试代码&#xff0c;这也是开发人员最常见的调试操作。但是当项目逐渐成型&#xff0c;慢慢的将各个模块部署到服务器后&#xff0c;调试的手段可能就仅仅剩下查看执行…

Linux 实操———— Shell 远程执行命令

引言 目前&#xff0c;开发人员的部署方式是&#xff0c;将项目打包(Maven 打包) 然后将 生成的 jar 包等文件&#xff0c;通过Xshell 等终端工具手动传输到远程服务器上&#xff0c;然后再通过在终端执行远程服务器上的 shell 脚本来启动服务。 本篇博客聚焦这样一种解决方案…

Spring Boot 设置 ASCII banner 艺术字

引言 无意中看到Spring boot 项目的 resources 目录下有一个 banner.txt &#xff0c;打开一看&#xff0c;居然是ASCII 字符画。于是兴起&#xff0c;简单研究了一下。 Spring boot 可以加载 resources 目录下的 banner.txt 文件&#xff0c;将字符画在启动之初输出到日志或…

MySQL 基础 ———— 分组查询

引言 承接上一篇《MySQL 基础 ————高频函数总结》&#xff0c;本篇单独针对分组查询进行简单的总结和归纳&#xff0c;并为后续更为复杂的DQL 语句做好铺垫。 查询语句&#xff1a; SELECT AVG(salary) FROM teacher; 实际上是以全表的 salary 字段来求平均值。但是在实…

MySQL 基础 ———— 连接查询

引言 本篇文章承接《数据库与SQL语句》专栏&#xff0c;进入DQL的重要环节&#xff0c;可以说&#xff0c;这一部分的内容应该占据SQL语言的大部分使用场景。 本篇的连接查询知识&#xff0c;和后面的一些重要的查询知识总结&#xff0c;共同构成了在工作中80%的MySQL应用场景…

MySQL 基础 ———— 子查询

引言 承接《MySQL 基础 ———— 连接查询》&#xff0c;本文介绍和展示SQL中子查询的使用。 子查询是出现在其他语句中的select 语句&#xff0c;也称为内查询。外部的查询语句&#xff0c;称为主查询或外查询。 一、子查询的分类和支持的子句 按照子查询出现的位置&#…

MySQL 基础 ———— SQL语句的执行顺序与 LIMIT 子句

引言 到目前为止&#xff0c;已经总结了常见的SQL子句&#xff0c;包括 SELECT 、FROM、JOIN ... ON、WHERE、GROUP BY、HAVING、ORDER BY。 虽然SQL的书写顺序是固定的&#xff0c;但在MySQL引擎中执行的顺序并不完全和书写顺序一致。除了上述这些子句&#xff0c;下面将会介…

MySQL 基础———— UNION 联合查询

引言 联合查询与连接查询不同&#xff0c;通过UNION 关键字&#xff0c;我们可以将多个查询语句一同执行并将结果集展示出来&#xff0c;不涉及到任何关联关系。 UNION 的含义是“联合&#xff0c;并集&#xff0c;结合”&#xff0c;在MySQL中可以将多个查询语句的结果合并成…