JVM - 类加载机制详解(初始化时机/双亲委派)

概述:

虚拟机的类加载机制主要是指:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的一整套机制

类的加载流程:

其主要的加载流程(生命周期)如图:

在这个生命周期的流程图中,加载验证准备初始化卸载这五个阶段的顺序是确定的,也就是说要加载类时需要依次进行这个五个阶段,但是类的解析阶段则不一定,有可能在初始化之后才开始进行解析(这是为了支持Java中的动态绑定特性)。

注:更多的是这些阶段是相互交叉混合进行的,可能在执行一个阶段时唤醒另一个阶段。

类加载的时机:

什么时候需要JVM加载类?JVM规范并没有做出强制性的规定,取决于虚拟机的实现方式,但是对于类的初始化,JVM规范有着严格的规定:

  • 遇到new,getstatic,putstatic,或者invokestatic这4条字节码指令时,如果类没有进行过初始化,则触发其初始化。

  • 使用Java.lang.reflect包中的方法对类进行反射调用时,若类没有初始化,则触发其初始化

  • 当初始化一个类时,若是发现其父类和没有初始化,则首先需要触发其父类的初始化

  • 当虚拟机启动时,虚拟机会首先初始化一个用户指定的需要执行的主类(含有main方法的类),虚拟机会先初始化这个主类

  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,这个方法句柄对应的类没有进行过初始化时,需要先触发其初始化

以上这五种情形称为对类的主动引用,其都会触发类的初始化操作,有且只有这五种情况会触发类的初始化。

不会触发初始化的情形:

  1. 通过子类引用父类的静态字段,不会导致子类初始化

  2. 通过数组定义来引用类,不会触发该类的初始化

  3. 使用类中定义的常量时不会触发该类的初始化(因为常量在编译阶段就会存入调用类的常量池中,并不会引用定义常量的类)

过程:

类加载到内存中的五个主要过程

加载:

加载过程中主要任务是:

  1. 通过一个类的全类名来获取此类的二进制字节流

  2. 将此字节流所代表的静态存储结构转化为方法区的运行时数据结构

  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

验证:

这个阶段主要是确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。验证阶段是一个非常重要但不是一定必须的阶段,对于一些已经多次验证过的代码,可以考虑关闭大部分的验证措施,以缩短类加载的时间。

验证操作主要包含是个阶段:

  1. 文件格式验证: 
    验证字节流是否符合Class文件规范,并且能被当前的虚拟机所处理,以保证输入的字节流能正确的解析并存储于方法区内,格式上符合一个Java类型信息的要求。
  2. 元数据验证: 
    对字节码描述的信息进行语义分析,保证描述的信息符合Java语言的规范的要求,以保证对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
  3. 字节码验证: 
    通过对数据流和控制流分析,确定程序语义是合法的,符合逻辑的,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的事件。
  4. 符号引用验证: 
    将符合引用转化为直接引用,保证解析动作能正常执行。主要是对类自身以外的信息进行匹配性校验。

准备:

这个阶段主要是为类变量分配内存设置类变量的类型初识值的阶段。

类变量分配内存:这个是指被static修饰的类变量会在这个阶段在方法区中分配内存,而实例变量则会在对象进行实例化时才进行内存分配(在Java堆中)

设置类变量的类型初始值:这个是指在为类变量设置初始值时设置的是这个变量的类型初始值,而不是程序中指定的初始值。例如对于static int data = 90而言,在这个阶段data的值被初始化为int类型的初始值0,而不是程序中指定的初始值90

注:对于常量而言(被final修饰的字段),其在编译时会生成ConstantValue属性,所以在这个阶段会直接初始化为指定的值。

解析:

这个阶段主要是将常量池中的符号引用转化为直接引用的过程。

  • 符号引用:用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要在使用时能准确无误的定位到目标即可。

  • 直接引用:直接引用可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用则引用的目标必须已经存在于内存中。

初始化:

这个阶段主要是执行类构造器< clinit >方法的过程,在这个阶段才会真正的执行程序员所制定的程序初始化意愿。

  • 类构造器< clinit >是由编译器自动收集所有类变量的赋值动作和静态语句块中的语句合并产生的,收集顺序是由语句在源文件中出现的顺序来决定的。

  • 父类的< clinit >优先于子类的 < clinit >方法执行,也就是父类的静态语句块要优先于子类的赋值操作

  • < clinit >方法对于类或者是接口不是必须的,若是类或者接口中没有静态语句块或者是对类变量的赋值,那么可以不用生成< clinit >方法

  • 接口初始化时不必先执行父类接口的< clinit >方法,只有当父类中定义的变量使用时父接口才会进行初始化,同样接口的实现类在初始化时也不必执行接口的< clinit >方法。

  • 虚拟机会保证一个类的< clinit >可以在多线程的环境中正确的加锁,同步,如果多个线程去初始化一个类,那么只有一个能正确的执行初始化操作,其他的线程都需要阻塞等待。

类加载器:

前边说的类加载阶段可以通过类的全类名来获取一个类的二进制流,但是这个获取流的方式并没有做硬性的规定,虚拟机团队在设计阶段就将这个动作放到虚拟机外边去实现,这样一来就可以允许应用程序自己决定如何去获取所需的类,而实现这一功能的模块叫做“类加载器”

类加载器主要实现类的加载动作,对于同一个类而言,若是由不同的类加载器加载,那么其实在进行判定时会认为是两个不同的类。更规范的说,对于任意一个类而言,都需要由加载他的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类命名空间。

类加载器的分类:

主要分为两大类,启动类加载器其他类加载器

  • 启动类加载器:主要使用C++实现,是虚拟机的一部分。主要加载放在lib目录下的可被识别的类库,无法在程序中直接引用,可以通过将加载任务委派给null即可。

  • 其他类加载器:主要使用Java实现,独立于虚拟机之外并且全都继承自抽象类java.lang.ClassLoader。里边分为两类,一类是拓展类加载器,一类是应用程序类加载器。

    • 拓展类加载器:主要用于加载lib\ext目录下的所有可被识别的类库。可以在程序中直接使用

    • 程序类加载器:用于加载用户类路径上的所指定的类库,如果程序没有指定自定义的类加载器,一般情况下这个就是程序中默认的类加载器。

类加载器的双亲委派模型:

如果一个类加载器收到了加载类的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的请求最终都应该传送到顶层的启动类加载器中去,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有指定的类)时,子加载器才会尝试自己去加载。

采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。

猜你喜欢

转载自blog.csdn.net/maihilton/article/details/81517034