虚拟机类加载机制(二)类加载过程



 

 

加载

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

1、通过类的全限定名来获取定义此类的二进制字节流。但规范并没有指明二进制字节流要从一个Class文件中获取,所以,在Java的发展历史中出现了很多字节流的提供方式:zip包,网络(例如applet),动态代理技术,其他文件(如jsp);

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

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

 

相对于类加载过程的其他阶段来说,加载阶段(准确的说,是在加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,既可以使用系统提供的类加载器完成,也可由用户自定义的类加载器来完成。

 

加载阶段与连接阶段的部分内容(如一部分字节码文件格式的验证动作)是交叉进行的,加载动作尚未完成,连接阶段可能已经开始。

 

验证

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

包含以下几个方面的验证工作:

1、文件格式验证;

2、元数据验证(确保不存在不符合Java语言规范的元数据信息存在,比如是否覆盖了父类中被final修饰的属性等);

3、字节码验证。主要是进行数据流和控制流分析;

4、符号引用验证。

 

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。并且,此时分配的初始值是指该数据类型的零值。假设有一个类变量的定义为

public static int aStaticVariable = 123;

 则在准备阶段过后该变量的初始值为0,而非123,因为此时并没有执行任何Java方法。而把aStaticVariable赋值为123的putstatic指令是在程序被编译后,存放在类的构造器<clinit>()方法之中,所以把aStaticVariable赋值为123的动作将发生在初始化阶段。但如果aStaticVariable是经过final修饰的,那么在准备阶段就会直接将其赋值123。

 

解析

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

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用是能无其一的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。

直接引用:可以是直接指向目标的指针、相对偏移量或者是一个能直接定位到目标的句柄。直接引用是虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实力上翻译出来的直接引用一般不会相同。如果有了直接引用,说明引用的目标已经存在内存中存在。

 

虚拟机规范并没有规定解析阶段发生的具体时间,只是要求了在执行anewarray, checkcast, getfield, getstatic, instanceof, invokeinterface, invokespecial, invokestatic, invokevirtual, multianewarray, new, putfield和putstatic这13个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。

 

虚拟机实现可能会对同一个符号引用的解析结果进行缓存,以提高响应效率。

 

解析动作主要针对类或接口(对应常量池的CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、类方法(CONSTANT_Methodref_info)及接口方法(CONSTANT_InterfaceMethodref_info)四类符号引用进行。

 

 

初始化

类的初始化阶段是类加载过程的最后一步。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。

前面提到,在准备阶段,虚拟机已经为变量赋值过一次初始值(初始零值,如果是经过final修饰,则直接按照代码赋值),而在初始化阶段,则是根据程序的定义进行类变量和static语句块的初始化工作。换做更专业的说法,类的初始化阶段是执行类构造器<clinit>()方法的过程。

  • <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})块中的语句合并产生的。编译器收集的顺序一定是先变量赋值,再静态语句块(无论两者在源文件中出现的顺序如何),因此,在静态语句块中可以访问到类变量的初始值。
  • <clinit>()方法与类的构造函数(或者说实例构造器<init>())方法不同,它不需要显式调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此,在虚拟机中第一个被执行的<clinit>()方法的类一定是java.lang.Object。
  • 由于父类的<clinit>()方法先于子类执行,所以父类中的静态语句块要优先于子类的变量赋值操作。
  • <clinit>()方法并不是必须的,如果类或接口中没有类变量的赋值或者静态语句块,则编译器可以不为这个类生成<clinit>()方法。
  • 接口中不能使用静态语句块,但可以有静态变量,因此接口和类一样都会生成<clinit>()方法。但不同的是接口中的<clinit>()不需要先执行父接口的<clinit>(),只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程被阻塞。所以,如果在<clinit>()方法中存在耗时操作,会形成很隐蔽的阻塞。

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值