Java虚拟机-类加载机制

总览

类从被加载到虚拟机,到被卸载。其整个生命周期包括以上几个阶段:加载 (Loading)、验证 (Verification)、准备 (Preparation)、解析 (Resolution)、初始化 (Initialization)、使用 (Using)、卸载 (Unloading)java

其中类加载过程包括加载 (Loading)、验证 (Verification)、准备 (Preparation)、解析 (Resolution)、初始化 (Initialization)五个阶段。除了解析 (Resolution)阶段顺序不肯定之外,其余四个阶段是按顺序开始的。数据库

解析阶段可能在初始化阶段以后开始,其目的是为了支持Java的动态绑定,好比多态(调用父类方法实际执行的是子类覆盖的方法)。安全

加载阶段

在加载阶段,虚拟机完成如下三件事情:网络

  1. 经过一个类的全限定名来获取其定义的二进制字节流 (获取的途径能够从Class文件中、Jar包中、网络中(好比Applet)、或由其余文件生成(JSP应用))。
  2. 将这个字节流所表明的静态存储结构转化为方法区的运行时数据结构
  3. 在Java堆中生成一个表明这个类的java.lang.Class对象,做为对方法区中这些数据的访问入口。

类加载器 ClassLoader

在加载阶段咱们能够实现本身的ClassLoader,从而能够动态的建立符合特定化需求的类,或者是能够从特定的数据源 (网络、文件系统、数据库等等) 获取class文件。数据结构

大体的类加载器层级结构以下,多线程

  • 启动类加载器 (Bootstrap ClassLoader),负责加载 JDK\jre\lib 下,或被-Xbootclasspath参数指定的路径中的,而且能被虚拟机识别的类库(如rt.jar,全部java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器没法被Java程序直接引用布局

  • 扩展类加载器 (Extension ClassLoader),负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的全部类库(如javax.*开头的类),开发者能够直接使用扩展类加载器spa

  • 应用程序类加载器 (Application ClassLoader), 负责加载用户类路径(ClassPath)所指定的类,开发者能够直接使用该类加载器。若是没有自定义的类加载器,通常这就是程序中默认的类加载器。线程

如上图所示的这种层级结构成为Java类加载器的双亲委派模型。当前加载器上层的叫作父加载器,但他们的关系不是靠继承来实现,而是使用组合的方式。设计

双亲委派

若是一个类加载器收到加载类的请求,首先将请求委托给父加载器,依次向上层请求,直到顶端的启动加载器,此时只有当该加载器找不到对应的类时,才会让子类去加载,直到找到该类。

Java设计者提出的这种约束模型,

  1. 首先具有了一种优先级的层次关系
  2. 其次保证Java程序运行的稳定和安全

例如要请求加载java.lang.Object类,最终全部的加载器都会委托启动加载器去加载,从而保证了不管是哪个加载器想要加载Object类,最终都指向同一个类。

验证

验证是为了确保Class文件中的字节流符合虚拟机的要求,而且不会损害虚拟机安全。

验证大体分为四个阶段,

  1. 文件格式验证,保证文件的字节流能被正确的解析,并被存储到方法区中
  2. 元数据验证,确保元数据信息符合Java语法规范
  3. 字节码验证,对类的方法体校验,确保运行时不会危害到虚拟机
  4. 符号引用验证,对类自身外的匹配校验 (好比常量池中的符号引用)

准备

主要为类变量分配内存并设置变量初始值,都在方法区中进行分配。

注意要点:

  1. 此时的内存分配只包括类变量 (static)。实例变量在对象初始化时分配在Java堆中。
  2. 此时变量的初始值是对应数据类型的默认零值(如0、null、false等)。

举个例子,以下

public static int number = 6;
复制代码

变量number在准备阶段后的值为0,而不是6。由于此时尚未开始执行Java方法,而将变量number赋值为6是在程序编译后,执行putstatic指令,存放于类的构造器<clinit>()方法中,因此number赋值为6的操做将在初始化阶段进行。

但若是上述变量前加上final关键字,则会在编译期就将其结果放入常量池中,即准备阶段后此时number值为6.

解析

该阶段虚拟机完成将常量池中符号引用转化为直接引用的过程。其中,

  • 符号引用,是以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关,即所引用的目标不必定加载到了内存中。
  • 直接引用,是一个指向目标的指针。与虚拟机实现的内存布局相关,若是目标有了直接引用,说明已经被加载到了内存中。

解析主要针对类或接口、字段、类方法、接口方法四类符号引用,他们和常量池中的类型对应关系以下表,

符号引用 常量类型(常量池)
类或接口 CONSTANT_Class_info
字段 CONSTANT_Fieldref_info
类方法 CONSTANT_Methodref_info
接口方法 CONSTANT_InterfaceMethodref_info

初始化

此阶段开始执行类中的Java代码,主要执行的是类构造器<clinit>()方法。

<clinit>()是由编译器自动收集类中全部类变量的赋值动做和静态语句块中的语句合并产生的,编译器收集的顺序由 语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它以前的类变量,定义在它以后的类 变量只能赋值,不能访问。例如如下代码:

public class Test {
    static { 
        i = 0; // 给变量赋值能够正常编译经过
        System.out.print(i); // 这句编译器会提示“非法向前引用”
    } 
    static int i = 1;
}
复制代码

因为父类的 <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>() 方法中有耗时的操做,就可能形成多个线程阻塞,在实际过程当中此种阻塞很隐蔽。

相关文章
相关标签/搜索