[JVM-5]类加载机制

概述

Java拥有动态加载类动态连接的特性,因此其加载过程并不像其他语言在编译时就已经完成,它是动态进行的,即在程序运行过程中动态加载入内存中。

加载过程

类加载过程

图中,加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段不一定:它在某些情况下可以初始化阶段之后在开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。

 

1、加载

加载阶段完成的是将虚拟机外部的二进制字节流按照虚拟机所需的格式存储在方法区之中。而为了完成这步需要完成哪些功能呢:

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

需要注意的是,上面所说的3个步骤,都只是规范要求的部分,这个要求其实是比较松的,很多东西并没有限制的很死,比如说第一步的获取二进制流,其并没有要求二进制流必须从Class文件获取,因此在使用过程中类的二进制流可以从网络获取,可以动态计算生成等等。

2、验证

验证作用是确保文件的字节流包含信息符合当前虚拟机要求,保证其并不会危害虚拟机的安全。因为以前说过 Class 文件并不都是源码编译而来的,人是可以手动修改生成 Class 文件的,因此这一步验证工作就十分有必要了。那么验证都需要验证哪些地方呢:

文件格式验证

这一步主要是保证Class文件格式上符合Java信息的要求。例如文件类型,版本号,常量池,常量池数据等等。。。。。。

此外在这一步字节流就会进入内存的方法区之中了,后面的操作都是基于方法区内的存储结构进行的。

元数据验证

对字节码描述信息进行语义分析,例如类是否有父类,重载是否正确,final,abstract有没有用错等,其主要目的是对类的元数据进行语义分析,保证符合Java语言规范。

字节码验证

对数据流和控制流进行分析。例如字节码指令集的正确,程序跳转的安全。其主要目的是检查方法体内的数据安全,确保程序语义合法,符合逻辑。

符号引用验证

符号引用验证也是一个比较特殊的阶段,其为解析阶段服务(这也验证了前面所说的,这几个过程并不是依次执行完成的)。在解析过程中,虚拟机将符号引用转换为直接引用,其主要是对常量池中的各种符号引用做匹配性校验。检验内容包括以下几个:

  1. 符号引用指向的类能否找到;
  2. 指定的类有没有描述的方法和字段;
  3. 符号引用指向的各种信息的访问权限是不是对的;
  4. 。。。。。。。。。。。。。。。

3、准备

准备阶段是正式为类变量分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配关于这点,有两个地方注意一下:

1、这时候进行内存分配的仅仅是类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中

2、这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如"public static int value = 123;",value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如"public static final int value = 123;"就不一样了,在准备阶段,虚拟机就会给value赋值为123。

各个数据类型的零值如下图:

4、解析

在验证阶段的符号引用验证说过解析阶段就是将符号引用转换为直接引用,那么符号引用和直接引用分别指什么呢,他们之间又有何区别:

  • 符号引用。是能够无歧义定位目标的任何形式的字面量,其与虚拟机实现的内存布局无关,引用的目标不一定需要加载入内存中;
  • 直接引用。可以直接指向目标指针,偏移量的引用,其和虚拟机实现的内存布局相关,引用的目标一定需要在内存中。

在这一步虚拟机会将类/接口,字段,类方法,接口方法等进行解析,变为直接引用。

5、初始化

初始化阶段主要是初始化类变量和其他资源,主要是通过执行类构造器<clint>()方法。

<clint>()是通过编译器自动收集所有类变量的赋值动作和静态语句块(static{}块)并按照顺序合并生成的。初始化阶段做的事就是给static变量赋予用户指定的值以及执行静态代码块

虚拟机并没有要求什么时候进行其他阶段的工作,但初始化阶段不同。当发生一下几种情况时,虚拟机必须要开始初始化工作。(作为初始化前的加载,验证,准备,解析也就都按部就班开始了)。

  1. 当在字节码层面遇到以下指令时,new(对象都要生成了,肯定要初始化了),get/put static(使用静态变量了,肯定要赋值了),invoke static(调用静态方法了都,肯定要为静态量赋值);
  2. 反射调用。当使用java。lang。reflect中的方法对类进行反射调用;
  3. 初始化一个类的时候,发现父类还有初始化,那么需要先初始化其父类,(父接口不用立即初始化,只有使用到其常量时,才需要将其初始化);
  4. 虚拟机需要一个入口,因此主类需要初始化;
  5. 动态方法解析,解析出方法是其他类的静态方法,那么需要将其初始化。

虚拟机规定有且仅有以上5种方法需要立即初始化,还有一些调用,看起来像需要初始化,但其实并不需要,可以称之为被动调用

  1. 子类直接使用父类的静态变量。虚拟机规定只有直接定义静态变量的类需要初始化,因此这种情况下,只会触发父类的初始化,而子类并不会触发;
  2. 数组对象。当定义对象数组时,只会触发数组类的初始化,其内的对象的类并不会初始化;
  3. 当一个类调用另一个类的常量时。此时并不会对常量类(被调用者)进行出初始化。只会将调用者初始化。因为在编译阶段,根据常量传播优化,会将常量类的常量放置到调用者的常量池中,此时这两个类已经没有了瓜葛,因此也就不存在将其初始化了。
posted @ 2019-06-28 14:38  予我渡北川  阅读(142)  评论(0编辑  收藏  举报