Java虚拟机中程序执行过程

类加载过程

引文

  在Java语言里,new表达式是负责创建实例的,调用构造器去对实例做初始化;构造器自身的返回值类型是void,并不是“构造器返回了新创建的对象的引用”,而是new表达式的值是新创建的对象的引用。

  对应的,在JVM里,“new”字节码指令只负责把实例创建出来(包括分配空间、设定类型、所有字段设置默认值等工作),并且把指向新创建对象的引用压到操作数栈顶。此时该引用还不能直接使用,处于未初始化状态(uninitialized);如果某方法a含有代码试图通过未初始化状态的引用来调用任何实例方法,那么方法a会通不过JVM的字节码校验,从而被JVM拒绝执行。

  能对未初始化状态的引用做的唯一一种事情就是通过它调用实例构造器,在Class文件层面表现为特殊初始化方法“< init >”。实际调用的指令是invokespecial,而在实际调用前要把需要的参数按顺序压到操作数栈上。在上面的字节码例子中,压参数的指令包括dup和ldc两条,分别把隐藏参数(新创建的实例的引用,对于实例构造器来说就是“this”)与显式声明的第一个实际参数(”ab”常量的引用)压到操作数栈上。
在构造器返回之后,新创建的实例的引用就可以正常使用了。

举例分析

  假设我们要运行Test类,首先虚拟机启动,尝试执行main方法发现类还未加载,即虚拟机还未包含Test类的二进制表示。加载过程通过引导类加载器查找.class并加载Test类。

  Test类加载后,在调用main之前需要先进行初始化。所有的类或接口在初始化前要先进行链接。链接包括校验、准备和可选的解析。

  校验阶段会检查被加载的Test类是否良构,是否具有正确的符号表,代码是否遵循Java编程语言和虚拟机的语义要求。

  准备阶段涉及到静态存储的内存分配,以及所有在Java虚拟机的实现内部需要使用的数据结构的内存分配,比如方法表。

  解析是检查Test中对其他类和接口的符号引用的过程,通过加载提及的其他类和接口来检查这些引用是否正确。可以选择非常早的解析也可以选择在符号引用被实际使用时再解析。

  接着进入初始化过程,初始化顺序有所有的类变量初始化器和静态初始化器按行文顺序构成。要求直接超类的初始化顺序必须先于子类。

  最后,完成初始化后Test类的main方法才会被调用。要求main函数必须声明为:public static void,且必须指定类型为String数组的形式参数。

1
2
public static void main(String[] args)
public static void main(String... args)

第一节 虚拟机启动

  虚拟机的启动是通过加载指定的类,然后调用指定类中main方法。

  Java虚拟机会动态的加载、链接与初始化类和接口。

  • 加载是根据特定名称查找类或接口类型的二进制表示(.class),并由二进制表示来创建类或接口的过程。
  • 链接是为了让类或接口可以被Java虚拟机执行,而将类或接口并入虚拟机运行时状态的过程。
  • 类或接口的初始化是指执行类或接口的初始化方法< clinit >

第二节 加载过程

  加载是指查找到具有特定名的类或接口类型的二进制形式的过程。

  典型实现:获取之前由Java编译器对源代码进行计算产生的二进制表示,从该二进制形式中构建表示该类或接口的Class对象。二进制格式通常是.class文件格式,但也可以是其他满足虚拟机要求的格式。ClassLoader类的defineClass方法可以被用来从class文件格式的二进制表示中构建Class对象。

  加载过程是由ClassLoader类以及其子类实现的。不同子类可以实现不同的加载策略,类加载器可以缓存类或接口的二进制表示、基于预期使用而预抓取它们,以及一起加载有关联的一组类。

  类加载过程产生了错误,会抛出LinkageError子类(ClassCircularityError,ClassFormatError,NotClassDefFoundError)之一的异常。因为加载涉及到对新数据结构分配内存,所以有可能会抛出OutOfMemoryError。

  创建一个标记为N的类或接口C,首先虚拟机要在方法区上为C创建与虚拟机中实现匹配的内部表示,C的创建可以由另一个类或接口D触发,其运行时常量池引用了C,也可以由D调用反射等方法触发。(数组类直接由Java虚拟机创建,其没有外部的.class二进制表示,非数组类则由类加载器加载.class来创建)

  虚拟机首先检查加载器L是否被记录为N的初始加载器,如果是则这次尝试创建操作无效,且加载动作会抛出异常LinkageError;如果不是,虚拟机尝试解析.class文件。解析阶段的异常先省略,解析C的直接父类或直接父接口省略。虚拟机标记C的定义类加载器为L,并记录L是C的初始加载器。


第三节 链接过程

  链接是指获取类或接口类型的二进制形式,并将其与Java虚拟机的运行时状态结合起来,使其可以被执行的过程。

链接涉及三种不同的行为:

  • 校验
  • 准备
  • 符号引用的解析

  因为链接涉及到对新数据结构分配内存,所以有可能会抛出OutOfMemoryError。

  链接包括验证和准备类或接口、其直接父类、其直接父接口、其元素类型。解析这个类或接口中的符号引用是链接过程的可选部分,虚拟机可以选择在用到符号引用时再去解析(延迟解析),或者验证类时就解析每个引用(预先解析)。要求类或接口链接前必须被成功的加载过,初始化之前必须被成功的验证和准备过。

校验

  校验可以保证类或接口的二进制表示在结构上正确。如果校验过程发生错误,会抛出LinkageError的子类VerifyError的异常。

  验证过程可能会使其他类或接口被加载,但未必需要验证或准备他们。

准备

  准备包括创建类或接口的static域(类变量和类常量)和将这些域初始化为缺省值,不需要执行任何源代码,或者说虚拟机字节码指令,静态域的显式初始化器会作为初始化过程的一部分而执行,而不是准备过程的一部分。

Java虚拟机的实现可以在准备阶段预计算额外的数据结构,以使后续对此类或接口的操作更加高效,特别有用的“方法表”或其他具有相同功能的数据结构,允许在类的实例上调用任何方法而无需在调用时搜索其超类。

解析

  类或接口的二进制表示会使用其他类或接口的二进制名字来以符号方式引用其他类或接口,以及它们的域、方法和构造器。这些符号引用包括域和方法所属的类或接口类型的名字、域和方法自身的名字,以及恰当的类型信息。

符号引用:一组符号描述锁引用的目标,如CONSTANT_XX_info等常量。其引用目标未必加载到内存中。在编译阶段时,类的实际地址还未可知,所以只能用符号引用来代替。

直接引用:可以是直接指向目标的指针,相对偏移量,一个能间接定位到目标的句柄。有了直接引用目标一定已经加载到内存了。

  在符号引用可以被使用前,它们必须先被解析,在解析时会检查符号引用是否正确,并且在典型情况下,若某个符号引用被重复的使用,那么它会被可以更高效处理的直接引用所替代。

  如果在解析过程中发生错误,会抛出IncompatibleClassChangeError或其子类(IllegalAccessError,InstantiationError,NoSuchFieldError,NoSuchMethodError)之一。如果一个类声明了一个native方法,但找不到此方法的实现,就会抛出LinkageError的子类UnsatisfiedLinkError。

  解析是根据运行时常量池里的符号引用来动态决定具体指的过程,Java虚拟机通过一些指令将符号引用指向运行时常量池,执行指令(anewarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfield,putstatic)需要对符号引用进行解析。对于除invokedynamic以外的指令,碰到此指令并解析它的符号引用后,表示对其他指令相同的符号引用已被解析过了。各种符号引用的解析过程先省略。


第四节 初始化过程

  类的初始化包括执行其静态初始化器和执行用于在类中声明的static域(类变量)的初始化器。接口的初始化包括执行用于在接口中声明的域(常量)的初始化器。在类被初始化之前,其直接超类必须先被初始化,但该类实现的接口并没有初始化,类似的,接口的超接口在该接口被初始化之前,还没有被初始化。

  

  初始化对于类或接口就是执行其初始化方法,只有以下指令会导致初始化:new,getstatic,putstatic,invokestatic。这些指令都会通过字段或方法引用来直接或间接引用某个类。执行new指令时,若指令引用的类或接口没初始化则进行初始化。在初始化以前,类或接口必须已经被链接过,即经过了验证和准备阶段,可能已经解析过了。

以下情况第一次方式时,类或接口类型T会在紧靠此时刻之前被初始化:

  • T是类,并且创建了T的实例
  • T是类,并且T声明的static方法被调用
  • T声明的static域被赋值
  • T声明的static域被使用,并且该域不是常量变量
  • T是顶层类,并且在词法上嵌套在T内的assert语句被执行

  对static域的引用只会导致实际声明它的类或接口被初始化,即使它可能是通过子类名、子接口名或实现了某个接口的类名而被引用的。

  对Class类中和java.lang.reflect包中的某些反射方法的调用也会导致类和接口被初始化。

  其他任何情况都不会使类或接口被初始化。

实例 超类在子类之前被初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Super {
static { System.out.print("Super "); }
}
class One {
static { System.out.print("One "); }
}
class Two extends Super {
static { System.out.print("Two "); }
}
class Test {
public static void main(String[] args) {
One o = null;
Two t = new Two();
System.out.print((Object) o == (Object) t);
}
}

  结果输出如下。类One永远不会初始化,因此永远不会被链接。类Two只有在其超类Super被初始化之后才会被初始化。

1
Super Two false

实例 只有声明static域的类才会被初始化

1
2
3
4
5
6
7
8
9
10
11
class Super {
static int taxi = 1729;
}
class Sub extends Super {
static { System.out.print("Sub "); }
}
class Test {
public static void main(String[] args) {
System.out.print(Sub.taxi);
}
}

  结果输出如下。因为Sub类永远没有被初始化,对Sub.taxi的引用实际上是对在Super类中声明的域的引用,它不会触发对Sub类的初始化。

1
1729

实例 接口初始化不会初始化超接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface I {
int i = 1, ii = Test.out("ii",2);
}
interface J extends I {
int j = Test.out("j",3), jj = Test.out("jj",4);
}
interface K extends J {
int k = Test.out("k",5);
}
interface Test {
public static void main(String[] args) {
System.out.println(J.i);
System.out.println(K.j);
}
static int out(String s, int i){
System.out.println(s + "=" + i);
return i;
}
}

  结果输出如下。J.i引用的域是一个常量变量,因此它不会使I被初始化。K.j引用的域实际上是在接口J中声明的,它不是常量变量,它会导致J接口的域被初始化,但超接口I中的域和接口K中的域都不会初始化。

  尽管K被用来引用接口J中的域j,但接口K并不会被初始化。

1
2
3
4
1
j=3
jj=4
3

详细的初始化过程

  暂缺,请参考《Java语言规范》12.4.2


第五节 创建新的类实例

  暂缺,请参考《Java语言规范》12.5


第六节 类实例的终结

  暂缺,请参考《Java语言规范》12.6


第七节 卸载类和接口

  暂缺,请参考《Java语言规范》12.7


第八节 程序退出

  暂缺,请参考《Java语言规范》12.8


参考博客和文章书籍等:

《Java语言规范》

《Java虚拟机规范》

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容