此篇文章主要介绍从一个Java类型(类或者接口)的生命周期(从它进入虚拟机到退出)开始阶段的装载、连接与初始化,以及占Java类型声明周期绝大部分时间的对象实例化、垃圾收集和对象终结,然后是Java类型生命周期的结束,也就是从虚拟机中卸载。
类型装载、连接与初始化
Java虚拟机通过装载、连接和初始化三个步骤,使一个类型可以被正在运行的Java程序所使用。其中装载就是把二进制形式的Java类型读入到Java虚拟机中;连接就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。
连接阶段分为三个子步骤:
验证:验证被加载的类型数据格式是否正确且适于Java虚拟机使用
准备:为该类型分配它所需要的内存,比如为它的类变量分配内存
解析:负责把常量池中的符号引用转换为直接引用,虚拟机可以推迟解析这一步。它可以在当运行中的程序真正使用某个符号引用时候再去解析它(把该符号引用转换为直接引用)
当以上步骤都完成后(解析步骤可选),该类型就为初始化做好了准备,在初始化期间,将给类变量赋以适当的初始值。
装载、连接和初始化这三个步骤必须按顺序执行,解析这一步骤可以在初始化之后执行。
在装载和连接的步骤中,Java虚拟机规范给实现提供了一定的灵活性。但是它严格的定义了初始化的时机。所有的Java虚拟机必须在每个类或者接口主动使用时初始化,下面这六种情况符合主动使用的要求。
当创建某个类的新实例时(在字节码中执行new指令;或者通过不明确的创建、反射、克隆、反序列化)
当调用某个类的静态方法时
当使用某个类或接口的静态字段,或者对该字段赋值时(用final修饰的静态变量除外,它被初始化为一个编译时的常量表达式)
当调用Java API中的某些反射方法时
当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)
当虚拟机启动时某个被标明为启动类的类(即含有main()方法的那个类)
除上述这六种情况外,所有其他使用Java类型的方式都是被动使用,它们都不会导致Java类型的初始化。
装载
装载阶段由三个基本动作组成,要装载一个类型,Java虚拟机必须经过三个步骤:
通过该类的完全限定名,产生一个代表该类型的二进制数据流(找到class文件)
解析这个二进制数据流为方法区的内部数据结构(解析这个class文件)
创建一个表示该类型的java.lang.Class类的实例(创建Class实例)
那么二进制数据流是怎么产生的呢?
Java虚拟机规范并没用说Java类型的二进制是应该怎样产生,一般来说可能有以下几种产生方式
从本地文件系统装载,或是通过网络下载,或者通过某种归档文件中解压等等
Java源文件动态编译为Class文件
动态为某个类型计算其class文件格式
总而言之,有了二进制数据之后,Java虚拟机才能够创建java.lang.Class的实例对象。而装载步骤的最终产品就是这个Class实例对象。
类装载器并不需要一直等到此类型“首次使用”时再去装载它,Java虚拟机规范允许缓存Java类型的二进制表现形式,在预料到某个类型将要使用时就去装载它,或者把这些类型装载到一些相关的分组里面。但如果一个类装载器在预装载时候遇到问题,无论如何,它应该在类型被首次使用时报告该问题(通过抛出一个LinkageError异常的子类)。
验证
当类型被装载后,下一步就准备进行连接了。连接过程第一步是验证-------确定类型符合Java语言的语义,并且它不会危及虚拟机的完整性。
验证上,不同虚拟机实现拥有一定的灵活性,虚拟机设计者可以决定如何以及何时验证类型。Java虚拟机规范则列出虚拟机可以抛出的异常以及在何时抛出他们。一般情况,规范会明确的说明异常或者错误在何种条件下应该被抛出,但是通长没有严格规定如何或者在何时检查错误条件。
不管怎样,在大多数Java虚拟机实现中特定类型的检查一般都在特定的时间发生。比如在装载过程中,虚拟机必须解析代表类型的二进制数据流,并且构造内部结构。在此期间,必须做一些待定的检查,以保障解析二进制文件的过程中不会导致虚拟机崩溃。一般检查会包括,确保二进制数据全部是预期的格式。另一个可能的装载时检查是,确保除了Object之外的类都有一个超类。在装载时检查一个类时,它必须确保该类的所有超类都已经被装载,而得到超类名字的唯一方法时观察类的二进制数据。
那么在正式验证阶段都会做哪些检查呢?任何在此之前没有进行的检查已经在此之后不会被检查的项目都包含在内。首先列出确保各个类之间二进制兼容的检查:
检查final的类不能拥有子类
检查final的方法不能被覆盖
确保类和超类之间不存在不兼容的方法(比如两个方法拥有同样的名字,入参,且入参再顺序上,数量和类型都相同,但是返回类型不同)
检查所有的常量池入口相互之间一致
检查常量池中所有的特殊类型字符串(类名,字段名和方法名、字段描述符和方法描述符)是否符合格式
检查字节码的完整性
准备
随着Java虚拟机装载一个类,并执行了一些它选择进行的验证之后,类就可以进入准备阶段了。在此阶段Java虚拟机为类变量分配内存,设置默认初始值。但在初始化阶段之前,类变量都没有初始化为真正的初始值。(准备阶段是不会执行Java代码的),在准备阶段,虚拟机把类变量新分配的内存根据类型设置为默认值。
在Java虚拟机内部,boolean一般为实现为一个int,也总是初始化为false。
在准备阶段,Java虚拟机实现可能也为一些数据结构分配内存,目的是提高程序的性能。例如方法表。
解析
类型经过验证和准备之后,它就可以进入第三个也就是最后一个连接阶段了------解析。解析的过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,然后把这些符号引用替换成直接引用的过程。
初始化
为了让一个类或者接口被首次主动使用,最后一个步骤就是初始化,也就是为类变量赋予正确的初始值。这些正确的初始值是根据程序员指定的主观计划而生成的。
在Java代码中,一个正确的初始值是通过类变量初始化语句或者静态变量初始化语句给出的。一个类变量初始化语句是变量声明后面的等号和表达式:
静态初始化语句是一个以static关键字开头的程序块
所有的类变量初始化语句和类型的静态初始化器都被Java编译器收集在一起,放到了一个特殊的方法中。对于类来说,此方法被称为初始化方法;对于接口来说,这个方法叫做接口初始化方法。在类和接口的Java class文件中,这个方法被称为“<clinit>”。此方法只能被Java虚拟机调用,专门用于把静态变量设置为它们的正确初始值。
初始化一个类包含两个步骤:
1,如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。
2,如果类存在初始化方法,则执行此方法。
当初始化一个类的直接超类的时候,也需要包含这两个步骤。超类总是在子类之前就被初始化。
<clinit>() 方法:<clinit>() 方法的代码并不显式的调用超类<clinit>()方法。在Java虚拟机调用类的<clinit>()方法之前,它必须确认超类<clinit>()方法以及被执行。
Java虚拟机必须保持初始化过程被正确的同步。如果多个线程需要初始化一个类,仅仅允许一个线程来执行初始化,其他线程需要等待。
并非所有的类都需要在它们的Class文件中拥有一个<clinit>()方法。下面几种情况不会出现<clinit>()方法:
此类没有声明任何类变量,也没有静态初始化语句。
此类声明了类变量,但没有明确使用类变量初始化语句或者静态初始化语句。
此类仅包含静态final变量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式。
下面这个例子,Java编译器不会为它产生<clinit>()方法
接口也可能在Class文件中包含一个<clinit>()方法,所有在接口中声明的隐式公开(public)、静态(static)和最终(final)字段都必须在字段初始化语句中初始化。如果接口包含任何不能再编译时被解析称为一个常量的字段初始化语句,接口就会有一个<clinit>()方法。
主动使用和被动使用:前面讲过,Java虚拟机在首次主动使用类型时初始化他们。只有6种情况被认为是主动使用:创建类的新实例,调用类中声明的静态方法,操作类或者接口中声明的非常量静态字段,调用Java API中特定的反射方法,初始化一个类的子类,已经指定一个类作为Java虚拟机启动时的初始化类。
使用一个非常量的静态字段只有当类或者接口的确声明了这个字段时才是主动使用。比如,类中声明的字段可能会被子类引用;接口中声明的字段可能会被子接口或者实现了这个接口的类引用。对于子类,子接口,实现类来说,这就是被动引用,也就是说被动引用并不会触发他们的初始化。
1 class Test_0{ 2 static String str_0 = "test_0"; 3 4 static { 5 System.out.println("init Test_0"); 6 } 7 } 8 9 class Test_1 extends Test_0{ 10 11 static { 12 System.out.println("init Test_1"); 13 } 14 } 15 16 public class Test { 17 static { 18 System.out.println("init Test"); 19 } 20 21 public static void main(String[] args) { 22 String sout = Test_1.str_0; 23 System.out.println(sout); 24 } 25 }
输出:
init Test
init Test_0
test_0
可见Test_1并没有被初始化。
如果说一个字段既是静态的(static)又是最终的(final),并且使用一个编译时常量表达式初始化,使用这样的字段,就不是对声明该字段的类主动引用。Java编译器把这样的字段解析成本地拷贝(存于引用者类的常量池中或者字节码流中,或者二者都有)。
垃圾收集和对象的终结:Java虚拟机必须实现具有某种自动堆存储管理策略------大部分是采用垃圾收集器。程序可以明确或隐含的为对象分配内存,但是却不能明确的释放内存,当一个对象不再为程序使用,虚拟机必须回收那部分内存。实现可以决定何时垃圾收集不再被引用的对象,或者选择不收集它们。
如果类声明了finalize(),垃圾收集器会在释放这个实例所占据的内存空间之前执行这个方法,因为一个终结方法是一个普通方法,它可以被程序所调用。垃圾收集器(最多)只会调用一个对象的终结方法一次,如果终结方法代码执行后,对象被重新引用,随后再次变为不被引用,垃圾收集器不会第二次调用终结方法。
卸载类型
Java虚拟机通过什么方法来确定一个动态装载的类型是否任何被程序需要呢,其判断方式与判断对象是否仍然被程序需要的方式很类型。如果程序不再引用某类型,那么这个类型就无法再对未来计算过程产生影响。类型变成不可触及的,而且可以被垃圾回收。
判断动态装载类型的Class实例在正常的垃圾回收过程中是否可以触及有两种方式。第一种,如果程序保持对Class实例类型的明确引用,它就是可触及的。其次如果在堆中还存在一个可触及的对象,在方法区中它的类型数据指向一个Class实例,那么这个Class实例就是可触及的。仅仅给出一个对象的引用,实现必须能够在方法区找到对象的类的类型数据。因此,通过类型数据,虚拟机可以确定对象的类,已经它的所有超类已经所有超接口的Class实例。
上图表示仅通过MyThread的实例,垃圾收集器可以“触及”MyThread和它的所有超类型(包括Cloneable、Thread、Runnble和Object)的Class实例。
参考:深入理解Java虚拟机第二版