`
chenzhou123520
  • 浏览: 4249815 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

JVM学习笔记(七):类加载的过程

阅读更多

本文根据《深入理解java虚拟机》第7章部分内容整理 

 

      在我的上一篇文章《JVM学习笔记(六):类加载的时机》中提到了java类从加载到卸载过程包括了加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。下面我们来详细讲解一下类加载的全过程,也就是加载、验证、准备、解析和初始化这五个阶段的过程。

 

  一.加载

    首先要说明的是“加载”(Loading)阶段只是“类加载”(Class Loading)过程的一个阶段。不要混淆了这两个概念。在加载阶段,虚拟机需要完成以下三件事情:

    1.通过一个类的权限定名称来获取定义此类的二进制字节流。

    2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

    相对于类加载过程的其他阶段,加载阶段是开发期相对来说可控性比较强,该阶段既可以使用系统提供的类加载器完成,也可以由用户自定义的类加载器来完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。

    二.验证

    验证是连接(linking)阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

    不同的虚拟机对类验证的实现可能会有所不同,但大致上都会完成下面四个阶段的检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。

    1.文件格式验证:该阶段主要是验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

    2.元数据验证:这一阶段主要是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。

    3.字节码验证:这一阶段是整个验证阶段最复杂的一个阶段,主要工作是进行数据流和控制流分析。在第二阶段对元数据信息中的数据类型做完校验后,这阶段将对类的方法体进行校验分析。这阶段的任务是保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。

    4.符号引用验证:主要是在虚拟机将符号引用转化为直接引用的时候进行校验,这个转化动作是发生在解析阶段。符号引用可以看做是对类自身以外(常量池的各种符号引用)的信息进行匹配性的校验。

    验证阶段对于虚拟机的类加载机制来说,是一个非常重要但不一定是必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,在实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,从而缩短虚拟机类加载的时间。

    三.准备

    准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。注:这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起被分配在Java堆中。

    四.解析

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

    符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。

    直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

    对于同一个符号引用可能会出现多次解析,虚拟机可能会对第一次解析的结果进行缓存。

    解析动作分为四类:包括类或接口的解析、字段解析、类方法解析、接口方法解析。

    五.初始化

    类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了加载(Loading)阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

    初始化阶段是执行类构造器<clinit>()方法的过程。对于<clinit>()方法具体介绍如下:

    1)<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序所决定。

    2)<clinit>()方法与类的构造函数不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中第一个执行的<clinit>()方法的类一定是java.lang.Object。

    3)由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。如下面的例子所示,输出结果为2而不是1。

 

public class Parent {
	public static int A = 1;
	static{
		A = 2;
	}
}

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

public class Test {
	public static void main(String[] args) {
		System.out.println(Sub.B);
	}
}

    4)<clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

    5)接口中可能会有变量赋值操作,因此接口也会生成<clinit>()方法。但是接口与类不同,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也不会执行接口的<clinit>()方法。

    6)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步。如果有多个线程去同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其它线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那么就可能造成多个进程阻塞。

 

0
0
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics