JVM总结(四):JVM类加载机制

这一节我们来总结一下JVM类加载机制。具体目录如下: 

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

  在Java中,类型的加载、连接和初始化过程都在程序运行期间完成的,这种策略虽然会使类加载时增加一些性能开销,但是提供了高度的灵活性,Java里天生可以动态扩展的语言特就是依赖于运行期动态加载和动态连接的特点实现的。 
  Class文件指的是一串二进制的字节流。实际上,每个Class文件都有可能代表着Java语言中的一个类或者接口。

类加载的过程

类加载过程概括

 
  在这七个过程中,加载、验证、准备、初始化、卸载这5个阶段的顺序是一定的,类的加载过程必须按照这种顺序按部就班地开始,而解析过程则不一定:它在某个情况下可以在初始化阶段之后再开始,这是为了支持Java语言语言的运行时绑定(也叫动态绑定和晚期绑定)。 
  这里强调的是:类加载阶段都是互相交叉地混合式进行的,通常是在一个阶段执行的过程中调用、激活另一阶段。

说说引用

  对类的初始化操作可分为主动引用和被动引用 
主动引用:在以下5种情况下会进行类的主动引用的初始化操作:

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这4条指令最常见的代码情景是:使用new关键字实例化对象、读取过设置一个类的静态字段(被final修饰、已在编译期把结果放进常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用java.lang.reflect包对类进行反射调用时,如果类没有进行过初始化,则应需要先触发其初始化。
  • 当初始化一个类时,如果发现其父类还没有进行过初始化,会触发其父类实例化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法类),虚拟机会先实例化那个类。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例的最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则要先触发其初始化。 
    被动引用:所有引用类的方式都不会触发初始化。 
    对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。 
    通过数组定义引用类,不会触发此类初始化:当初始化对象数组时,并不会实际触发对象的初始化操作。但是会触发一个是由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。值得注意的是:该类代表了实际的对象数组,数组中应有的方法和属性都实现在这个类里。Java语言对数组的访问比C/C++相对安全是因为这个类分装了数组元素的访问方法。 
    常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义类的初始化。

值得注意的是: 
  接口也有自己的初始化过程:编译器会为接口生成“()”类构造器,用于初始化接口中所定义的成员变量。 
  接口和类初始化的区别:当一个类在初始化时,其父类都基本上初始化过了,然而接口在初始化的时候,只有真正用到父接口的时候(如引用接口中定义的常量)才会进行初始化。

详解类加载全过程:

加载

在加载阶段,虚拟机需要完成以下3件事情:

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

  非数组类的加载是可控性最强的。用户除了使用系统提供的引导类加载类来完成,也可以由用户自定义的类加载器去加载(重写一个类加载器的loadClass())。 
  注意:数组类本身不通过类加载器创建,它是由JVM直接创建的。但数组类和类加载器仍有很紧密的关系,因为数组类的元素类型最终是靠类加载器去创建。 
  加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。然后在内存中实例化一个java.lang.Class类的对象(可以在Java堆中,也可以在方法区中),该对象将作为程序去访问方法区中的这些类型数据的外部接口。 
  加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段就可能开始了。但是夹在加载阶段进行的动作,仍然属于连接阶段的内容。

验证

  验证是连接的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危及虚拟机本身的安全。 
验证阶段的四个步骤:文件格式检验、元数据检验、字节码检验、符号引用检验。

  • 文件格式检验: 
    检验字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。检验可能包含下列几种:是否以魔数开头、主次版本号是否在虚拟机的处理范围之内,常量池中的常量是否不被支持、文件是否被删除或附加什么信息等等。 
    只有通过文件格式检验的二进制字节流才能进入内存的方法区进行存储,所以后面的3个检验阶段都是基于方法区的存储结构进行的,不会在操作字节流。
  • 元数据检验: 
    对字节码描述的信息进行语义分析,以保证其描述的内容符合Java语言规范的要求。 
    验证点包括:是否有父类(除了object)、父类是否继承了不可被继承的类(被final修饰的类)、如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法、类中的方法和字段是否与父类产生矛盾(覆盖了父类的final字段、出现不合规矩的方法重载等)。 
    元数据检验主要是对类的元数据信息进行语义校验,保证不符合Java语言规范的元数据信息不存在。
  • 字节码检验: 
    通过数据流和控制流分析,确定程序语义是合法、符合逻辑的。第二阶段是对元数据信息中的数据类型做了检验,这一阶段是对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事情。 
    检验点包括:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作、保证指令跳转不会跳转到方法体之外的地方、保证方法体内的类型转换都是有效的。 
    事实上,即便是经过字节码检验后的方法体也不一定是安全的。
  • 符号引用检验 
    最后一个检验发生在虚拟机将符号引用转化为直接引用时,这个转化动作将在连接的第三阶段–解析阶段中发生的。符号引用检验可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。 
    校验点:符号引用中通过字符串描述的全限定名是否能找到对应的类、在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段、符号引用中的类、字段、方法的访问权限是否能让当前类访问到等。 
    符号引用检验的目的是确保解析动作的正常执行,如果无法通过符号引用检验,将会抛出java.lang.IncompatibleClassChangeError异常的子类,如IllegalAccessError、NoSuchfiledError、NoSuchMethodError等。

准备

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存将在方法区中进行分配。此时进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。另外,在这里分配的静态类变量是将其值定义为0等默认值,而不是我们定义的。因为这时尚未执行任何Java方法,我们定义的赋值的putStatic指令是程序被编译后,存放在类构造器()方法中,所以正确的赋值将在初始化阶段执行。 
  如果类变量被final修饰,那么在这种情况下,在编译时Javac将会为该变量生成ConstantValue属性,在准备阶段虚拟机会根据该属性设置类变量的正确值。

解析

  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 
a、符号引用:以一组符号来描述所引用的目标,符号可以是任何形式字面量,只要使用时无歧义地定位到目标就行。 
b、 直接引用:直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。引用的目标已经在内存中存在。 
  虚拟机实现可以根据需要来判断到底在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用时才去解析它。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化

类加载的最后一步,真正执行类中定义的Java程序代码(字节码)。 
初始化阶段是执行类构造器()方法的过程,根据程序员通过程序制定的主观的计划去初始化类变量和其他资源。

posted on 2016-02-25 16:51  逝宇、  阅读(2918)  评论(0编辑  收藏  举报

导航