一个.class文件定义了JVM中的一个类型,包括域、方法、继承信息、注解和其他元数据。规范中对类文件的格式有详细描述,任何想在JVM上运行的语言都必须遵守。
类是平台能加载的最小程序代码单元。要将新的类加入到JVM的当前运行态中,有几步操作必须执行。首先,类文件必须被加载进来并连接,而且必须进行大量的验证。之后会提供一个代表该类型的新Class
对象给正在运行的系统,并可以创建新的实例。
本节会讨论所有这些步骤,并介绍一下类加载器,也就是控制整个过程的那些类。我们先来看看加载和连接。
5.1.1 加载和连接:概览
JVM的目的是使用类文件并执行其中的字节码。要实现这个目的,JVM必须以字节数据流的方式取出类文件中的内容,并将其转换成可用的格式加入运行态中。这个分两步走的过程被称为加载和连接,但连接又会被分解为几个子阶段。
加载
这个过程首先要读取构成类文件的字节数据流并给类的表现形式解冻。该过程一开始是创建一个字节数组,其内容通常是从文件系统中读取的,然后产生与所加载的类对应的Class
对象。在这个过程中会对类做一些基本检查,但在加载过程结束时,Class
对象还不成熟,所以类也不可用。
连接
加载完成之后,类必须被连接起来。这一步骤分为三个子阶段——验证,准备和解析。验证阶段证实类文件符合预期,不会引起系统的运行时错误或其他问题。之后是类的准备阶段,在类文件中引用的其他类型全部都要定位到,以确保该类已准备就绪。
连接步骤中各子阶段之间的相互关系如图5-1所示。
图5-1 加载与连接(及连接的子阶段)
5.1.2 验证
验证是一个非常复杂的过程,它分为几个步骤。
首先是完整性检查。这实际上是加载过程中的一部分,会确保类文件格式良好,可以连接。
接着是检查常量池(详情参见5.3.3节)中的符号信息是自相一致的,并要遵守常量的基本行为准则。其他不涉及代码的静态检查也要在这一阶段完成,比如检查final
方法有没有被重写。
之后是验证中最复杂的部分——方法的字节码检查。要检查字节码行为良好,并且不会试图摆脱VM的环境控制。下面是一些主要检查。
- 是否所有方法都遵守访问控制关键字的限定。
- 方法调用的参数个数和静态类型是否正确。
- 确保字节码不会试图滥用堆栈。
- 确保变量使用之前被正确初始化了。
- 检查变量是否仅被赋予恰当类型的值。
做这些检查是出于性能方面的考虑,这样可以加快解释码的运行速度,运行时就不用再做这些检查了。同时还简化了运行时把字节码编译为机器码的过程(即时编译,详情参见6.6节)。
准备
类的准备包括分配内存和准备好初始化类中的静态变量,但不会现在初始化变量,也不会执行任何VM字节码。
解析
解析会促使VM检查类文件中所引用的类型是不是都是已知的类型。如果有运行时未知的类型,那它们也需要被加载进来。这些可见的未知类型会再次引发类加载过程。
一旦需要加载的其他类型全被定位并解析完成,VM就可以初始化那个最初要加载的类。这时所有静态变量都可以被初始化,所有静态初始化代码块都会运行。现在你运行的字节码就是来自新加载进来的类里的。这一步完成之后,类的加载就已全部完成,类也就可以使用了。
5.1.3 Class对象
连接和加载过程的最终结果是一个Class
对象,用于表示加载并连接起来的新类型。尽管出于性能方面的考虑,Class
对象只是在要求的地方做了初始化,但现在它在VM中完全生效了。代码可以继续执行了,它可以使用新类型并创建新实例。此外,Class
对象提供了一些不错的方法,比如getSuperClass
,可以用它返回Class
对象的父类。
Class
对象可以和反射API一起实现对方法、域、构造方法等类成员的间接访问。Class
对象中有对类成员Method
和Field
对象的引用。反射API可以用这些对象实现对它们的间接访问。图5-2是这种结构的高层视图。
图5-2 Class对象与Method引用
运行时的哪个部分会定位并连接字节流以生成新的加载类?在下一个主题中,我们会讨论这个问题,即能够完成这些工作的类加载器,它是由抽象类ClassLoader
的子类们组成的。
5.1.4 类加载器
Java平台里有几个经典的类加载器,它们在平台的启动和常规操作过程中承担不同的任务:
根(或引导)类加载器——通常在VM启动后不久实例化,一般用本地代码实现。最好把它看做VM的一部分。它的作用通常是负责加载系统的基础JAR(主要是
rt.jar
),而且它不做验证工作。扩展类加载器——用来加载安装时自带的标准扩展。一般包括安全性扩展。
应用(或系统)类加载器——这是应用最广泛的类加载器。它负责加载应用类。在大多数SE(Java标准版)的环境中,主要工作都是由它来完成。
定制类加载器——在更复杂的环境中,比如EE(Java企业版)或比较复杂的SE框架,通常会有些附加(即定制)的类加载器。有些团队甚至为他们的某个应用程序编写了特定的类加载器。
除了核心任务,类加载器还经常要从JAR文件或classpath中加载资源(不是类文件,比如图片或配置文件)。
例子——工具类加载器
在EMMA测试覆盖工具(http://emma.sourceforge.net/)中使用的一个类加载器可以作为加载时转化的例子。
当为了加上额外的测试辅助信息而加载类时,EMMA的类加载器会修改字节码。当在这些代码上运行测试用例时,EMMA会记录测试用例实际测试了哪些方法和代码分支。从这些记录中,开发人员能看出对一个类的单元测试是否全面。关于测试和覆盖,在11和12章还有更多的相关讨论。
有些框架和代码还经常会使用带有额外属性的专用(甚至用户自定义的)类加载器。这些类加载器经常会在加载时对字节码进行转换,我们在第1章有提到过。
图5-3中是类加载器的继承层级以及不同加载器之间的相互关系。
图5-3 类加载器层级
我们先来看一个专用类加载器的例子——如何用类加载实现依赖注入。
5.1.5 示例:依赖注入中的类加载器
DI有两个核心思想。
- 系统内的功能单元要靠依赖项和配置信息才能正常发挥作用。
- 在对象自身的上下文里,很难表示依赖项,即使可以,也很复杂难懂。
你脑海中应该浮现出一幅包含了行为,配置和依赖项信息(它们处在对象之外)的画面。这部分通常被称为对象的运行时路线。
第3章以Guice框架为例介绍了DI。本节中我们会讨论利用类加载器实现DI的方式,但这种方式与Guice不同,实际上它更像简化版的Spring框架。
在这个想象出来的DI框架下,我们像这样启动应用程序:
java-cp<CLASSPATH>org.framework.DIMain/path/to/config.xml
CLASSPATH中必须包含DI框架的JAR文件以及在config.xml文件中引用的所有类(以及它们的所有依赖项)的JAR文件。
我们改写了前面一个类似的例子——代码清单3-7中的服务,结果如清单5-1所示。
代码清单5-1 HollywoodService
——不同的DI风格
public class HollywoodServiceDI { private AgentFinder finder = null; //空的构造方法 public HollywoodServiceDI {} //setter方法 public void setFinder(AgentFinder finder) { this.finder = finder; } public List<Agent>getFriendlyAgents { ...//同代码清单3-7 } public List<Agent>filterAgents(List<Agent>agents, String agentType) { ...//同代码清单3-7 }}
为了将它置于DI框架的管理之下,还需要一个配置文件:
<beans> <bean ... /> <bean p:finder-ref=/"agentFinder/"/></beans>
在这种方式中,DI框架利用配置文件来确定要构造的对象。这个例子需要hwService
和agentFinder
两个bean,框架会为每个bean调用空构造方法,之后是setter方法(比如为HollywoodServiceDI
的依赖项AgentFinder
调用setFinder
)。
这说明类加载分为两个阶段。第一阶段由应用类加载器完成,负责加载DIMain
及其引用的类。然后DIMain
开始运行,并在main
的参数中得到配置文件的位置。
这时候,框架已经在JVM中运行起来了,但config.xml中指定的用户类还碰都没碰呢。实际上,在DIMain
检查配置文件之前,框架不可能知道要加载什么类。
要启动config.xml中指定的应用配置,需要类加载的第二阶段。这要用到定制的类加载器。首先,要检查config.xml文件的一致性,确保它没有错误。然后,如果毫无差错,定制的类加载器就会试图从CLASSPATH
中加载指定类型。如果任何一步失败了,整个过程就会被中止。
如果成功了,DI框架可以继续创建所需的实例,并调用实例上的setter方法。如果这些都顺利完成了,程序上下文就开始运行了。
我们简单介绍了一下Spring风格的DI方式,其中大量使用了类加载。在Java技术中,还有很多要用到类加载器及其相关技术的领域。下面是一些众所周知的例子:
- 插件架构;
- 厂商提供的或自主研发的框架;
- 从非正常位置(非文件系统或URL)获取类文件;
- Java EE;
- 任何需要在JVM进程已经启动后加入新的、未知代码的情况下。
我们对类加载的讨论就到此为止。让我们进入下一节,探讨Java 7为满足反射等需求而提供的新API。