第7章 类型的生命周期
java虚拟机通过装载、连接和初始化一个java类型,使该类型可以被正在运行的java程序所使用。
装载:是把二进制形式的java类型读入java虚拟机中。
连接:是把读入的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接分三个子步骤(验证、准
备和解析)。验证步骤确保java类型数据格式正确并且适合于java虚拟机使用;准备步骤则负责为
该类型分配它所需的内存,并将类变量设置为默认值;解析步骤负责把常量池中的符号引用转换为
直接引用。虚拟机的实现可以推迟解析这一步,它可以在程序真正使用某个符号引用时再去解析
它。
初始化:当验证、准备和解析(可选)步骤都完成时,该类型就已经为初始化做好了准备,在初始化期
间,将给类变量赋以适当的初始值。
初始化时机:所有的虚拟机实现必须在每个类或接口首次主动使用时初始化。
符合主动使用的情形:
(1) 当创建某个类的新实例时(在字节码中执行new指令,不明确的创建:反射、clone和反序列化)。
(2) 当调用某个类的静态方法时(在字节码中执行invokestatic指令)。
(3) 当使用某个类或接口的静态字段,或者对该字段赋值时(在字节码中执行getstatic或putstatic),用
final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。
(4) 当调用java API中的某些反射方法时,比如Class类中的方法或者java.lang.reflect包中的类的方法。
(5) 当初始化某个类的子类时(类初始化时,要求它的超类已经被初始化)。
(6) 当虚拟机启动时某个被标明为启动类的类(含有main方法的那个类)。
除了这6种情形外,所有其他使用java类型的方式都是被动使用,它们不会导致java类型的初始化。
任何一个类的初始化都要求它的超类在此之前已经完成初始化;然而,对于接口来说,这条规则并不适用。只有在某个接口所声明的非常量字段被使用时,该接口才会被初始化,而不会因为实现这个接口的子接口或类要初始化而被初始化。而且一个接口的初始化,并不要求它的祖先接口预先初始化。
装载阶段:由三个基本动作组成,要装载一个类型,java虚拟机必须:
(1) 通过该类型的完全限定名,产生一个代表该类型的二进制数据流。
(2) 解析这个二进制数据流为方法区的内部数据结构(虚拟机实现相关的内部数据结构)。
(3) 在堆中创建一个表示该类型的java.lang.Class类的实例。
验证阶段:连接过程的第一步是验证,确认类型符合java语言的语义,并且它不会危及虚拟机的完整性。
准备阶段:在准备阶段,java虚拟机为类变量分配内存,设置默认初始值;但在到达初始化阶段之前,类
变量都没有被初始化为真正的初始值(准备阶段不会执行java代码)。在准备阶段,java虚拟
机实现也可能为一些数据结构分配内存,比如方法表。
解析阶段:解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换
为直接引用的过程。
初始化:通过类变量初始化语句或静态初始化语句为类变量赋予正确的初始值。java源码中所有的类变量
初始化语句和静态初始化块都被java编译器收集到一起,放到一个特殊的方法中。对于类来说,
这个方法被称作类初始化方法;对于接口来说,它被称为接口初始化方法。在类和接口的class文
件中,这个方法被称为<clinit>。java程序是无法调用这个方法的,它只能被虚拟机调用,专门用
于为类型的静态变量设置正确的初始值。
java虚拟机必须确保初始化过程被正确地同步。如果多个线程需要初始化一个类,仅仅允许一个线程来进行初始化,其他线程需要等待。当活动的线程完成了初始化过程之后,它必须通知其他等待的线程。
初始化步骤:
(1) 如果类存在直接超类,且直接超类还没有被初始化,就先进行直接超类的初始化(第一个被初始化
的类永远是Objec)。初始化接口并不需要初始化它的父接口。
(2) 如果类或接口存在一个初始化方法,就执行此方法(<clinit>)。
<clinit>()方法的代码并不显式地调用超类的<clinit>()方法。在java虚拟机调用类的<clinit>()方法之前,它必须确认超类的<clinit>()方法已经被执行了。
并非所有的类都需要在它们的class文件中拥有一个<clinit>()方法。如果类没有声明任何类变量,也没有静态初始化语句,那么它就不会有<clinit>()方法。如果类声明了类变量,但是没有明确使用类变量初始化语句或者静态初始化语句,那么类也不会有<clinit>()方法。如果类仅包含static final变量的初始化语句,而且这些类变量的初始化语句采用编译时常量表达式,类也不会有<clinit>()方法。只有那些的确需要执行java代码来赋予类变量正确初始值的类才会有类初始化<clinit>()方法。
所有在接口中声明的隐式 public static final字段都必须在字段初始化语句中初始化。如果接口包含任何不能在编译时被解析成一个常量的字段初始化语句,接口就会拥有一个<clinit>()方法。
主动使用和被动使用:使用一个非常量的静态字段,只有当类或者接口的确声明了这个字段是才是主动使用。比如,类中声明的静态字段可能会被子类引用,对于子类来说就是被动使用,使用它们不会触发子类的初始化。
如果一个字段是static final的,并且使用一个编译时常量表达式初始化,使用这样的字段就不是对声明该字段的类的主动使用。java编译器把这样的字段解析成对常量的本地拷贝(该常量存在于引用者类的常量池或者字节码流中)。
public class PassiveUse {
public static void main(String[] args) {
System.out.println(NewBaby.hoursOfSleep);
System.out.println(NewBaby.greeting);
}
static {
System.out.println("PassiveUse is initialized");
}
}
class NewParent {
static int hoursOfSleep = (int) (Math.random() * 3.0);
static {
System.out.println("NewParent is initialized");
}
}
class NewBaby extends NewParent {
static final String greeting = "hello world";
static int hoursOfCry = (int) (Math.random() * 2.0);
static {
System.out.println("NewBaby is initialized");
}
}
输出为:
PassiveUse is initialized
NewParent is initialized
1
hello world
类实例化:类可以被明确或者隐含地实例化,实例化一个类有四种途径:
(1) 明确地使用new操作符。
(2) 调用Class或者java.lang.reflect.Construtor对象的newInstance()方法。
(3) 调用任何现有对象的clone()方法。
(4) 通过java.io.ObjectInputStream类的getObject()方法反序列化。
@SuppressWarnings("unused")
public class ClassInstantiation {
public static void main(String[] args)
throws ClassNotFoundException, InstantiationException,
IllegalAccessException, CloneNotSupportedException {
Example obj1 = new Example("new");
Class<Example> myClass = Example.class;
Example obj2 = myClass.newInstance();
Example obj3 = (Example) obj1.clone();
}
}
@SuppressWarnings("all")
class Example implements Cloneable {
Example() {
System.out.println("create by newInstance()");
}
Example(String msg) {
System.out.println("create by " + msg);
}
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
输出为:
create by new
create by newInstance()
当java虚拟机创建一个类的新实例时,首先都需要在堆中为保存对象的实例变量分配内存。所以在对象的类中和它的超类中声明的变量(包括隐藏的实例变量)都要分配内存。一旦虚拟机为新的对象准备好了堆内存,它立即把实例变量初始化为默认的初始值。随后虚拟机就会为实例变量赋正确的初始值。
根据创建对象的方法不同,java虚拟机使用三种技术之一来完成这个工作。
如果对象通过clone()调用创建的,虚拟机把原来被克隆的实例变量中的值拷贝到新对象中。
如果对象是通过readObject()调用反序列化的,虚拟机通过从输入流中读入的值来初始化那些非transient类型的实例变量。
否则,虚拟机调用对象的实例初始化方法<init>。
java编译器为它编译的每一个类都至少生成一个实例初始化方法。在class文件中这个实例初始化方法被称为<init>。针对源码中类的每一个构造方法,java编译器都产生一个<init>()方法。如果类没有明确声明任何构造方法,编译器默认产生一个无参数的构造方法,它仅仅调用超类的无参构造方法;编译器在class文件中创建一个<init>()方法,对应它的默认构造方法。
如果构造方法中通过明确的this()调用另一个构造方法。它对应的<init>()方法由两部分组成:一个同类与this()参数相同的<init>()方法的调用;实现了对应构造方法的方法体的字节码。
如果构造方法不是通过this()调用开始的(代码第一行不是this()),而且这个类型不是Object,<init>()方法则由三部分组成:一个超类的无参<init>()方法调用(如果是Object,这项不存在);任意实例变量初始化语句的字节码;实现了对应构造方法的方法体的字节码。
如果构造方法通过明确的super()开始,它的<init>()方法会调用对应参数类型的超类<init>()方法;任意实例变量初始化语句直接码;实现了对应构造方法的的方法体的字节码。
对于除Object外的每一个类,<init>()方法都必须从另一个<init>()方法调用开始。<init>()方法不允许捕捉由它们所调用的<init>()方法抛出的任何异常;如果超类的<init>()方法被意外中止了,那么子类的<init>()方法也必须同样被意外中止。
卸载类型:java虚拟机判断一个动态装载的类型是否仍然被程序需要,其方式与判断对象是否仍然被程序需要的方式很类似。如果程序不再引用某个类型,那么这个类型就无法在对未来的计算过程产生影响。类型编程不可触及的,而且可以被垃圾收集。
判断动态装载的类型的Class实例在正常的垃圾收集过程中是否可触及有两种方式:
(1) 如果程序保存对Class实例的明确引用,它就是可触及的。
(2) 如果在堆中还存在一个可触及的对象,在方法区中它的类型数据指向一个Class实例,那么这个Class实
例就是可触及的。