深入JVM之:虚拟机类加载机制

虚拟机类加载机制

7.2、虚拟机类加载时机

在这里插入图片描述

加载,验证,准备,初始化,卸载是固定的。

什么时候加载,都可,虚拟机规范未制定。但是什么时候初始化,严格规定。

  1. 遇到 new getstatic putstatic invokestatic 这四条字节码指令,若类型未初始化,需要先出发其初始化阶段。

    • 使用new实例化对象时。
    • 读取或设置一个类型的静态字段。
    • 调用一个类的静态方法时。
  2. 使用反射包进行反射调用时。

  3. 在初始化类时,若发现其父类未被初始化,则先初始化其父类。

  4. 虚拟机启动时,用户指定一个要执行的主类,main() 虚拟机优先初始化此类。

  5. 当接口中定义了jdk8的默认方法时,若接口实现类初始化,则接口在它之前被初始化。

在对一个类型进行主动引用时,这个类必须被初始化。

比如:

  • 通过子类引用父类的静态字段,不会导致子类初始化。
  • 通过数组定义来引用类,不会导致此类的初始化。
  • 常量在编译期间存入调用类的常量池中,本质上未直接引用到定义常量的类,所以这个类不会被初始化。

为什么在初始化数组的时候数组元素的类未被初始化呢?

因为虚拟机自动创建的是一个由Java虚拟机自动生成的,直接继承于Object类的子类。创建动作由newarray触发

7.3、类加载的过程

7.3.1、加载

包括 加载,连接,连接包括验证 准备 解析,初始化。

加载阶段需要完成以下事情:

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

Java虚拟机规范并未要求这个二进制字节流必须从某个Class文件中获取,所以我们可以从jar,war中读取。

数组不同,对于数组而言,数组本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来的。

7.3.2、验证

确保Class文件字节流中的全部内容都符合Java虚拟机规范的全部约束。

分为,文件格式验证,元数据验证,字节码验证,符号引用验证。

7.3.3、准备

准备阶段是正式为类中定义的变量(类变量,静态变量)分配内存并设置初始值的阶段。这些变量的内存都应在方法区中分配。

jdk7以前,确实是在方法区永久代中分配的。但是在jdk7及以后,hotspot把字符串常量池及静态变量移出方法区,放入了堆里。而在jdk8及以后,类变量会随着Class对象一起存放在Java堆中,类变量在方法区就完全成为逻辑上的概念了。

强调

准备阶段,进行内存分配的仅仅是类变量,静态变量,而不包括实例变量。实例变量会随着对象的实例化随着对象一并分配到堆空间中。

public static int value = 123;

这段代码,在准备阶段完成后,初始值是0而不是123,因为这时并没有开始执行Java方法,而把value赋值的putstatic指令是程序编译后,存放于中,把value赋值为123要放在类的初始化阶段进行。

特殊情况

public static final int value = 123;

字段表中存在ConstantValue 我们在准备阶段就直接将123赋值给它了。

7.3.4、解析

将部分符号引用转化为直接引用。

符号引用与直接引用

符号引用:以一组符号描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可。引用的目标不一定是已经加载到虚拟机当中的内容

直接引用:是可以直接指向目标指针、相对偏移量或者是一个能间接定位到目标的句柄。,有了直接引用,引用的目标必定已经在虚拟机内存中存在

在同一实体中,如果一个符号引用之前已经被成功解析过,那么后续引用解析应一直成功。反之亦然。

解析动作主要针对:类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符。

1、类 或 接口的解析

假设,当前代码所处的类为D,如果要把一个从未解析的符号引用N解析为一个类或接口C的直接引用,那么虚拟机需要完成以下三个步骤。

  1. 如果C不是一个数组类型,那么虚拟机将会把代表N的全限定名传给D的类加载器,加载这个类C。加载是一个连锁反应,期间可能触发到多个接口的加载。只要有一个失败,就失败。
  2. 若C是一个数组类型,并且数组元素为对象,那么加载这个对象的类就像步骤一一样,然后由虚拟机在内存中直接创建一个代表该数组维度和元素的对象。
  3. 若上面两部已经完成, 那么C已经成为一个有效的类或接口了。但是我们还是要进行符号引用验证确认D是否具备访问C的权限。不具备权限,抛出IllegalAccessError异常。

针对以上三点,JDK9之后,我们还要检查模块间的访问权限。

2、字段解析

要解析一个未被解析过的字段符号引用,需要先对它所属的类或接口进行符号解析。若出现任何异常,则解析失败。

搜索步骤:

  1. 若C本身就包含了简单名称和仔段描述符都与目标匹配的字段,则直接返回这个字段的直接引用,查找结束。
  2. 若C中实现了接口,那么在它的各个接口和它的父接口,若出现简单名称和仔段描述符都与目标相同的字段,返回直接引用,查找结束。
  3. 否则,递归查找它的父类,找到结束。
  4. 否则,查找失败,报出NoSuchFieldError 异常。

若查找成功返回了字段的直接引用,则会对这个字段进行权限验证。若不具备对字段的访问权限,报IllegalAccessError

注意,类继承的类和多个接口中不能有同名同类型的实例变量。

3、方法解析
  1. 第一步和字段解析一样,需要先对它的父类和接口解析。
  2. 由于类和接口的方法符号引用的常量类型定义是分开的,若发现class_Index 索引类型是个接口的话,直接抛出IncompatiavleClassChangeError异常。
  3. 在类C的父类中递归查找是否有见到那名称和描述符相匹配的方法,错存在结束。
  4. 在类C父类查找。
  5. 在它们实现的接口中查找,若找到,则说明此类是个抽象类,抛出AbstractMethodError
  6. 否则,查找失败,报出NoSuchMethodError异常。
4、接口解析

先解析class_index属于的类或接口的符号引用,解析成功,是接口。

搜索:

  1. 若发现是类不是接口,会报出IncompatiavleClassChangeError异常。
  2. 在接口C中查找是否有简单名称和描述符匹配的方法,有找到。
  3. 否则,递归查找父接口,包括Object类。
  4. 失败。

注意,JDK9之后,有了接口静态私有方法,完全可能出现IllegalAccessError。

7.3.5、初始化

初始化阶段真正开始执行java类中编写的代码。将主导权交给程序。

初始化阶段,就是执行类构造器的阶段()方法的过程

🔶方法是由编译器自动收集类中的静态变量的赋值动作以及静态语句块中的代码合并产生的。

编译器收集的顺序是由语句在源文件出现的顺序决定的。

注意,静态语句块只能访问到定义在静态语句块之前的变量,对于之后的变量,它可以赋值,但是无法访问。

🔶方法与类的构造函数不同,方法不同,它不需要显式的调用父类构造器。因为Java虚拟机保证了在子类方法执行前,父类的已经执行。所以,这也就说明父类的静态语句块要优先于子类的执行

🔶方法对于类或接口来说不是必须的,若一个类中没有静态语句块或者对静态变量进行赋值,那么完全可以没有此方法。

7.4、类加载器

“通过一个类的全限定名获取描述这个类的二进制字节流”,这个动作就是通过类加载器进行实现。ClassLoader

7.4.1、类与类加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机的唯一性。

比较两个类是否相等,前提是它们是由同一个类加载器创建的,若它们的类加载器不同,那么它们必定不相等。

同一个类加载器下,一个类型只能被初始化一次。

7.4.2、双亲委派模型

对于Java虚拟机来说,只有两种类加载器,一种是启动类加载器(BootStrap ClassLoader)另一种是其它类加载器。

三层类加载器

JDK8及以前

🔶启动类加载器

​ 这个类加载器负责加载存放在<JAVA_HOME>\lib 下目录,根据文件名识别,并且是JAVA虚拟机能够识别的文件名进行加载。

​ 它无法被程序直接引用。

🔶扩展类加载器

​ 负责加载<JAVA_HOME>\lib\ext 目录下的文件。是Java系统类库的扩展机制。Java虚拟机允许将具有通用性的类库放置在ext目录下扩展功能。

​ 但是自JDK9依赖,这种扩展机制被模块化所取代。

​ 我们可以在程序中直接调用这个类加载器加载文件。

🔶应用程序类加载器

这个加载器是系统默认类加载器,负责加载用户类库ClassPath上的所有类库。我们可以调用它进行类的加载。

双亲委派架构

双亲委派架构要求,除了启动类加载器,其余类加载器都应有自己的父类加载器,叫做组合,服用代码。

工作流程

一个类加载器在收到类加载指令时,不会先自己尝试加载这个类,他会把它先交给父类加载器,若父类加载不了,再自己加载。

好处就是,天生具有优先级的层次关系。例如,Object类,无论哪个类加载器加载这个类,最终都会被传到启动类加载器中,这就天然保证了Object在各类加载器中,是同一个类。

若没有类加载器,那么用户自己也可以定义一个Object类,并且放在ClassPath下,那么系统中就会出现多个Object类,这就不对了。

双亲委派模型对于保证Java程序的稳定运作极其重要。

7.4.3、破坏双亲委派模型

第一次破坏

在JDK1.2双亲委派模型引入,由于ClassLoader在之前就有,有了用户自定义加载器,双亲委派设计者不得不做一些妥协,为了兼容这部分代码,加入了findClass()方法,引导用户重写这个方法,而不是在loadClass()写代码。

若父类加载失败,则调用自己的findClass()方法,这样既不影响用户按照自己意愿加载类,又可以保证新写出的加载类符合双亲委派规则。

第二次破坏

由于自身模型缺陷导致。

双亲委派模型很好的解决了各个类加载器协作时,基础类型一致性的问题。越基础的类越是靠上层加载器加载。因为它们总是被用户继承,使用的API。若基础类型要回调用户代码怎么办呢?

为此,引入了 “线程上下文类加载器”。

若创建线程时未设置,则从父线程继承一个。若全局未设置,则是默认的应用程序类加载器。通过Thread的setContextClassLoader()进行设置。

有了这个类加载器,我们就可以让父类加载器请求子类加载器完成类加载行为。这种行为,打通了双亲委派模型的一般性原则。

第三次破坏

由于用户追求程序动态性导致的。也就是代码热替换,模块热部署。

7.5、Java模块化系统

Java模块化系统是对Java技术的一项重大升级。为了实现模块化的重要目标–可配置的封装隔离机制,Java虚拟机对类加载架构也做了调整。

Java模块定义包含:

  • 依赖其他模块的列表。
  • 导出的包列表,即其他模块可以使用的列表。
  • 开放包的列表,其他模块可以通过反射访问的模块列表。
  • 使用的服务列表。
  • 提供服务的实现列表。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值