深刻理解Java虚拟机:虚拟机类加载机制

 

7.1 概述

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

类是在运行期间第一次使用时动态加载的,而不是编译时期一次性加载。安全

 

7.2 类加载的时机

类的生命周期

类的生命周期:网络

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化
  6. 使用
  7. 卸载

加载、验证、准备、初始化和卸载这5个阶段的顺序是肯定的,类的加载过程必须按照这种顺序循序渐进地开始,而解析阶段则不必定:它在某些状况下能够在初始化阶段以后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。数据结构

5种状况须要“初始化”

对于初始化阶段,虚拟机规范则严格规定了有且只有5种状况必须当即对类进行“初始化”(而加载、验证、准备天然须要在此以前开始):多线程

  • 遇到new、 getstatic、 putstatic或invokestatic这4条字节码指令时,若是类没有进行过初始化,则须要先触发其初始化。 生成这4条指令的最多见的Java代码场景是:使用new关键字实例化对象的时候、 读取或设置一个类的静态字段(被final修饰、 已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,若是类没有进行过初始化,则须要先触发其初始化。
  • 当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。
  • 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用JDK 1.7的动态语言支持时,若是一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、 REF_putStatic、 REF_invokeStatic的方法句柄,而且这个方法句柄所对应的类没有进行过初始化,则须要先触发其初始化。

如下几种常见的状况,不会触发初始化:函数

  • 经过子类引用父类的静态字段,不会致使子类初始化。
  • 经过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自Object的子类,其中包含了数组的属性和方法。
  • 常量在编译阶段会存入调用类的常量池中,本质上并无直接引用到定义常量的类,所以不会触发定义常量的类的初始化。

 

7.3 类加载的过程

包含了加载、验证、准备、解析和初始化这 5 个阶段。布局

1.加载

完成如下3件事:spa

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

其中二进制字节流能够从如下方式中获取:线程

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
  • 从网络中获取,最典型的应用是 Applet。
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
  • 由其余文件生成,例如由 JSP 文件生成对应的 Class 类。

2.验证

确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。

验证阶段大体上会完成下面4个阶段的检验动做:

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

3.准备

准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类变量初始值(“一般状况”下是数据类型的零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。

注:实例变量将会在对象实例化时随着对象一块儿分配在Java堆中。

4.解析

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

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量(明肯定义在Java虚拟机规范的Class文件格式中)。符号引用与虚拟机实现的内存布局无关,引用的目标并不必定已经加载到内存中。
  • 直接引用(Direct References):直接引用能够是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,若是有了直接引用,那引用的目标一定已经在内存中存在。

5.初始化

到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。初始化阶段是执行类构造器< clinit >()方法的过程。

  • < clinit >()方法是由编译器自动收集类中的所有类变量的赋值动做静态语句块(static{}块)中的语句合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句块能够赋值,可是不能访问。
public class Test{
  static{
    i=0; //给变量赋值能够正常编译经过
    System.out.print(i); //这句编译器会提示"非法向前引用"
  }
  static int i=1;
}
  • < clinit >()方法与类的构造函数(或者说实例构造器< init >()方法)不一样,它不须要显式地调用父类构造器,虚拟机会保证在子类的< clinit >()方法执行以前,父类的< clinit >()方法已经执行完毕。所以在虚拟机中第一个被执行的< clinit >()方法的类确定是java.lang.Object。
  • 父类的< clinit >()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操做。
static class Parent{
  public static int A=1;
  static{
    A=2;
  }
}

static class Sub extends Parent{
  public static int B=A;
}

public static void main(String[] args){
  System.out.println(Sub.B); //2
}
  • < clinit >()方法对于类或接口不是必须的,若是一个类中不包含静态语句块,也没有对类变量的赋值操做,编译器能够不为该类生成< clinit >()方法。
  • 接口中不可使用静态语句块,但仍然有类变量初始化的赋值操做,所以接口与类同样都会生成< clinit >()方法。但与类不一样的是,执行接口的< clinit >()方法不须要先执行父接口的< clinit >()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也同样不会执行接口的< clinit >()方法。
  • 虚拟机会保证一个类的< clinit >()方法在多线程环境下被正确的加锁和同步,若是多个线程同时初始化一个类,只会有一个线程执行这个类的< clinit >()方法,其它线程都会阻塞等待,直到活动线程执行< clinit >()方法完毕。若是在一个类的< clinit >()方法中有耗时的操做,就可能形成多个线程阻塞,在实际过程当中此种阻塞很隐蔽。

 

7.4 类加载器

类与类加载器

对于任意一个类,都须要由加载它的类加载器和这个类自己一同确立其在Java虚拟机中的惟一性,每个类加载器,都拥有一个独立的类名称空间。

这里的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字作对象所属关系断定结果为 true。

类加载器分类

从 Java 虚拟机的角度来说,只存在如下两种不一样的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
  • 全部其余类的加载器,这些类由 Java 实现,独立于虚拟机外部,而且全都继承自抽象类 java.lang.ClassLoader。

从 Java 开发人员的角度看,类加载器能够划分得更细致一些:

  • 启动类加载器(Bootstrap ClassLoader):此类加载器负责将存放在 \lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,而且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即便放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器没法被 Java 程序直接引用,用户在编写自定义类加载器时,若是须要把加载请求委派给启动类加载器,直接使用 null 代替便可。
  • 扩展类加载器(Extension ClassLoader):这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 /lib/ext 或者被 java.ext.dir 系统变量所指定路径中的全部类库加载到内存中,开发者能够直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。因为这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以通常称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者能够直接使用这个类加载器,若是应用程序中没有自定义过本身的类加载器,通常状况下这个就是程序中默认的类加载器。

双亲委派模型

应用程序都是由以上3种类加载器互相配合进行加载的,若是有必要,还能够加入
本身定义的类加载器。

下图展现的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents
Delegation Model,JDK1.2以后才被引入)。该模型要求除了顶层的启动类加载器外,其他的类加载器都应当有本身的父类加载器。这里类加载器之间的父子关系通常不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。

  • 工做过程

一个类加载器首先把类加载请求委派给父类加载器去完成,只有当父加载器没法完成时,子加载器才会尝试本身去加载。

  • 好处

Java 类随着它的类加载器一块儿具有了一种带有优先级的层次关系。保证Java程序的稳定运做。

  • 实现

实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中。逻辑是:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器做为父加载器。若是父类加载失败,抛出ClassNotFoundException异常后,再调用本身的findClass()方法进行加载。