第一部分 类和类的装载
我们来看一下类以及它们被JVM装载的时候做了些什么?
在这个新的有关动态的Java编程特征的系列文章中,将会看到在正在执行的Java应用程序的背后发生了些什么。企业级Java专家Dennis Sosnoski给出了Java二进制格式和发生在JVM内部的类中的事情。遵循这条路线,他介绍正在装载的类所影响的范围(从正在运行的一简单的Java应用程序所必须的大量的类到在J2EE和类似的复杂的框架结构中类装载器冲突所可能导致的问题)。
这篇文章揭示了Java动态编程这组主题所包含的一系列的新的知识。这些主题包括从Java二进制类文件格式的结构到使用反射访问运行时的元数据,以及所有的在运行时编辑和构造新的类的方法。贯穿这个材料的全部基本路线是Java平台的编程思想,是比用其它直接编译成本地代码的语言更加动态的工作。如果你理解了这些动态的特征,你就可用Java语言做一些用其它的主流的编程语言所不能做的事情。
在这篇文章中。我介绍了位于Java平台的动态特征之下的一些基本概念。这些概念围绕用于描述Java类的二进制格式,包括类被装载进JVM(Java虚拟机)时所发生的事情。这篇文件不仅为理解这个系列主题的其它文章提供基础,同时也演示了一些非常实际的在Java平台上工作的开发人员所关心的事情。
一个类的二进制形式
用Java语言的开发人员通常不必关心通过编译器运行他们的源代码时所发生的一些细节问题。在这个系列主题中。我会介绍许多有关从源代码到可执行的程序这个过程的背后细节,因此,我们先来看一下编译器所产生的二进制类。
二进制类的格式实际上是被JVM(Java虚拟机)规范定义的。正常的类的描述是一个编译器利用Java语言的源代码生成的,并且通常被保存在一以.class为扩展名的文件中。但是这些特征都不是本质的。其它的一些编程语言已经被开发使用Java的二进制类的格式,并且,因为一些目的,新的类的描述被创建并且被直接装载进一个正在执行的JVM中。但是JVM所关心的,重要的不是这些源代码或它是怎样被存储的,而是这个格式自身。
因此,先来这种类格式看上去象什么呢?下面(List 1.)列出了一个非常短的类的源代码,紧跟着是用编译器输出的这个类文件的一部分十六进制的显示:
List 1.Hello.java的源代码和(部分)二进制表示
public class Hello
{
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
0000: cafe babe 0000 002e 001a 0a00 0600 0c09 ................
0020: 0013 0100 063c 696e 6974 3e01 0003 2829 .....
0030: 5601 0004 436f 6465 0100 046d 6169 6e01 V...Code...main.
0040: 0016 285b 4c6a 6176 612f 6c61 6e67 2f53 ..([Ljava/lang/S
0050: 7472 696e 673b 2956 0c00 0700 0807 0014 tring;)V........
0060: 0c00 1500 1601 000d 4865 6c6c 6f2c 2057 ........Hello, W
0070: 6f72 6c64 2107 0017 0c00 1800 1901 0005 orld!...........
0080: 4865 6c6c 6f01 0010 6a61 7661 2f6c 616e Hello...java/lan
0090: 672f 4f62 6a65 6374 0100 106a 6176 612f g/Object...java/
00a0: 6c61 6e67 2f53 7973 7465 6d01 0003 6f75 lang/System...ou
...
二进制的内部
List1中所显示的二进制类的表示的第一件事情是标识Java二进制类的格式的“café babe”签名,这个签名只是一种确认实际请求的Java类的格式的一个实例的数据块的简易方法。每个Java的二进制类,即使在不同的文件系统上,也需要用这四个字节开始。
数据的其它部分不是很有趣。跟在签名后面是一对类格式的版本号(在这个例子中,用1.4.1javac编译生成的时候,会产生次版本为0、主版本为46------十六进制的形式是0x2e的版本号),然后是常量池中的条目的计数。跟在条目计数(在这个例子中是26,或0x001a)后面的是实际的常量池数据。这是保存所有类定义所使用的常量的地方。它包括类和方法的名字、签名以及字符串(这些字符串是你能够认可的在十六制的存放处的正确性的文本解释)、以及连同在一起的各种二进制值。
在常量池中项目是可变长度的,每个项目的第一个字节标识了项目的类型和它应该怎样被解码。我不打算对这些内容做详细介绍,如果你有兴趣以实际的JVM规范开始,这里有许多有用参考。关键点是常量池包含了所有的对其它类和这个类所使用的方法的引用,还有这个类自身以及它的方法的实际定义。尽管平均值可能会少一些,但是常量池的大小很容易的超过二进制类的在小的一半或更多。
跟在常量池后面是几个引用常量池条目的项目,它们是类本身,它的超类以及接口。这些项目的后面是有关字段和方法的信息,这些信息是做为复合结构来描述自己的。对于方法的可执行代码以代码属性(code attributes)的形式被包含在方法的定义中。这种代码是JVM的指令形式,通常叫做字节码(bytecode),这是下一节的主题之一。
在Java类的格式中属性(Attributes)用来做为几种定义的用途,包括已经提到的字节码(bytecode),用于字段的常量值,异常处理,以及调试信息。但是,属性(Attributes)不只有这些可能的用途。从一开始,JVM规范要求JVMs(Java虚拟机)忽略未知类型的属性。这种要求对于属性的使用提供了灵活性,使得它在将来能够服务于其它的用途,例如提供与用户类一起工作的框架所需要的元信息------这是一种Java源于C#语言所广泛使用的方法。不幸的是,no hook have yet been provided for making of this flexibility at the user level.
字节码和堆栈
组成类文件的可执行部分的字节码是适应特定类型计算机(JVM)是的实际的机器码,这所以叫做虚拟机是因为它是用软件来设计实现的,而不是硬件。每个运行在JVM上的应用程序都是建立在这种机器的一种实现。
虚拟机实际上相当的简单,它使用堆栈结构,这就意味着它们在被使用之前指令操作要被装载进一个内部