深入Java虚拟机——类型装载、连接(转)

时间:2022-09-30 17:16:56

来自http://hi.baidu.com/holder/item/c38abf02de14c7d31ff046e0

Java虚拟机通过装载、连接和初始化一个Java类型,使该类型可以被正在运行的Java程序所使用。其中,装载就是把二进制形式的Java类型读入Java虚拟机中;而连接就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接阶段分为三个子步骤——验证、准备和解析。“验证”步骤确保了Java类型数据格式正确并且适于Java虚拟机使用而“准备”步骤则负责为该类型分配它所需的内存,比如为它的类变量分配内存。“解析”步骤则负责把常量池中的符号引用转换为直接引用。虚拟机的实现可以推迟解析这一步,它可以在当运行中的程序真正使用某个符号引用时再去解析它(把该符号引用转换为直接引用)。当验证准备和(可选的)解析步骤都完成了时,该类型就已经为初始化做好了准备。在初始化期间,都将给类变量赋以适当的初始值。整个过程如图7-1所示

就像在图7-1中看到的那样,装载、连接和初始化这三个阶段必须按顺序进行。唯一的例外就是连接阶段的第三步——解析,它可以在初始化之后再进行。

在类和接口被装载和连接的时机上,Java虚拟机规范给实现提供了一定的灵活性。但是它严格定义了初始化的时机。所有的Java虚拟机实现必须在每个类或接口首次主动使用时初始化。下面这六种情形符合主动使用的要求。

1.      当创建某个类的新实例时(或者通过在字节码中执行new指令;或者通过不明确的创建、反射、克隆或者反序列化)。

2.      当嗲用某个类的静态方法时(即在字节码中执行invokestatic指令时)。

3.      当使用某个类或接口的静态字段,或者对该字段赋值时(即在字节码中,执行getstatic或putstatic指令时),用final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。

4.      当调用Java API的某些反射方法时,比如类Class中的方法或者java.lang.reflect包中的类的方法

5.      当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)。

6.      当虚拟机启动时某个被表明为启动类的类(即含有main()方法的那个类)

除上述这六种情形外,所有其他使用Java类型的方式都是被动使用,他们都不会导致Java类型的初始化。

在上面我们曾提到,任何一个类的初始化都要求它的超类在此之前已经初始化了。以此类推,该规则就意味着某个类的所有祖先类必须在该类之前被初始化。然而,对于接口来说,这条规则并不适用。只有在某个接口所声明的非常量字段被使用时,该接口才会被初始化,而不会因为实现这个接口的子接口或类要初始化而被初始化。因而,任何一个类的初始化都要求它的所有祖先类(而不是祖先接口)预先被初始化。而一个接口的初始化,并不要求它的祖先接口预先被初始化。

“在首次主动使用时初始化”这个规则直接影响着装载、连接和初始化类的机制。在首次主动使用时,其类型必须被初始化。然而,在类型能被初始化之前,它必须已经被连接了,而在它能被连接之前,它必须已经被加载了。Java虚拟机的实现可以根据需要在更早的时候装载以及连接类型,没有必要一直要等到该类型的首次主动使用采取装载和连接它。无论如何,如果一个类型在它的首次主动使用之前还没有被装载和连接的话,那它必须在此时被装载和连接,这样它才能被初始化。

7.1.1 装载

装载阶段由三个基本动作组成,要装载一个类型,Java虚拟机必须:

1.      通过该类型的完全限定名,产生一个代表该类型的二进制数据流。

2.      解析这个二进制数据流为方法区内的内部数据结构

3.      创建一个表示该类型的java.lang.Class类的实例

这个二进制数据流可能遵守java class文件格式,但是也可能遵守其他的格式。就像前一章提到的那样,所有的Java虚拟机实现必须能识别Java class文件格式,但是个别的实现也可以识别其他的二进制格式。

Java虚拟机规范并没有说Java类型的二进制数据应该怎样产生。下面是一些可能的产生“类型的二进制数据”的方式:

1.      从本地文件系统装载一个Java class文件。

2.      通过网络系在一个Java class文件。

3.      从一个ZIP、Jar、CAB或者其他某种归档文件中提取Javaclass文件。

4.      从一个专有数据库中提取Java class文件。

5.      把一个Java 源文件动态编译为class文件格式。

6.      动态为某个类型计算其class文件数据

7.      使用上述任何方法,但是使用不同于Java class文件的其他二进制文件格式。

有了类型的二进制数据之后,Java虚拟机必须对这些数据进行足够的处理,然后它才能创建java.lang.Class的实例对象。虚拟机必须把这些二进制数据解析为与实现相关的内部数据结构。装载步骤的最终产品就是这个Class类的实例对象,它成为Java程序与内部数据之间的接口。要访问关于该类型的信息(它们是存储在内部数据结构中的),程序就要调用该类型对应的Class实例对象的方法。这样一个过程,就是把一个类型的二进制数据解析为方法区的内部数据结构、并在堆上建立一个Class对象的过程,这被称为“创建”类型。

就像在前一章曾提到的,Java类型要么由启动类装载器装载,要么通过用户自定义的类装载器装载。启动类装载器是虚拟机实现的一部分,它以与实现无关的方式装载类型(包括JavaAPI的类和接口),用户自定义的类装载器是类java.lang.ClassLoader的子类实例,它以定制的方式装入类。

类装载器(启动型或者用户自定义的)并不需要一直等到某个类型“首次主动使用”时再去装入它。Java虚拟机规范允许类装载器缓存Java类型的二进制表现形式,在预料某个类型将要被使用时就装载它,或者把这些类型装载到一些相关的分组里面。如果一个类装载器在预先装载时遇到问题,无论如何,它应该在该类型被首次主动使用时报告该问题(通过抛出一个LinkageError异常的子类)。换句话说,如果一个类装载器在预先装载时遇到缺失或者错误的class文件,它必须等到程序首次主动使用该类时才报告错误。如果这个类一直没有被程序主动使用,那么该类装载器将不会报告错误。

7.1.2         验证

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

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

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

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

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

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

1.      检查final的类不能拥有子类。

2.      检查final的方法不能被覆盖

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

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

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

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

检查字节码的完整性

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

虚拟机的实现没有强求在正式的连接验证阶段进行字节码验证。所有的Java虚拟机都必须设法为它们执行的每个方法验证字节码的完整性。比如,不能因为一个超出了方法末尾的跳转指令就导致虚拟机实现崩溃。它们必须在字节码验证的时候检查出这样的跳转指令是非法的,从而抛出一个错误。

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

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

7.1.3         准备

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

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

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

7.1.4         解析

类型经过了连接的前两个阶段——验证和准备——之后,它就可以进入第三个(也就是最后一个)连接阶段了——解析。解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。

http://lqzit.iteye.com/blog/1758320