类的生命周期
类从被加载到虚拟机内存中开始,直到卸载出内存为止,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。验证、准备和解析统称为连接(Linking)。其中,加载、验证、准备、初始化和卸载这个五个阶段的顺序是确定的。解析阶段不一定,某些情况下可以初始化之后开始,也就是运行时绑定(动态绑定)的特效。注意,这几个阶段通常都是互相交叉混合式进行,通常会在一个阶段执行过程中调用或者激活下一个阶段,但是各阶段开始的时间点是确定顺序的。
1. 加载
加载阶段,虚拟机需要完成三件工作:
1) 通过一个类的全限定名获取定义此类的二进制流。(并未指定要从一个class文件中获取,可以从其他渠道获取,如网络,动态生成,jar,ZIP,数据库等等)
2) 将字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3) 在JAVA堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
相对于类加过程的其他阶段,加载阶段是开发期可控性最强的阶段。因为加载阶段既可以使用系统提供的类加载其完成,也可以由用户自定义的类加载去完成。
加载完成后,虚拟机外部的二进制字节流按照虚拟机所需的格式存储在方法区之中,发发去中的数据存储格式由虚拟机实现自行定义。实例化的java.lang.Class对象作为该区域的外部入口。
验证的主要目的是确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证字节流是否符合规范,如是否以魔术0xCAFFBABE开头,版本号,常量池中的类型等等
此阶段的验证是基于字节流进行的,经过此阶段验证,字节流才会进入内存的方法区存储,后面阶段的验证都是基于方法区的存储结构进行的。
对字节码描述的信息进行语义分析以保证其描述的信息符合JAVA语言规范的要求:如是否存在父类,是否继承了不被允许继承的类,是否实现了其父类或接口之中要求实现的所有方法等等
通过数据流和控制流的分析,对类的方法体进行校验分析,确保方法再运行时不会做出危害虚拟机安全的行为。如保证方法体中类型转换有效,命令跳转不会跳转到方法体外的字节码命令等。
此阶段发生在解析阶段中将符号引用转化为直接引用时,以确保解析动作能正常执行。主要验证内容包括是否可通过全限定名查找到对应的类、指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段、访问权限验证等等。
准备阶段是正式为类变量(static修饰)分配内存并设置类变量初始值的阶段。这些变量的内存均在方法区中分配。有两点需要注意:a. 此阶段进行内存分配的变量为类变量,不包括实例变量,实例变量在对象实例化是随对象一起分配在堆中。b. 初始值通常情况下是数据类型的零值如int类型的零值是0。
public static int value = 123;
public static final int value = 123;
如果类的字段属性表中存在ConstantValue属性时,则准备阶段变量value就会被初始化为ConstantValue属性所指定的值,即准备阶段value的值为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程序代码,根据开发者制定的主观计划去初始化类变量和其他资源,或者说初始化阶段就是执行类构造器<cInit>()方法的过程。
a) 由编译器自动收集类中所有的类变量的赋值动作和静态语句块(static{}块)中的语句合并产生。编译器收集顺序是由语句在源文件中出现的顺序决定。静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问。
b) 与实例构造器<init>()不同。不需要显示调用父类构造器,虚拟机会保证在子类的<cInit>()方法执行之前,父类的<cInit>()已经执行。
c) 父类的<cInit>()方法先执行,意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
d) <cInit>()方法对于类或者接口并不是必须的,如果一个类中没有静态语句块也没有对变量的赋值操作,则编译器可以不为此类生成<cInit>()方法。
e) 接口中不能使用静态语句块。执行接口的<cInit>()方法不需要先执行父接口的<cInit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。接口的实现类在初始化时也一样不会执行接口的<cInit>()方法。
f) 虚拟机保证一个类的<cInit>()方法在多线程环境中被正确的加锁和同步。
虚拟机规范严格规定有且只有4中情况下必须对类立即进行初始化。
1) 遇到new、getstatic、putstatic或者invokestatic四条指令时,如果类没有进行初始化,则触发初始化操作。
(类实例化,调用静态方法,访问类或者接口的静态变量或者对静态变量赋值)
2) 使用java.lang.reflect包的方法对类进行反射调用时。
3) 当初始化一个类发现父类还没有进行过初始化,则先触发父类的初始化。
4) 当虚拟机启动时,用户需要制定一个要执行的主类,虚拟机先初始化这个主类。
例子:
class StaticClass{
static StaticClass sc = new StaticClass();
static{
System.out.println("static init");
}
public StaticClass(){
System.out.println("class constructor");
}
public static void main(String[] args){
}
}
结果:
class constructor
static init
结果显示:实例初始化先于静态初始化。
分析:首先,main方法调用触发类初始化(静态初始化),初始化静态变量以及静态代码块时,遇到sc变量,且该变量引用的是本类的实例。此时,由于类初始化一旦完成,就不会再重复触发类初始化了,所以,此时将进行实例初始化,从而导致实例初始化先于静态初始化完成。