类加载机制:类加载过程

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序以下图所示:
 

 

其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是肯定的,而解析阶段则不必定,它在某些状况下能够在初始化阶段以后开始,这是为了支持  Java 语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,由于这些阶段一般都是互相交叉地混合进行的,一般在一个阶段执行的过程当中调用或激活另外一个阶段。(例如:加载阶段的同时会交叉使用到验证阶段的步骤)
下面详细讲述类加载过程当中每一个阶段所作的工做:
 
 
三大阶段:加载,链接,初始化
 
加载
加载时类加载过程的第一个阶段,在加载阶段,虚拟机须要完成如下三件事情:
  • 1.经过一个类的全限定名来获取其定义的二进制字节流。
  • 2.将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构。
  • 3.在 Java 堆中生成一个表明这个类的 java.lang.Class 对象,做为对方法区中这些数据的访问入口。
相对于类加载的其余阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动做)是可控性最强的阶段,由于开发人员既可使  用系统提供的类加载器来完成加载,也能够自定义本身的类加载器来完成加载。
加载阶段完成后,虚拟机外部的  二进制字节流就按照虚拟机所需的格式存储在方法区之中,并且在 Java 堆中也建立一个 java.lang.Class 类的对象,这样即可以经过该对象访问方法区中的这些数据。
说到加载,不得不提到类加载器,下面就具体讲述下类加载器。
站在  Java 虚拟机的角度来说,只存在两种不一样的类加载器:
  • 启动类加载器:它使用 C++ 实现(这里仅限于 Hotspot,也就是 JDK1.5 以后默认的虚拟机,有不少其余的虚拟机是用 Java 语言实现的),是虚拟机自身的一部分。
  • 全部其余的类加载器:这些类加载器都由 Java 语言实现,独立于虚拟机以外,而且所有继承自抽象类 java.lang.ClassLoader,这些类加载器须要由启动类加载器加载到内存中以后才能去加载其余的类。
站在  Java 开发人员的角度来看,类加载器能够大体划分为如下三类:
  • 启动类加载器:Bootstrap ClassLoader,它负责加载存放在JDK\jre\lib(JDK 表明 JDK 的安装目录,下同)下的jar,(如 rt.jar,全部的java.*开头的类均被 Bootstrap ClassLoader 加载)。启动类加载器是没法被 Java 程序直接引用的。
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由 java.ext.dirs 系统变量指定的路径中的全部类库(如javax.*开头的类),开发者能够直接使用扩展类加载器。
  • 应用程序类加载器:Application ClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者能够直接使用该类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。
这几种类加载器的层次关系以下图所示:

 

这种层次关系称为类加载器的双亲委派模型。咱们把每一层上面的类加载器叫作当前层类加载器的父加载器,固然,它们之间的父子关系并非经过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。该模型在  JDK1.2 期间被引入并普遍应用于以后几乎全部的 Java 程序中,但它并非一个强制性的约束模型,而是 Java 设计者们推荐给开发者的一种类的加载器实现方式。
双亲委派模型的工做流程是:若是一个类加载器收到了类加载的请求,它首先不会本身去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,所以,全部的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即没法完成该加载,子加载器才会尝试本身去加载该类。
使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是  Java 类随着它的类加载器(说白了,就是它所在的目录)一块儿具有了一种带有优先级的层次关系,这对于保证 Java 程序的稳定运做很重要。例如,类java.lang.Object 类存放在JDK\jre\lib下的 rt.jar 之中,所以不管是哪一个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了 Object 类在程序中的各类类加载器中都是同一个类。
验证
验证的目的是为了确保  Class 文件中的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。不一样的虚拟机对类验证的实现可能会有所不一样,但大体都会完成如下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
  • 文件格式的验证:验证字节流是否符合 Class 文件格式的规范,而且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区以内。通过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
  • 元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合 Java 语法规范的元数据信息。
  • 字节码验证:该阶段验证的主要工做是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会作出危害虚拟机安全的行为。
  • 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身之外的信息(常量池中的各类符号引用)进行匹配性的校验。
 
 
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有如下几点须要注意:
  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  • 这里所设置的初始值一般状况下是数据类型默认的零值(如 0、0L、null、false 等),而不是被在 Java 代码中被显式地赋予的值。
假设一个类变量的定义为:
public static int value = 3;
那么变量  value 在准备阶段事后的初始值为 0,而不是 3,由于这时候还没有开始执行任何 Java 方法,而把 value 赋值为 3 的 putstatic 指令是在程序编译后,存放于类初始化方法 <clinit>中(<clinit>方法是Java中间语言的一个类初始化方法,它是在初始化阶段才被JVM调用),因此把 value 赋值为 3 的动做将在初始化阶段才会执行。注意:并不是全部的类都会拥有一个 <clinit>方法,在如下条件中该类不会拥有 <clinit>方法:
  • 该类既没有声明任何类变量,也没有静态初始化语句;
  • 该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化;
  • 该类仅包含静态 final 变量的类变量初始化语句,而且类变量初始化语句是编译时常量表达式。
(补充说明:在反编译的间语言中,咱们还会发现有<init>方法,这个是对象的初始化方法。它是在对象实例化的时候才被JVM调用)
下表列出了  Java 中全部基本数据类型以及 reference 类型的默认零值:

 

这里还须要注意以下几点:
  • 对基本数据类型来讲,对于类变量(static)和全局变量,若是不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来讲,在使用前必须显式地为其赋值,不然编译时不经过。
  • 对于同时被 static 和 final 修饰的常量,必须在声明的时候就为其显式地赋值,不然编译时不经过;而只被 final 修饰的常量则既能够在声明时显式地为其赋值,也能够在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型 reference 来讲,如数组引用、对象引用等,若是没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 若是在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
假设上面的类变量  value 被定义为:
public static final int value = 3;
编译时  Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 3。
解析
解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。解析阶段可能开始于初始化阶段以前,也可能在初始化阶段以后开始,虚拟机会根据须要来判断,究竟是在类被加载器加载时就对常量池中的符号引用进行解析(初始化以前),仍是等到一个符号引用将要被使用前才去解析它(初始化以后)。咱们也能够经过ClassLoader.loadClass(String name, boolean resolve)方法的第二个参数决定是否须要进行解析。
初始化
初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的  Java 程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员经过程序指定的主观计划去初始化类变量和其余资源,或者能够从另外一个角度来表达:初始化阶段是执行类初始化方法<clinit>的过程。