类加载的七个阶段

一个类的生命周期

类生命周期的7个阶段

类从被加载到虚拟机内存中开始,到卸载出内存为止。他的整个生命周期包括七个阶段:加载,验证,准备,解析,初始化,使用,卸载7个阶段。其中验证,准备,解析3个部分统称为链接*(Linking)java

 

阶段顺序

加载,验证,准备,初始化,卸载这五个阶段的顺序是肯定的,可是对于”解析”阶段却不必定。它在某些状况下能够再初始化以后再开始,这样作是为了支持java的运行时绑定特性(也称为动态绑定或晚期绑定)数据库

 

加载数组

何时须要开始类第一个阶段”加载”呢?虚拟机规范没有强制束缚。这点交给虚拟机的具体实现来自由把控。安全

“加载loading”阶段是整个类加载的第一个阶段。网络

 

加载阶段虚拟机须要完成如下三件事多线程

  1. 经过一个类的权限定名来获取定义此类的二进制字节流(将整个class文件解析成二进制流),此步骤由类加载器完成。这一动做是放在Java虚拟机外部去实现的,以便让应用程序本身决定如何获取所需的类。
  2. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据区。(将字节流的数据存入运行时数据区)
  3. 在内存中生成一个表明这个类的java.lang.Class对象。做为方法区这个类的各类数据的访问入口。(方法区内本身生成一个对象,在多线程中经常使用的类对象就是它)

 

注意:好比”经过一个类的全限定名来获取定义此类的二进制字节流”没有指定必定得从某个class文件中获取。因此咱们能够从zip压缩包,从网络中获取,运行时计算生成,数据库中读取忙活着从加密文件中读取等等。布局

咱们也能够经过JHSDB看到,JVM启动后,相关的类已经加载进入了方法区,成为了方法区的运行时结构。(注意!第一步已经把class文件的数据加载进内存了。因此说相关的类已经进入了方法区,成为了方法区的运行时结构了)。测试

验证编码

是链接阶段的第一步,这个阶段的目的是为了确保Class文件的字节流中,包含的信息符合虚拟机的要求。而且不会危害虚拟机的自身安全。但从总体上看,验证阶段大体会完成4个阶段的检验动做:文件格式验证,元数据验证,字节码验证,符号引用验证。加密

 

文件格式验证(非重点)

第一阶段要验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理,这一阶段可能包括下面这些验证点:

  1. 是否以魔数cafebaby开头。
  2. 主,次版本号是否在当前java虚拟机接受范围内。
  3. 常量池的常量中是否有不被支持的常量数据(检查常量tag标志)。
  4. 指向常量的各类索引值中是否有指向不存在的常量或不符合类型的常量。
  5. Utf8 info型的常量中是否有不符合UTF-8编码的数据。
  6. Class文件中各部分及文件自己是否有被删除的或附加的信息。

......以上只是一小部分,不必深刻研究。

 

总结:这个阶段的验证是基于二进制字节流进行的。只有经过了这个阶段的验证以后,这段字节流才被容许进入Java虚拟机内存的方法区中进行存储,因此后面三个验证阶段所有是基于方法区的存储结构(内存)上进行的,不会再直接读取,操做字节流了。

 

元数据验证(非重点)

咱们直接用编译器直接编译出来的.class文件通常没这些问题。可是.class文件的来源很杂。所以再此处再次判断。

元数据:描述类与类之间关系的数据。

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》

  1. 这个类是否有父类(除了Java,lang.Object以外,全部的类都应当有父类)。
  2. 这个类的父类是否继承了不容许被继承的类(被final修饰的类)。
  3. 类中的字段,方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不一样等。)
  4. .....

元数据验证是第二阶段,主要对类的元数据信息进行语义验证。保证不存在《Java语言规范》定义相悖的元数据信息。

字节码验证

字节码验证第三阶段是整个验证过程当中最复杂的一个阶段。主要目的是经过数据流分析和控制流分析。肯定程序语义是合法的,符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证校验类的方法在运行时不会作出危害虚拟机安全的行为。例如:

  1. 保证任意时刻操做数栈的数据类型与指令代码序列都能配合工做。例如不会出现相似于”在操做数栈放置了一个int类型的数据,使用时却按long类型加载入局部变量表中”这样的状况。
  2. 保证任何跳转指令都不会跳转到方法体之外的字节码指令上。
  3. 保证方法体中的类型转换老是有效的。例如把一个子类的对象赋值给父类数据类型,这是安全的。可是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系,彻底不相干的一个数据类型,则是危险和不合法的。
  4. .......

若是一个方法体中的字节码没经过字节码验证,那确定是有问题的。

符号引用验证(非重点)

最后一个阶段的校验行为发生在虚拟机将符号引用转换为直接引用的过程。这个转化动做将在连续的三个阶段------解析阶段中发生,符号引用验证能够看作是对类自身之外(常量池中的各个符号引用)的各种信息进行匹配性校验。通俗来讲就是,该类是否缺乏或者被禁止访问它依赖的某些外部类,方法,字段等资源。本阶段须要校验的内容以下:

  1. 符号引用中经过字符串描述的全限定名是否能找到对应的类。
  2. 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
  3. 符号引用中类,字段,方法的可访问性。
  4. 是否可被当前类访问。
  5. .....

符号引用验证的主要目的是确保解析行为可以正常执行,若是没法经过符号引用验证,将会抛出异常。

验证总结:验证阶段对于虚拟机的加载机制来讲,是很是重要,且不是必需要执行的阶段。由于验证阶段只有经过或者不经过的差异,只要经过了验证,其后就对程序运行没有任何影响了。若是程序运行的所有代码(包括本身编写的,第三方包中的,从外部加载的,动态生成的等全部代码)都被反复编译与验证过了,在生产环境的实施阶段就能够考虑用-Xverify:none参数来关闭大部分验证措施,以缩短虚拟机类加载的时间。

 

准备(给静态变量赋初值)

准备这个阶段是正式为类中定义的变量(被static修饰的变量)分配内存并设置类变量初始值的阶段。这些变量所使用的内存都将在方法区中进行分配。(为何静态方法可使用该类的class对象。由于class对象在类加载时出现。而static则是在准备阶段出现)。

这个阶段容易产生混淆的概念:

  1. 首先这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量(成员变量)。实例变量将会在对象实例化时随着对象一块儿分配在java堆中。
  2. 其次,这里所说的初始值”一般状况”下是数据类型的零值。假设一个类变量定义为。

public static int value = 123;

那变量value在准备阶段后的初始值为0而不是123。所以此时尚未开始执行任何Java方法,而把value赋值给123是后续的初始化环节。

 

解析

解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。

符号引用是一种定义,能够是任何字面上的含义。而直接引用就是直接指向目标的指针,相对偏移量。

直接引用的对象都存在于内存中。你能够把通信录里女朋友手机号码类比成符号引用,把面对面和你吃饭的女友类比为直接引用。By:享学king老师的例子。

我就直白一点:直接引用的做用是在咱们运行时数据区内部,帮助调用者找到数据的实际内存地址。符号引用就是咱们运行时数据区在类加载阶段,还未对类进行布局时,咱们经过符号引用访问class文件中数据的实际内存地址,加载进运行时数据区进行布局。布局完后就拥有了所谓的直接引用。能够相对概念来思考。在运行时数据区与class文件之间,符号引用就像直接引用同样。

 

解析大概能够分为:(不重要)

  1. 类或接口的解析
  2. 字段解析
  3. 类方法解析
  4. 接口方法解析

咱们常常遇到的异常就与这个阶段有关。

java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。(字段解析异常)

java.lang.IllegalAccessError 字段或者方法,访问权限不具有时的错误。(类或接口的解析异常)

java.lang.NoSuchMethodError 找不到相关方法时的错误。(类方法解析、接口方法解析时发生的异常)

初始化(给静态变量赋代码里的值)

做用

当一个Java类第一次被真正使用到的时候,JVM会进行该类的初始化操做。初始化过程的主要操做是执行静态代码块和初始化静态域(假如一个类有一个变量public static int a = 10。在准备阶段会赋值为0。而到了初始化阶段会赋值为10)。在一个类被初始化以前,它的直接父类也须要被初始化。可是,一个接口的初始化,不会引发其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静态域。

咱们讲到了这里,先提一下何时须要进行类加载,何时须要进行链接,何时初始化呢?

答案是大部分都是直接去触发类加载的。要触发就直接触发类的初始化了。而初始化以前的步骤顺序都是确认的,所以类加载与链接也跟着执行了。固然还有部分特殊的操做只会触发类加载不会触发类初始化,后面会举例罗列。

初始化条件

初始化主要对一个class中的static{}语句进行操做(对应的字节码就是client方法)。

Static{}语句对于类或者接口而言都不是必须的。若是一个类中,没有静态语句块,也没有对变量的赋值操做,那么编译器能够不为这个类生成<client>()方法。

初始化阶段,虚拟机规范则是严格规定了有且只有6中状况必须当即对类进行初始化(加载,验证,准备再次以前就必须开始,解析不必定)。

  1. 遇到new(实例化),getstatic(获取静态变量值),putstatic(存放静态变量值)或invokestatic(静态方法调用)这四条字节码指令时,若是类没有进行过初始化,则须要先触发其初始化,生成这4条指令最多见的Java场景是。
  1. 使用new关键字实例化对象时。
  2. 读取或设置一个类的静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候。
  3. 调用一个类的静态方法时。
  1. 使用java.lang.reflect包的方法对类进行反射调用的时候,若是类没有进行过初始化,则须要先触发其初始化。
  2. 当初始化一个类的时候,若是发现其父类尚未进行初始化,须要先触发其父类的初始化。
  3. 当虚拟机启动时,用户须要指定一个要执行的主类(包括main()方法的那个类),虚拟机会先初始化这个主类。
  4. 当使用JDK1.7的动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,而且这个方法句柄所对应的类没有进行过初始化。则须要先触发其初始化。
  5. 当一个接口定义了JDK1.8新加入的默认方法(被default关键字修饰的接口方法)时,若是这个类发生了初始化,那该接口要在其以前被初始化。

猛地一看,好家伙,那也别一条条记了,只要代码跟类沾点边儿就直接初始化呗?别慌。

咱们来看一下何时不会触发初始化。

虽然确实存在反例,但也不能彻底不能归类记忆。以上6种咱们统称为主动引用。除此以外的全部引用类型都不会触发初始化。称为被动引用。具体见如下案例:

 

 

案例

首先先定义一个父类

 

再定义一个子类

 

此时,在调用方法中列举5个例子,查看类的加载以及初始化状况。

 

案例1(M1方法)

 

 

由结果可知:若是子类引用父类中的静态字段,会使父类进行初始化,而不会触发子类的初始化(可是子类会被加载)。

 

此时父类的class文件遇到了getstatic字节码指令,而子类没有。

 

经过给VM添加-XX:+TraceClassLoading,得知子类虽然没有被初始化,可是已经被加载。

总结:子类继承父类但没有重写父类的静态字段。当调用子类.字段时只会触发父类初始化。子类仅加载。

案例2(M2方法)

 

使用数组的方式

 

并无与类初始化相关的字节码指令。所以不会初始化。

 

经过给VM添加-XX:+TraceClassLoading,得知使用数组的父类虽然没有被初始化,可是已经被加载。

这只是分配了一堆这个数据类型的空间。并无操做。能够从这个角度去理解。若是须要给这个数组赋值,须要先遍历这个数组而后依次对每一个下标进行赋值。

案例3(M3方法)

 

打印一个String常量

 

没有初始化字节码指令,所以不进行初始化。

 

经过给VM添加-XX:+TraceClassLoading,得知父类也没有被加载。

什么缘由呢?

 

这里说的就是String常量池的变量

 

    能够发如今编译Test的时候,就已经把SuperClazz的常量加载到了Test的常量池中。此外能够测试int类型的数据。可是据观察没有在常量池中找到123字面量,可是也不会进行类加载。(因此String常量池真实一个特殊的存在

案例4

 

 

 

运行发现,触发了初始化,因此必然已经类加载了。

缘由:再次证实了String常量池的特殊性。

 

线程安全

其实初始化的时候就是对静态代码块进行赋值static{}。那么若是多线程去同时初始化一个类,此时虚拟机会保证一个类的<clinit>()方法在多线程环境下会被正确的加锁,同步。因此,若是一个类的<clinit>()(也就是static{})方法中有耗时很长的操做,就有可能形成堵塞。同时也能够利用这点将一些操做放在这里以达到线程同步的效果。

扩展:在单例模式懒汉式进阶版——延迟初始化占位类模式正是用了这个思想。类加载绝对是线程安全的。