概述
这一章以一个Java类型(类或接口)的生命周期为例来讨论开始阶段的装载、连接和初始化,以及占Java类型生命周期绝大部分时间的对象实例化、垃圾收集和对象终结,然后是java类型生命周期的结束,也就是从虚拟机中卸载。
7.1、类型装载、连接与初始化
Java虚拟机通过装载、连接和初始化一个Java类型,使该类型可以被正在运行的java程序所使用。其中,类装载就是把二进制形式的Java类型读入Java虚拟机中;而连接就把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接阶段分为三个子步骤——验证、准备和解析。“验证”步骤确保了Java类型数据格式正确并且适于Java虚拟机使用。“准备”步骤则负责为该类型分配它所需的内存,比如为它的类变量分配内存。“解析”步骤则负责把常量池中的符号引用转换为直接引用。虚拟机的实现可以推迟解析这一步,它可以在当运行中的程序真正使用某个符号引用时再去解析它。之后,进行初始化,在初始化期间,都将给类变量赋以适当的初始值。
在类和接口被装载和连接的时机上,java虚拟机规范没有严格的规定,但是它严格的定义了初始化的时机,所有的Java虚拟机实现必须在每个类或接口首次主动使用时初始化。下面六种情况符合主动使用的要求:
1)、当创建某个类的实例时(通过new指令或通过不明确的创建、反射、克隆、反序列化)
2)、当调用某个类的静态方法时
3)、当使用某个类或接口的静态字段,或者对该字段赋值时。(final修饰的静态字段除外,它被初始化为一个编译时的常量表达式)
4)、当调用Java API中的某些反射方法时,比如类Class中的方法或者java.lang.reflect包中的类的方法
5)、当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)
6)、当虚拟机启动时某个被标明为启动类(即含有main()方法的那个类)
注:任何一个类的初始化都要求它的所有祖先类预先初始化,而一个接口的初始化,并不要求它的祖先接口预先被初始化。也就是说只有某个接口所声明的非常量字段被使用时,该接口才会被初始化,而不会因为实现这个接口的子接口或类要初始化而被初始化。但是当实现了父接口的子类(或扩展了父接口的子接口)被装载时,父接口也必须被装载。
7.1.1装载
装载阶段有三个基本动作组成,要装载一个类型,Java虚拟机必须:
1)、通过该类型的完全限定名,产生一个代表该类型的二进制数据流。
2)、解析这个二进制数据流为方法区内的内部数据结构。
3)、创建一个表示该类型的java.lang.Class类型的实例 (装载步骤最终的产品就是这个Class类的实例对象,它成为程序与内部数据结构之间的接口,要访问关于该类型的信息,程序就要调用该类型对应的Class实例对象的方法)。
7.1.2验证
装载后就要进行连接了,连接过程的第一步是验证——确认类型符合Java语言的语义,并
且它不会危及虚拟要的完整性。
其实有一些特定的检查并不发生在验证这一步,如:1)、在装载过程中,虚拟机大多会检查
二进制数据以确保数据全部都是预期的格式(符合java class文件格式);2)、在装载时,还
要确保除了Object类之外的每一个类都有一个超类,因为装载一个类的时候必须确保该类
的所有超类都已经被装载了。3)、还有一种检查往往发生在验证阶段之后,那就是符号引用
的验证,当虚拟机搜寻一个被符号引用的元素(类型、字段和方法)时,它必须首先确认该
元素存在,如果虚拟机发现元素存在,它必须进一步检查引用类型有访问该元素的权限。
那么验证阶段做哪些检查呢,如下:
1、 确保各个类之间二进制兼容的检查
1)、检查final的类不能拥有子类。
2)、检查final的方法不能被覆盖
3)、确保在类型和超类型之间没有不兼容的方法声明(例如两个方法拥有同样的名字,参数在数量和顺序类型上都相同,但是返回类型不同)
2、检查所有的常量池入口相互之间一致
3、检查常量池中的所有的特殊字符串(类名、字段名、和方法名、字段描述符和方法描述符是否符合格式)
4、检查字符字节码的完整性(这是最复杂的任务,所有java虚拟机都必须设法为它们执行的每个方法检验字节码的完整性)
7.1.3准备
在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值。但到达初始化阶段之前,类变量都没有被初始化为真正的初始值,在准备阶段是不会执行java代码的。
Char类型默认为’\u0000’,byte默认为(byte)0,boolean默认为0,float默认为0.0f,double默认为0.0d,long默认为0L。
7.1.4 解析
解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。
7.1.5 初始化
在java代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化语句给出的。
所有的类变量初始化语句和类型的静态初始化器都被java编译器收集在一起,放到一个特殊的方法中,称为类初始化方法。在类和接口的Java class文件中,这个方法被称为”<clinit>”。通常的Java程序方法是无法调用这个<clinit>方法的,只能被java虚拟机调用。
初始化一个类包含两个步骤:
1)、如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。
2)、如果类存在一个类初始化方法(<clinit>)就执行此方法。
注:初始化一个接口只需一步:如果接口存在一个接口初始化方法的话,就执行此方法。
初始化的顺序按照类变量初始化语句和静态初始化语句出现的顺序初始化。
并不是所有的类都需要在它的class文件中有一个<clinit>()方法。如果类没有声明任何类变量,也没有静态初始化语句,那么它就不会有<clinit>()方法。如果类声明了类变量,但是没有明确使用类变量初始化语句或者静态初始化语句初始化它们,那么类也不会有<clinit>()方法。
所有在接口中声明的隐式公开(publict)、静态(static)、最终(final)字段必须在字段初始化语句中初始化,如果接口包含任何不能在编译时被解析成为一个常量的字段初始化语句,接口就拥有一个<clinit>()方法。例如:
interface Example{
intketchup = 5;
int mustard= (int) (Math.random()*5.0);
}
ketchup将会被初始化为一个编译时常量,而mustard字段被<clinit>()方法初始化。
主动使用与被动使用
在第六章中曾提到主动使用类类型的6种情况,他们会引发初始化。当使用一个非常量的静态字段时,只有这个字段是被当前类或接口声明的情况下才是主动使用,如果是子类使用父类中声明的字段,子接口和实现了该接口的类使用此接口中的字段都被认为是被动使用,不会引发初始发,例如:
public class NewParent {
static int hoursOfsleep = (int)(Math.random()*3.0);
static{
System.out.println("NewParentwas initialized");
}
}
public class NewbornBaby extends NewParent {
static int housOfCrying = 6+(int)(Math.random()*2.0);
static{
System.out.println("NewbornBabywas initialized.");
}
}
public class Example {
static{
System.out.println("Example wasinitialized.");
}
public static void main(String[] args) {
int hours = NewbornBaby.hoursOfsleep;
System.out.println(hours);
}
}
运行结果:
Example was initialized.
NewParent was initialized
2
7.2 对象的生命周期
一旦一个类被装载、连接和初始化,它就随时可以使用了。程序可以访问它的静态字段,调用它的静态方法,或者创建它的实例。
7.2.1类实例化
实例化一个类有四种途径:明确地使用new操作符;调用Class或者java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;或者通过java.io.ObjectInputStream类的getObject()方法反序列化。除了明确地实例化对象之外,还有几种情况下会隐含地实例化,例如:1)在任何java程序中第一个隐含实例化对象可能就是保存命令行参数的String对象。2)对于java虚拟机装载的每一个类型,它会暗中实例化一个Class对象来代表这个类型。3)当Java虚拟机装载了在常量池中包含CONSTANT_String_info入口的类的时候,它会创建新的String对象的实例来表示这些常量字符串。4)另一种隐含创建对象的途径是通过执行包含字符串连接操作符的表达式产生对象。
当Java虚拟机创建一个类的实例时,首先都需要在堆中为保存对象的实例变量分配内存(所有在对象的类中和它的超类中声明的变量都要分配内存)。准备好内存后,它立即把实例变量初始化为默认的初始值。一旦虚拟机完成了新对象的分配内存和为实例变量赋默认初始值后,它随后就会为实例变量赋正确的初始值,这一步会有三种情况:1)如果对象是通过clone()调用来创建的,虚拟机把原来被克隆的实例变量中的值拷贝到新对象中。2)如果对象是调用一个ObjectInputStream的readObject()调用反序列化的,虚拟机通过从输入流中读入的值来初始化那些非暂时性的实例变量。3)虚拟机调用对象的实例初始化方法。
Java编译器为它编译的每一个类都至少生成一个实例初始化方法。在Java的class文件中,这个实例初始化方法称为”<init>”。一个<init>()方法中可能包含三种代码:调用另一个<init>()方法;实现对任何实例变量的初始化;构造方法体的代码。
一个类的构造方法有如下几种情况:
1、 如果构造方法通过明确地调用同一个类中的另一个构造方法(一个this()调用)开始,
它对应的<init>()方法由两部分组成:
1)、一个同类的<init>()方法的调用
2)、实现了对应构造方法的方法体的字节码
2、 如果构造方法不是通过一个this()调用开始的,而且这个对象不是Object,<init>()方法则由三部分组成:
1)、一个超类的<init>()方法的调用
2)、任意实例变量初始化方法的字节码
3)、实现了对应构造方法的方法体的字节码
3、如果构造方法没有使用一个this()调用开始,而且这个对象是Object,则上面列表中的第一个元素就不存在,因为Object没有超类。
如果构造方法通过明确的调用超类的构造方法(一个super()调用)开始,它的<init>()方法会调用对应的超类的<init>()方法。如果构造方法没有明确地从this()或者super()调用开始,对应的<init>()方法默认会调用超类的无参数<init>()方法。
7.22 垃圾收集与对象终结
当一个对象不再为程序所引用了,虚拟机必须回收那部分内存。
7.3 卸载类型
Java虚拟机中类的生命周期和对象的生命周期很相似,虚拟机装载、连接并初始化类,使程序能使用类,当程序不在引用它们的时候可卸载它们。
类的垃圾收集和卸载之所以在Java虚拟机中很重要,是因为Java程序可以在运行时通过用户自定义的类装载器装载类型来动态扩展程序。如果程序持续通过用户自定义的类装载器装载类型,方法区的内存就会不断增长,如果不进行垃圾收集,内存可能被占满。
使用启动类装载器装载的类型永远是可触及的,所以永远不会被卸载。只有使用用户定义的类装载器装载的类型才会变成不可触及的,从而被虚拟机回收。
判断动态装载的类型的Class实例在正常的垃圾收集过程中是否可以触及有两种方式:1)如果程序保持对Class实例的明确引用,它就是可触及的。2)如果在堆中还存在一个可触及的对象,在方法区中它的类型数据指向一个Class实例,那么这个Class实例就是可触及的。
通过类MyThread的实例,垃圾收集器可以“触及”MyThread和它的所有超类型(Cloneable、Thread、Runnable、Object)的class实例。