类加载

类加载时机

类加载的过程

新术语

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

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

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

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


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属性所指定的初始值。

基本数据的零值

数据类型 零值
int 0
long 0L
short (short)0
char ‘\u0000’
byte (byte)0
boolean false
float 0.0f
double 0.0d
reference null

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看“破坏”双亲委派模型

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

服务发现机制


JVM006_类加载的过程相关推荐

  1. java构造器_Java类加载的过程

    阅读本文约需要8分钟  大家好,我是你们的导师,经常看我朋友圈的同学应该知道,我每天会在微信上给大家免费提供以下服务! 1.长期为你提供最优质的学习资源! 2.给你解决技术问题! 3.每天在朋友圈里分 ...

  2. 加载文件流_jvm类加载的过程

    一个类从加载到虚拟机到使用结束从虚拟机卸载包括了加载.验证.准备.解析.初始化.使用.卸载,即为一个类的生命周期 下面来看一下类加载的过程,即加载.验证.准备.解析.初始化5个阶段都做了什么事: 阶段 ...

  3. JVM学习记录-类加载的过程

    类的整个生命周期的7个阶段是:加载(Loading).验证(Verification).准备(Preparation).解析(Resolution).初始化(Initialization).使用(Us ...

  4. 类java的步骤_java类加载的过程

    类加载就是三个过程:加载.链接.初始化 链接又可以分为验证.准备.解析 1.加载 将class字节码文件通过类加载器装入内存中 2.验证 确保当前class文件的字节流所包含的内容符合当前JVM的规范 ...

  5. java 类加载的过程

    类加载过程 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading).验证(Verification).准备(Preparation).解析(Resolution) ...

  6. 【教3妹学java】类加载的过程是什么样的?

    插: 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到网站.  坚持不懈,越努力越幸运,大家一起学习鸭~~~ 2哥:"今天只有残留的躯壳,迎接光辉 ...

  7. Java类加载的过程

    JVM和类 当我们调用 Java 命令运行某个 Java 程序时,该命令将会启动一条 Java 虚拟机进程,不管该 Java 程序有多么复杂,该程序启动了多少个线程,它们都处于该 Java 虚拟机进程 ...

  8. Java类加载的过程原理

    我们编写好的代码java代码,经过编译编程.class文件,然后类加载器把.class字节码文件加载到JVM中,接下来执行我们的代码,最后将该类卸载出JVM.而类加载到虚拟机--卸载出虚拟机,这整个生 ...

  9. jvm类加载过程_JVM知识点——深入理解JVM的类加载

    前言: 前面又说到Java程序实际上是将.class文件放入JVM中运行.虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换,解析和初始化,最终形成可以被虚拟机直接使用的Java类 ...

最新文章

  1. 华人计算机视觉鼻祖、双院外籍院士黄煦涛逝世,昔日名师门徒遍天下
  2. jquery对事件的监听方法addEventListener()
  3. Python中import模块的两种模式
  4. 轮廓检测_轮廓检测| Richer Convolutional Features | CVPR | 2017
  5. ARM(IMX6U)裸机之I.MX6ULL硬件启动方式的选择
  6. php如何操作mysql数据库代码_php如何操作mysql数据库的类(附代码)
  7. [Unity][FlowCanvas] 预制体中新建的组件无法拖入 GameObject 类型的黑板的解决办法:关闭预制体之后再打开
  8. java添加背景图片_Java怎么添加背景图片
  9. 【AutoML白皮书】:感知、认知、决策算法布局提升企业决策水平.pdf(附下载链接)...
  10. 「leetcode」203.移除链表元素:听说用虚拟头节点会方便很多?
  11. ML/DL-复习笔记【二】- L1正则化和L2正则化
  12. split-lapply-cbind模式--R语言
  13. ssh配置config文件,实现vscode免密登陆
  14. 智能云仓库存管理 v1.2.0
  15. Python打造qq音乐歌曲下载器
  16. 子之错父之过什么意思_子不教,父之过。教不严,师之惰。是什么意思?
  17. matlab如何用二分法求函数零点,如何用二分法求函数的“零点”的近似值
  18. 关于tableau的介绍
  19. 韩顺平php ecshop,ecshop教程
  20. pika异步consumer简单使用

热门文章

  1. Java PrintWriter close()方法与示例
  2. asp.net mvc3.0第一个程序helloworld开发图解
  3. C# 操作线程的通用类[测试通过]
  4. 自定义设置一个屏保程序
  5. MFC新建文件夹、打开文件夹的实现方法
  6. 嵌入式linux设计师,make在linux——《嵌入式linux设计与应用》
  7. aix oracle监听配置_Oracel:ORA-12518:监听程序无法分发客户机连接
  8. 关于lock_guard使用细节
  9. cdgb调试linux崩溃程序
  10. 计算机应用基础第二章,计算机应用基础第二章上机操作题