7.1.2验证

当类型被装载后,就准备进行连接了。连接过程的第一步是验证——确认类型符合Java语言 的语义,并且它不会危及虚拟机的完整性。

在验证上,不同的虚拟机实现拥有一些灵活性。虚拟机实现的设计者可以决定如何以及何 时验证类型。Java虚拟机规范列出了虚拟机可以拋出的异常以及在何种条件下必须抛出它们。不 管Java虚拟机可能遇到了什么样的麻烦,都应该有一个异常或者错误可以抛出。规范表明了在每 种情形下应该抛出何种异常或者错误。某些情况下,规范明确地说明何时这种异常或者错误应 该被抛出,但是通常没有严格地规定应该如何或者在何时检查错误条件。

不管怎样,在大多数Java虚拟机实现中特定类型的检查一般都在特定的时间发生。比如,在 装载过程中,虚拟机必须解析代表类型的二进制数据流,并且构造内部数据结构。在这个时候, 必须做一些特定的检查,以保证解析二进制数据的初始工作不会导致虚拟机崩溃。在这个解析 期间,虚拟机大多会检查二进制数据以确保数据全部是预期的格式。Java class文件格式的解析器可能检查魔数,确保每一个部分都在正确的位置,拥有正确的长度,验证文件不是太长或者太短,等等。虽然这些检查在装载期间完成,是在正式的连接验证阶段之前进行,但它们仍然 在逻辑上属于验证阶段。检查被装载的类型是否有任何问题的整个过程都属于验证。

另外一个很可能在装载时进行的检查是,确保除了Object之外的每一个类都有一个超类。在 装载时检查的原因是当虚拟机装载一个类时,它必须确保该类的所有超类都已经被装载了。对 于给定的类,得到其超类名字的惟一方法就是观察类的二进制数据。因为Java虚拟机无论如何都 要在装载的时候检查每个类的超类数据,所以在装载阶段做这个检查是顺理成章的。

在大部分虚拟机实现中,还有一种检査往往发生在正式的验证阶段之后,那就是符号引用的 验证。在前面的章节中描述过,动态连接的过程包括通过保存在常景池中的符号引用查找被引用 的类、接口、字段以及方法,把符号引用替换成直接引用。当虚拟机搜寻一个被符号引用的元素 (类型、字段或者方法)时,它必须首先确认该元素存在。如果虚拟机发现元素存在,它必须进 —步检查引用类型有访问该元素的权限。这些对存在性和访问权限的检查逻辑上是验证的一部分, 属于连接的第一阶段,但是往往在解析的时候发生,那是连接的第三阶段。解析自身也可能延迟 到符号引用第一次被程序所使用时,所以这些检查甚至有可能在初始化之后才进行。

那么在正式的验证阶段做哪些检查呢?任何在此之前还没有进行的检查以及在此之后不会被检查的项目都包含在内。在正式的验证阶段需要完成的候选检查在下面列出。首先列出确保 各个类之间二进制兼容的检查:

•检查final的类不能拥有子类。

•检查final的方法不能被覆盖。

•确保在类型和超类型之间没有不兼容的方法声明(比如两个方法拥有同样的名字,参数在 数量、顺序、类型上都相同,但是返回类型不同)。

请注意,当这些检査需要查看其他类型的时候,它只需要査看超类型。超类需要在子类初 始化前被初始化,所以这些类应该已经被装载了。当实现了父接口的类被初始化的时候,不需 要初始化父接口。然而,当实现了父接口的子类(或者是扩展了父接口的子接口)被装载时, 父接口也必须被装载。(它们不会被初始化,只是被装载了,可能被某些虚拟机实现可选地连接 了。)装载一个类的时候,它所有的超类都会被装载。在验证期间,这个类和它所有的超类型都 需要确保互相之间仍然二进制兼容。

•检查所有的常量池入口相互之间一致(比如,一个CONSTANT_String_info人口的 string_index项目必须是一个CONSTANT_Utf8_info人口 的索引)。

•检查常量池中的所有的特殊字符串(类名、宇段名和方法名、字段描述符和方法描述符) 是否符合格式。

•检查字节码的完整性。

上面所列出的最复杂的任务就是字节码验证。所有的Java虚拟机都必须设法为它们执行的每 个方法检验字节码的完整性。比如,不能因为一个超出了方法末尾的跳转指令就导致虚拟机实 现崩溃。它们必须在字节码验证的时候检查出这样的跳转指令是非法的,从而抛出一个错误。

虚拟机的实现没有强求在正式的连接验证阶段进行字节码验证。比如,实现可以自由地选 择在执行每条语句的时候单独进行验证。然而,Java虚拟机指令集设计的一个目标就是使得字节码流可以通过一次性使用一个数据流分析器进行验证。在连接过程中一次性验证字节码流,而 非在程序执行的时候动态验证,使得Java程序的运行速度得到很大的提高。

当通过一个数据流分析器进行字节码验证的时候,虚拟机可能不得不为了确保符合Java语言 的语义而装载其他的类。比如,设想一个类包含一个方法,其中把一个java.lang.的实例的引用 赋值给了一个java.lang.Number类型的字段。在这个情况下,虚拟机将在字节码验证的时候装载 类Float,确保这是一个Number类的子类。它也不得不装载Number来确保它没有被声明为final。 虚拟机此时不需要初始化Float,只需要装载它。Float会在首次主动使用时被初始化。

更多关于类验证过程的信息,请参见第3章。

7.1.3准备

随着Java虚拟机装载了一个类,并执行了一些它选择进行的验证之后,类就可以进人准备阶 段了。在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值。但在到达初始化阶段之前, 类变量都没有被初始化为真正的初始值。(在准备阶段是不会执行Java代码的。)在准备阶段,虚 拟机把给类变量新分配的内存根据类型设置为默认值。不同类型的默认值在表7-1中列出。

虽然在表7-1中出现了boolean类型,Java虚拟机不太支持boolean。在内部,boolean常常被 实现为一个int,会被默认地置为0 (就是boolean取false值)。因此,boolean类变量,就算它们在 内部是被作为int实现的,也总是被初始化成false。

在准备阶段,Java虚拟机实现可能也为一些数据结构分配内存,目的是提高运行程序的性能。 这种数据结构的例子如方法表,它包含指向类中每一个方法(包括从超类继承的方法)的指针。 方法表可以使得继承的方法执行时不需要搜索超类。第8章中描述了方法表的更多细节。

7.1.4解析

•类型经过了连接的前两个阶段-验证和准备-之后,它就可以进人第三个(也就是最

后一个)连接阶段了-解析。解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。在前面提到过,在符号引用被程序首次使 用之前(有可能在该类初始化之后,会解析多次,比如每个方法的字节码中的常量池),连接的这个步骤都是可选的。第8章中描述了解析的更多细节。

7.1.5初始化

为了准备让一个类或者接口被首次主动使用,最后一个步骤就是初始化,也就是为类变量陚予正确的初始值。这里的“正确”初始值指的是程序员希望这个类变量所具备的起始值。正 确的初始值是和在准备阶段賦予的默认初始值对比而言的。前面说过,根据类型的不同,类变量已经被賦予了默认初始值。而正确的初始值是根据程序员制定的主观计划而生成的。

在Java代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化语句给出的。— 个类变量初始化语句是变量声明后面的等号和表达式:

class Examplela{

// "=3 * (int) (Math.random() * 5.0);"is the class variable
// initializer

static int size = 3 * (int) (Math.random() * 5.0);

}

静态初始化语句是一个以static关键字开头的程序块:
class Examplelb{
static int size;
static {

size = 3 * (int) (Math.random() * 5.0);

}

}

所有的类变量初始化语句和类型的静态初始化器都被Java编泽器收集在一起,放到一个特殊 的方法中。对于类来说,这个方法被称作类初始化方法;对于接口来说,它被称为接口初始化 方法。在类和接口的Java class文件中,这个方法被称为“<clinit>”。通常的Java程序方法是无法 调用这个<clinit>方法的。这种方法只能被Java虚拟机调用,专门用把类型的静态变量设置为它 们的正确初始值。

初始化一个类包含两个步骤:

1)如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。

2)如果类存在一个类初始化方法,就执行此方法。

当初始化一个类的直接超类的时候,也是需要包含这两个步骤。因此,第一个被初始化的 类永远是Object,然后是被主动使用的类的继承树上所有的类。超类总是在子类之前被初始化。

初始化接口并不需要初始化它的父接口,因此初始化一个接口只需一步:如果接口存在一 个接口初始化方法的话,就执行此方法。

<clinit> ()方法的代码并不显式地调用超类的<clinit> ()方法。在Java虚拟机调用类的 <clinit> ()方法之前,它必须确认超类的<clinit> ()方法已经被执行了。

Java虚拟机必须确保初始化过程被正确地同步。如果多个线程需要初始化一个类,仅仅允许一个线程来执行初始化,其他的线程程序需要等待。当活动的线程完成了初始化过程之后,它必须通知其他等待的线程。第20章介绍了有关同步、等待和通知的内容。

1. <clinit> ()方法

前面说过,Java编译器把类变量初始化语句和静态初始化语句的代码都放到Class文件的 <clinit> ()方法中,顺序就按照它们在类或者接口声明中出现的顺序。思考下面的类的例子:
并非所有的类都需要在它们的class文件中拥有一个<clinit> ()方法。如果类没有声明仟何 类变量,也没有静态初始化语句,那么它就不会有<clinit> ()方法。如果类声明了类变量,但 是没有明确使用类变量初始化语句或者静态初始化语句初始化它们,那么类不会有<clinit> () 方法。如果类仅包含静态final变量的类变量初始化语句,而且这些类变量初始化语句采用编译 时常量表达式,类也不会有<clinit> ()方法。只有那些的确需要执行Java代码来陚予类变量正 确初始值的类才会有类初始化方法。

下面是一个类的例子,Java编译器不会为它产生<clinit> ()方法。

class Exampleld {
static final int angle=35;
static final int length=angle*2;
}
类Exampleld 声明了两个常量-angle和length,并且通过表达式给它们陚予了初始值,这 些表达式是编译时常量。编译器知道angle表示35,length表示70。但Exampleld 类被Java虚拟机装载的时候,angle和length并没有作为类变量保存在方法区中。因此,不需要<clinit> ()方法 来初始化它们。angle和length字段并非类变量,它们是常量,被java编译器特殊处理了。

Exampleld的angle和length字段没有被当做类变量,Java虚拟机在使用它们的任何类的常量 池或者字节码流中直接存放的是它们表示的常量的int值。比如,如果一个类使用了Exampleld的 angle字段,该类不会在常量池中保存一个指向Exampleld类的angle字段的符号引用,而是直接 在它们的字节码流中嵌人一个值35。如果angle的常量值超过了short值的限制(-32 768 - 32 767 ), 比如是35000,那么类会在它的常量池中保存一个CONSTANT_Intger_info人口,值为35000。

下面是-个类的例子,它同吋使用一个常量和一个其他类的类变量。
// On CD-ROM in file classlife/ex1/Example1e.java
class Example1e {

// The class variable initializer for symbolicRef uses a symbolic
// reference to the size class variable of class Example1a
static int symbolicRef = Example1a.size;

// The class variable initializer for localConst doesn't use a
// symbolic reference to the length field of class Example1d.
// Instead, it just uses a copy of the constant value 70.
static int localConst = Example1d.length * (int) (Math.random()
* 3.0);
}
java编译器为类Examplele产生如下的<clinit()方法:
// The code for symbolicRef's class variable initializer begins here
// Push int value from Exanplela.size.
// This getstatic instruction refers to a
// symbolic reference to Bxamplela.size.

0 getstatic #9 <field int size>

// Pop int, store into class variable
// symbolicRef
3 putstatic #10 <Field int symbolicRef >
// The code for localConst's class variable intializer begins here:
// Expand byte operand to int, push int
// result. This is the local copy of
6 bipush 70 // Exampleld's length constant, 70.
// Invoke Math.random(), which will push
//a double return value
double
8 invokestatic #8 <Method double random()>
11 ldc2_w #11 <Double 3.0> //Push double constant 3.0
14 dmul // Pop two doubles,multiply,push result
15 d2i // Pop double,convert to int, push int
16 imul // Pop two ints, multiply, push int result
// Pop int, store into class variable
// localConst

17 putstatic #7 <Field int localConst>
20 return //Return void from <clinit> method

偏移量为0的getstatic指令使用一个符号引用(位于常量池人口9)指向类Examplela的size字段。在偏移量6的bipush指令后面跟随了一个字节,保存了Exampleld.length表示的常量值。 Examplele的常量池没有包含指向Exampleld的任何符号引用。

接口也可能在class文件中包含一个<clinit>()方法。所有在接口中声明的隐式公开(pubnc )、 静态(static)和最终(final)字段都必须在宇段初始化语句中初始化。如果接口包含任何不能 在编译时被解析成为一个常量的字段初始化语旬,接口就会拥有一个<clinit> ()方法。下面是 —个例子:
// On CD-ROM in file classlife/ex1/Example1f.java
interface Example1f {

int ketchup = 5;
int mustard = (int) (Math.random() * 5.0);
}

请注意,只有mustard字段被这个<clinit> ()方法初始化了。因为ketchup字段被初始化为一 个编译时常量,它被编译器特殊处理了。虽然使用Examplelf.mustard的类型保存指向这个字段 的符号引用,但是使用Examplelf.ketchup的类型将会保存ketchup的常量值5的一份本地拷贝。

2.主动使用和被动使用

在前面讲过,Java虚拟机在首次主动使用类型时初始化它们。只有6种活动被认为是主动使 用:创建类的新实例,调用类中声明的静态方法,操作类或者接口中声明的非常量静态字段, 调用Java API中特定的反射方法,初始化一个类的子类,以及指定一个类作为Java虚拟机启动时 的初始化类。

使用一个非常量的静态字段只有当类或者接口的确声明了这个字段时才是主动使用。比如, 类中声明的字段可能会被子类引用;接口中声明的字段可能会被子接口或者实现了这个接口的 类引用。对于子类、子接口和实现了接口的类来说,这就是被动使用一使用它们并不会触发 它们的初始化。只有当字段的确是被类或者接口声明的时候才是主动使用。下面的例子说明了 这个原理:

原文地址:https://www.cnblogs.com/mongotea/p/11979545.html