类和类的装载
我们来看一下类以及它们被JVM装载的时候做了些什么?
在这个新的有关动态的Java编程特征的系列文章中将会看到在正在执行的Java应用程序的背后发生了些什么企业级Java专家Dennis Sosnoski给出了Java二进制格式和发生在JVM内部的类中的事情遵循这条路线他介绍正在装载的类所影响的范围(从正在运行的一简单的Java应用程序所必须的大量的类到在JEE和类似的复杂的框架结构中类装载器沖突所可能导致的问题)
这篇文章揭示了Java动态编程这组主题所包含的一系列的新的知识这些主题包括从Java二进制类文件格式的结构到使用反射访问运行时的元数据以及所有的在运行时编辑和构造新的类的方法贯穿这个材料的全部基本路线是Java平台的编程思想是比用其它直接编译成本地代码的语言更加动态的工作如果你理解了这些动态的特征你就可用Java语言做一些用其它的主流的编程语言所不能做的事情
在这篇文章中我介绍了位于Java平台的动态特征之下的一些基本概念这些概念围绕用于描述Java类的二进制格式包括类被装载进JVM(Java虚拟机)时所发生的事情这篇文件不仅为理解这个系列主题的其它文章提供基础同时也演示了一些非常实际的在Java平台上工作的开发人员所关心的事情
一个类的二进制形式
用Java语言的开发人员通常不必关心通过编译器运行他们的源代码时所发生的一些细节问题在这个系列主题中我会介绍许多有关从源代码到可执行的程序这个过程的背后细节因此我们先来看一下编译器所产生的二进制类
二进制类的格式实际上是被JVM(Java虚拟机)规范定义的正常的类的描述是一个编译器利用Java语言的源代码生成的并且通常被保存在一以class为扩展名的文件中但是这些特征都不是本质的其它的一些编程语言已经被开发使用Java的二进制类的格式并且因为一些目的新的类的描述被创建并且被直接装载进一个正在执行的JVM中但是JVM所关心的重要的不是这些源代码或它是怎样被存储的而是这个格式自身
因此先来这种类格式看上去象什么呢?下面(List )列出了一个非常短的类的源代码紧跟着是用编译器输出的这个类文件的一部分十六进制的显示
List Hellojava的源代码和(部分)二进制表示
public class Hello
{
public static void main(String[] args) {
Systemoutprintln(Hello World!);
}
}
: cafe babe e a a c
: d e fa
: c e e <init>()
: f d eVCodemain
: b ca f c e f([Ljava/lang/S
: e b c tring;)V
: c d cc fc Hello W
: f c c orld!
: cc f a fc eHellojava/lan
: f f a a fg/Objectjava/
a: c e f d flang/Systemou
二进制的内部
List中所显示的二进制类的表示的第一件事情是标识Java二进制类的格式的café babe签名这个签名只是一种确认实际请求的Java类的格式的一个实例的数据块的简易方法每个Java的二进制类即使在不同的文件系统上也需要用这四个字节开始
数据的其它部分不是很有趣跟在签名后面是一对类格式的版本号(在这个例子中用javac编译生成的时候会产生次版本为主版本为十六进制的形式是xe的版本号)然后是常量池中的条目的计数跟在条目计数(在这个例子中是或xa)后面的是实际的常量池数据这是保存所有类定义所使用的常量的地方它包括类和方法的名字签名以及字符串(这些字符串是你能够认可的在十六制的存放处的正确性的文本解释)以及连同在一起的各种二进制值
在常量池中项目是可变长度的每个项目的第一个字节标识了项目的类型和它应该怎样被解码我不打算对这些内容做详细介绍如果你有兴趣以实际的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上的应用程序都是建立在这种机器的一种实现
虚拟机实际上相当的简单它使用堆栈结构这就意味着它们在被使用之前指令操作要被装载进一个内部的堆栈指令集包括所有的一般的算术运算和逻辑操作还有有条件和无条转移装载/存储调用/返回堆栈的维护以及几种特殊的指令类型包括立即数的一些指令被直接编码进指令另外一些直接从常量池引用值
虽然虚拟机是简单的但执行起来却不是这样的第一代JVM基本上是虚拟机的字节码的解析器相对而言比较简单但却遇到严重的性能问题———解析代码总是要比执行本地代码花费更长的时间为了减少这些性能问题第二代JVM添加了即时(JIT)翻译JIT技术是在Java字节码第一次执行之前把它编译成本地代码从而为重复执行提供了更好的性能当前的JVM做的更好它使用相应的技术来监控程序的执行并且选择性使使用代码得到优化
装载类
把源代码编译成本地代码的语言(如C和C++)在源代码被编译之后通常需要链接这样的步骤这种链接过程把独立编译的源文件连同共享类库的代码合并到一起从而形成一个可执行的程序Java语言是不同的使用Java语言编译器生成的类文件一般情况下单独保存的直到它们装载进一个JVM为止即使是建立一个JAR文件也不会改变这种情况———JAR文件只是类文件的一个容器
优于一个分开的步骤JVM把类装载进内存的时候链接类成为JVM所要执行的工作的一部分这样就可以在初始化装载的时候增加一些系统开销但是也为Java应用程序提供了高级的灵活性例如应用程序可以使用直到运行时才知道的实际实现的接口来编写这种后期绑定(late binding)的方法来装配一个应用程序在Java平台中被广泛使用servlets就是一个普通的例子
对于装载类的规则在JVM规范的细节中被清楚的说明了基本原则是类只有在需要的时候才被装载(或者至少是显示的装载JVM的这种方法在实际装载过程中有一些灵活性但是必需保持一个固定的类初始化的顺序)每个被装载的类可以有其它的它所依赖的类因此装载过程是递归的在Listing 中的类显示了这种递归装载是怎样工作的这个Demo类包含了一个简单的创建Greeter类的一个实例并且调用这个类的greet方法的main方法Greeter类的构造器创建了一个Message的实例然后它在greet方法中使用这个Message实例
Listing 用于类装载演示的源码
public class Demo
{
public static void main(String[] args) {
Systemoutprintln(**beginning execution**);
Greeter greeter = new Greeter();
Systemoutprintln(**created Greeter**);
greetergreet();
}
}
public class Greeter
{
private static Message s_message = new Message(Hello World!);
public void greet() {
s_messageprint(Systemout);
}
}
public class Message
{
private String m_text;
public Message(String text) {
m_text = text;
}
public void print(javaioPrintStream ps) {
psprintln(m_text);
}
}
设置java命令的命令行参数为verbose:class这样就可打印类装载过程的轨迹Listing 显示了使用这个参数的来运行Listing 时的部分输出
[Opened /usr/java/jsdk/jre/lib/rtjar]
[Opened /us