Java虚拟机通过加载【loading】、链接【linking】和初始化【initialization】的过程,使得某个类型对当前正在取运行的程序可用。
链接分为三个子步骤:
具体的实现可能会延迟解析【Resolution 】步骤,直到正在运行的程序实际使用了某个符号引用。验证【Verification 】、准备【Preparation 】和(可选)解析【Resolution 】完成后,类型就可以进行初始化了。
Java虚拟机规范在类和接口加载【【loading】】和链接【linking】的时机上为实现提供了灵活性,但是严格定义了初始化的时间。任何JVM实现必须在每个类或接口首次主动使用时初始化,下面是6种主动使用的时机:
除了这里列出的6种主动使用途径之外,类型的所有其他使用途径都是被动使用,它们不会触发类型的初始化。
在首次主动使用时,类型必须初始化。然而,在初始化它之前,它必须被链接。在它被链接之前,它必须被加载。
类的初始化需要预先初始化它的超类,且递归。但是接口不同,只有某个接口所声明的非常量字段被使用时,该接口才会被初始化,而不会因为该接口的子接口或者子实现类被初始化而初始化。
因此,类的初始化要求其祖先类(不包括祖先接口)被初始化,而接口的初始化,并不要求其祖先接口被初始化。
加载过程包括三个基本活动。要加载类型,Java虚拟机必须:
Java虚拟机规范没有指定必须如何生成类型的二进制数据。为一个类型生成二进制数据的一些可能的方法是:
有了类型的二进制数据,Java虚拟机必须对其进行充分的处理,最终可以为其创建Class实例。虚拟机必须可以将二进制数据解析为依赖于实现的内部数据结构(见JVM虚拟机博客中,有关存储类数据的潜在内部数据结构的讨论)。
Class实例是加载【Loading】步骤的最终产物,它充当应用程序和内部数据结构之间的接口。要访问关于存储在内部数据结构中的类型的信息,程序将调用该类型的Class实例的对应方法。将类型的二进制数据解析为方法区中的内部数据结构,并在堆上实例化Class对象的过程称为创建【creating 】类型。
类型的加载时通过类加载器实现的~~
类型被加载【loading】后,就可以连接【linking】它了。连接【linking】它过程的第一步是验证【Verification】——确保类型符合Java语言的语义,并且不会破坏虚拟机的完整性。
验证并非严格的限制在加载【loading】之后,准备【Preparation】之前。在大多数Java虚拟机实现中,某些特定类型的检查一般都在特定的时机发生,比如:
那么在正式的验证【Verification】阶段会检查什么呢?即在正式的验证【Verification】阶段之前没有被检查过的,且在之后也不会被检查的:
在准备【Preparation】阶段,Java虚拟机为类变量【Class Variables ,即静态变量】分配内存,并将它们设置为默认的初始值。在初始化阶段之前,不会将类变量初始化为真正的初始值(在准备阶段并不会执行任何Java代码)。在准备期间,Java虚拟机将类变量新分配的内存设置为由变量类型决定的默认值。
PS:虚拟机对boolean的支持很少,故在北部,它是以int类型实现的,默认值为0,亦FALSE。
在准备阶段,Java虚拟机实现还可能为某些特殊的数据结构分配内存,以提高正在运行的程序的性能。一个这种特殊的数据结构的例子是:方法表,它包含指向类中每个方法的指针,包括那些继承自其超类的方法。方法表允许在对象上调用继承的方法时,无需在调用时搜索超类。
解析是从类型的常量池中定位类、接口、字段和方法的符号引用,并将这些符号引用替换为直接引用的过程。
类或者接口只有在被初始化后,也就是为类变量赋予正确的初始值,才类或接口的首次主动使用做好了准备。
在Java代码中,通过类变量初始化语句或静态初始化语句,可以为类变量指定适当的初始值。
Java编译器会收集类型的所有类变量初始化语句和静态初始化语句,并将它们放入一个特殊的方法中。对于类,此方法称为类初始化方法;对于接口,则称为接口初始化方法。在类和接口的Java class文件中,这个方法被命名为"< clinit >",该方法无法被Java程序调用,它仅可以被虚拟机调用,专用于将类变量初始化为正确的初始化值。
public class Initialization {
// 类变量初始化语句
public static String name1 = "piemon";
public static String name2;
// 静态初始化语句
static {
name2 = "anokata";
}
public static void main(String[] args) {
System.out.println("echo");
}
}
<clinit>字节码:
static {};
Code:
0: ldc #5 // String piemon
2: putstatic #6 // Field name1:Ljava/lang/String;
5: ldc #7 // String anokata
7: putstatic #8 // Field name2:Ljava/lang/String;
10: return
类的初始化由两个步骤组成:
public interface InitializationInterface {
int month = 10;
int day = (int)(Math.random() * 2.0);
}
<clinit>字节码:
public interface InitializationInterface {
public static final int month;
public static final int day;
// 注意:该方法中,只存在对day变量的初始化字节码
static {};
Code:
0: invokestatic #1 // Method java/lang/Math.random:()D
3: ldc2_w #2 // double 2.0d
6: dmul
7: d2i
8: putstatic #4 // Field day:I
11: return
}
public interface InitializationInterface2 {
int month = 10;
int day = 16; //day的声明可以在编译时解析为常量
}
public interface InitializationInterface2 {
public static final int month;
public static final int day;
}
接口中声明的字段,都是隐式由public static final修饰的,且必须使用字段初始化语句进行初始化。
如果接口存在任何一个无法在编译时解析为常量的字段,那么他必然存在< clinit >方法,如例子中的day字段。
PS:Java规范定义了常量表达式的涉猎操作符,比如+、*、/等等,这些操作符会在编译时直接计算。
接口的初始化比较特殊,只有一个步骤:
PS:子类的< clinit >并不会显式的调用父类的< clinit >方法。在虚拟机调用< clinit >方法之前,虚拟机必须保证父类的< clinit >已经被执行了。
Java虚拟必须保证多个线程初始化同一个类时,有且仅有一个线程会执行< clinit >方法,其他线程wait,当执行< clinit >的线程完成了初始化,他必须通知这些等待的线程。
Java虚拟机在首次主动使用某类型时时初始化它们。
使用一个非常量的静态字段时,只有当类或者接口中的确声明了这个字段时,才算做主动使用,比如:
//接口
public interface Face {
String face = "face";
}
//父类
public class PClazz {
static String pclazz = "pclazz";
static {
System.out.println("PClazz ...");
}
}
// 子类、子实现
public class Clazz extends PClazz implements Face {
static String clazz = "clazz";
static {
System.out.println("Clazz ...");
}
}
// 测试类
测试程序:
public static void main(String[] args) {
System.out.println(Clazz.face);
//loging
//face
}
public static void main(String[] args) {
System.out.println(Clazz.pclazz);
//loging
//PClazz ...
//pclazz
}
public static void main(String[] args) {
System.out.println(Clazz.clazz);
//PClazz ...
//Clazz ...
//clazz
}
如果一个字段是由static final修饰的,且使用一个编译时常量表达式初始化,那么使用这样的字段,就不是对声明该字段的类的主动使用。Java编译器会将这样的字段解析成对常量的本地拷贝(该常量存在于引用者类的常量池中或字节码流中,或二者兼有)
加载、链接和初始化类之后,就可以使用它了。程序可以访问它的静态字段,调用它的静态方法,或者创建它的实例。
实例化类有4种显式途径:
实例化类的隐式图途径:
当Java虚拟机隐式或显式地创建类的新实例时,它首先在堆上分配内存来容纳对象的实例变量。所有在对象的类及其超类中声明的变量,包括隐藏的实例变量,都需要分配内存。
一旦虚拟机为新对象准备好了堆内存,它就立即将实例变量初始化为默认的初始值。
一旦虚拟机为新对象分配了内存并将实例变量初始化为默认值,它就可以为实例变量赋值正确的初始值了。根据创建对象的方式,Java虚拟机使用三种技术之一来完成此任务:
Java编译器为它编译的每个类至少生成一个实例初始化方法。在Java类文件中实例初始化方法名为“< init >”。对于类的源代码中的每个构造函数,Java编译器都会为其生成一个< init >()方法。如果类没有显式声明构造器,则编译器会主动生成一个默认的无参数构造函数,该构造函数只调用超类的无参数构造函数。
如果一个类声明了一个名为finalize()的返回void的方法,垃圾收集器会在释放这个实例所占的内存之前执行该方法一次。
finalize()是一个普通方法,可以被应用程序调用。但是垃圾收集器只会调用一次!!
如果终结方法调用后,对象引用又被激活,一段时间后又重新变得不被引用,那么垃圾收集器不会再第二次调用finalize()