Java 编程的动态性 第1 部分: 类和类装入--转载
原文地址:http://www.ibm.com/developerworks/cn/java/j-dyn0429/
本文是这个新系列文章的第一篇,该系列文章将讨论我称之为 Java 编程的动态性的一系列主题。这些主题的范围从 Java 二进制类文件格式的基本结构,以及使用反射进行运行时元数据访问,一直到在运行时修改和构造新类。贯穿整篇文章的公共线索是这样一种思想:在 Java 平台上编程要比使用直接编译成本机代码的语言更具动态性。如果您理解了这些动态方面,就可以使用 Java 编程完成那些在任何其它主流编程语言中不能完成的事情。
本文中,我将讨论一些基本概念,它们是这些 Java 平台动态特性的基础。这些概念的核心是用于表示 Java 类的二进制格式,包括这些类装入到 JVM 时所发生的情况。本文不仅是本系列其余几篇文章的基础,而且还演示了开发人员在使用 Java 平台时碰到的一些非常实际的问题。
用二进制表示的类
使用 Java 语言的开发人员在用编译器编译他们的源代码时,通常不必关心对这些源代码做了些什么这样的细节。但是本系列文章中,我将讨论从源代码到执行程序所涉及的许多幕后细节,因此我将首先探讨由编译器生成的二进制类。
二进制类格式实际上是由 JVM 规范定义的。通常这些类表示是由编译器从 Java 语言源代码生成的,而且它们通常存储在扩展名为 .class
的文件中。但是,这些特性都无关紧要。已经开发了可以使用 Java 二进制类格式的其它一些编程语言,而且出于某些目的,还构建了新的类表示,并被立即装入到运行中的 JVM。就 JVM 而言,重要的部分不是源代码以及如何存储源代码,而是格式本身。
那么这个类格式实际看上去是什么样呢?清单 1 提供了一个(非常)简短的类的源代码,还附带了由编译器输出的类文件的部分十六进制显示:
清单 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 ................
0010: 000d 000e 0800 0f0a 0010 0011 0700 1207 ................
0020: 0013 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()
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
...
二进制类文件的内幕
清单 1 显示的二进制类表示中首先是“cafe babe”特征符,它标识 Java 二进制类格式(并顺便作为一个永久的 ― 但在很大程度上未被认识到的 ― 礼物送给努力工作的 barista,他们本着开发人员所具备的精神构建 Java 平台)。这个特征符恰好是一种验证一个数据块 确实声明成 Java 类格式的一个实例的简单方法。任何 Java 二进制类(甚至是文件系统中没有出现的类)都需要以这四个字节作为开始。
该数据的其余部分不太吸引人。该特征符之后是一对类格式版本号(本例中,是由 1.4.1 javac 生成的次版本 0 和主版本 46 ― 用十六进制表示就是 0x2e),接着是常量池中项的总数。项总数(本例中,是 26,或 0x001a)后面是实际的常量池数据。这里放着类定义所用的所有常量。它包括类名和方法名、特征符以及字符串(您可以在十六进制转储右侧的文本解释中识别它们),还有各种二进制值。
常量池中各项的长度是可变的,每项的第一个字节标识项的类型以及对它解码的方式。这里我不详细探究所有这些内容的细节,如果感兴趣,有许多可用的的参考资料,从实际的 JVM 规范开始。关键之处在于常量池包含对该类所用的其它类和方法的所有引用,还包含了该类及其方法的实际定义。常量池往往占到二进制类大小的一半或更多,但平均下来可能要少一些。
常量池后面还有几项,它们引用了类本身、其超类以及接口的常量池项。这些项后面是有关字段和方法的信息,它们本身用复杂结构表示。方法的可执行代码以包含在方法定义中的 代码属性的形式出现。用 JVM 的指令形式表示该代码,一般称为 字节码,这是下一节要讨论的主题之一。
在 Java 类格式中, 属性被用于几个已定义的用途,包括已提到的字节码、字段的常量值、异常处理以及调试信息。但是属性并非只可能用于这些用途。从一开始,JVM 规范就已经要求 JVM 忽略未知类型的属性。这一要求所带来的灵活性使得将来可以扩展属性的用法以满足其它用途,例如提供使用用户类的框架所需的元信息,这种方法在 Java 派生的 C# 语言中已广泛使用。遗憾的是,对于在用户级利用这一灵活性还没有提供任何挂钩。
字节码和堆栈
构成类文件可执行部分的字节码实际上是针对特定类型的计算机 ― JVM ― 的机器码。它被称为 虚拟机,因为它被设计成用软件来实现,而不是用硬件来实现。每个用于运行 Java 平台应用程序的 JVM 都是围绕该机器的实现而被构建的。
这个虚拟机实际上相当简单。它使用堆栈体系结构,这意味着在使用指令操作数之前要先将它们装入内部堆栈。指令集包含所有的常规算术和逻辑运算,以及条件转移和无条件转移、装入/存储、调用/返回、堆栈操作和几种特殊类型的指令。有些指令包含立即操作数值,它们被直接编码到指令中。其它指令直接引用常量池中的值。
尽管虚拟机很简单,但实现却并非如此。早期的(第一代)JVM 基本上是虚拟机字节码的解释器。这些虚拟机实际上 的确相对简单,但存在严重的性能问题 ― 解释代码的时间总是会比执行本机代码的时间长。为了减少这些性能问题,第二代 JVM 添加了 即时(just-in-time,JIT)转换。在第一次执行 Java 字节码之前,JIT 技术将它编译成本机代码,从而对于重复执行提供了更好的性能。当代 JVM 的性能甚至还要好得多,因为使用了适应性技术来监控程序的执行并有选择地优化频繁使用的代码。
装入类
诸如 C 和 C++ 这些编译成本机代码的语言通常在编译完源代码之后需要链接这个步骤。这一链接过程将来自独立编译好的各个源文件的代码和共享库代码合并起来,从而形成了一个可执行程序。Java 语言就不同。使用 Java 语言,由编译器生成的类在被装入到 JVM 之前通常保持原状。即使从类文件构建 JAR 文件也不会改变这一点 ― JAR 只是类文件的容器。
链接类不是一个独立步骤,它是在 JVM 将这些类装入到内存时所执行作业的一部分。在最初装入类时这一步会增加一些开销,但也为 Java 应用程序提供了高度灵活性。例如,在编写应用程序以使用接口时,可以到运行时才指定其实际实现。这个用于组装应用程序的 后联编方法广泛用于 Java 平台,servlet 就是一个常见示例。
JVM 规范中详细描述了装入类的规则。其基本原则是只在需要时才装入类(或者至少看上去是这样装入 ― JVM 在实际装入时有一些灵活性,但必须保持固定的类初始化顺序)。每个装入的类都可能拥有其它所依赖的类,所以装入过程是递归的。清单 2 中的类显示了这一递归装入的工作方式。 Demo
类包含一个简单的 main
方法,它创建了 Greeter
的实例,并调用 greet
方法。 Greeter
构造函数创建了 Message
的实例,随后会在 greet
方法调用中使用它。
清单 2. 类装入演示的源代码
public class Demo
{
public static void main(String[] args) {
System.out.println("**beginning execution**");
Greeter greeter = new Greeter();
System.out.println("**created Greeter**");
greeter.greet();
}
}
public class Greeter
{
private static Message s_message = new Message("Hello, World!"); public void greet() {
s_message.print(System.out);
}
}
public class Message
{
private String m_text; public Message(String text) {
m_text = text;
} public void print(java.io.PrintStream ps) {
ps.println(m_text);
}
}
在 java
命令行上设置参数 -verbose:class
会打印类装入过程的跟踪记录。清单 3 显示了使用这一参数运行清单 2 程序的部分输出:
清单 3. -verbose:class 的部分输出
[Opened /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/sunrsasign.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jsse.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/jce.jar]
[Opened /usr/java/j2sdk1.4.1/jre/lib/charsets.jar]
[Loaded java.lang.Object from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.io.Serializable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.lang.String from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
...
[Loaded java.security.Principal from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.security.cert.Certificate
from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded Demo]
**beginning execution**
[Loaded Greeter]
[Loaded Message]
**created Greeter**
Hello, World!
[Loaded java.util.HashMap$KeySet
from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
[Loaded java.util.HashMap$KeyIterator
from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
这只列出了输出中最重要的部分 ― 完整的跟踪记录由 294 行组成,我删除了其中大部分,形成了这个清单。最初的一组类装入(本例中是 279 个)都是在尝试装入 Demo
类时触发的。这些类是每个 Java 程序(不管有多小)都要使用的核心类。即使删除 Demo main
方法的所有代码也不会影响这个初始的装入顺序。但是不同版本的类库所涉及的类数量和名称都不同。
在上面这个清单中,装入 Demo
类之后的部分更有趣。这里的顺序显示了只有在准备创建 Greeter
类的实例时才会装入该类。不过, Greeter
类使用了 Message
类的静态实例,所以在可以创建 Greeter
类的实例之前,还必须先装入 Message
类。
在装入并初始化类时,JVM 内部会完成许多操作,包括解码二进制类格式、检查与其它类的兼容性、验证字节码操作的顺序以及最终构造java.lang.Class
实例来表示新类。这个 Class
对象成了 JVM 创建新类的所有实例的基础。它还是已装入类本身的标识 ― 对于装入到 JVM 的同一个二进制类,可以有多个副本,每个副本都有其自己的 Class
实例。即使这些副本都共享同一个类名,但对 JVM 而言它们都是独立的类。
非常规(类)路径
装入到 JVM 的类是由 类装入器控制的。JVM 中构建了一个 引导程序类装入器,它负责装入基本的 Java 类库类。这个特殊的类装入器有一些专门的特性。首先,它只装入在引导类路径上找到的类。因为这些是可信的系统类,所以引导程序装入器跳过了对常规(不可信)类所做的大量验证。
引导程序不是唯一的类装入器。对于初学者而言,JVM 为装入标准 Java 扩展 API 中的类定义了一个 扩展类装入器,并为装入一般类路径上的类(包括应用程序类)定义了一个 系统类装入器。应用程序还可以定义它们自己的用于特殊用途(例如运行时类的重新装入)的类装入器。这样添加的类装入器派生自 java.lang.ClassLoader
类(可能是间接派生的),该类对从字节数组构建内部类表示( java.lang.Class
实例)提供了核心支持。每个构造好的类在某种意义上是由装入它的类装入器所“拥有”。类装入器通常保留它们所装入类的映射,从而当再次请求某个类时,能通过名称找到该类。
每个类装入器还保留对父类装入器的引用,这样就定义了类装入器树,树根为引导程序装入器。在需要某个特定类的实例(由名称来标识)时,无论哪个类装入器最初处理该请求,在尝试直接装入该类之前,一般都会先检查其父类装入器。如果存在多层类装入器,那么会递归执行这一步,所以这意味着通常不仅在装入该类的类装入器中该类是 可见的,而且对于所有后代类装入器也都是可见的。这还意味着如果一条链上有多个类装入器可以装入某个类,那么该树最上端的那个类装入器会是实际装入该类的类装入器。
在许多环境中,Java 程序会使用多个应用程序类装入器。J2EE 框架就是一个示例。该框架装入的每个 J2EE 应用程序都需要拥有一个独立的类装入器以防止一个应用程序中的类干扰其它应用程序。该框架代码本身也将使用一个或多个其它类装入器,同样用来防止对应用程序产生的或来自应用程序的干扰。整个类装入器集合形成了树状结构的层次结构,在其每个层次上都可装入不同类型的类。
装入器树
作为类装入器层次结构的实际示例,图 1 显示了 Tomcat servlet 引擎定义的类装入器层次结构。这里 Common 类装入器从 Tomcat 安装的某个特定目录的 JAR 文件进行装入,旨在用于在服务器和所有 Web 应用程序之间共享代码。Catalina 装入器用于装入 Tomcat 自己的类,而 Shared 装入器用于装入 Web 应用程序之间共享的类。最后,每个 Web 应用程序有自己的装入器用于其私有类。
图 1. Tomcat 类装入器
在这种环境中,跟踪合适的装入器以用于请求新类会很混乱。为此,在 Java 2 平台中将 setContextClassLoader
方法和getContextClassLoader
方法添加到了 java.lang.Thread
类中。这些方法允许该框架设置类装入器,使得在运行每个应用程序中的代码时可以将类装入器用于该应用程序。
能装入独立的类集合这一灵活性是 Java 平台的一个重要特性。尽管这个特性很有用,但是它在某些情况中会产生混淆。一个令人混淆的方面是处理 JVM 类路径这样的老问题。例如,在图 1 显示的 Tomcat 类装入器层次结构中,由 Common 类装入器装入的类决不能(根据名称)直接访问由 Web 应用程序装入的类。使这些类联系在一起的唯一方法是通过使用这两个类集都可见的接口。在这个例子中,就是包含由 Java servlet 实现的 javax.servlet.Servlet
。
无论何种原因在类装入器之间移动代码时都会出现问题。例如,当 J2SE 1.4 将用于 XML 处理的 JAXP API 移到标准分发版中时,在许多环境中都产生了问题,因为这些环境中的应用程序以前是依赖于装入它们自己选择的 XML API 实现的。使用 J2SE 1.3,只要在用户类路径中包含合适的 JAR 文件就可以解决该问题。在 J2SE 1.4 中,这些 API 的标准版现在位于扩展的类路径中,所以它们通常将覆盖用户类路径中出现的任何实现。
使用多个类装入器还可能引起其它类型的混淆。图 2 显示了 类身份危机(class identity crisis)的示例,它是在两个独立类装入器都装入一个接口及其相关的实现时产生的危机。即使接口和类的名称和二进制实现都相同,但是来自一个装入器的类的实例不能被认为是实现了来自另一个装入器的接口。图 2 中通过将接口类 I
移至 System 类装入器的空间就可以解除这种混淆。类 A
仍然有两个独立的实例,但它们都实现了同一个接口 I
。
图 2. 类身份危机
结束语
Java 类定义和 JVM 规范一起为运行时组装代码定义了功能极其强大的框架。通过使用类装入器,Java 应用程序能使用多个版本的类,否则这些类就会引起冲突。类装入器的灵活性甚至允许动态地重新装入已修改的代码,同时应用程序继续执行。
这里,Java 平台灵活性在某种程度上是以启动应用程序时较高的开销作为代价的。在 JVM 可以开始执行甚至最简单的应用程序代码之前,它都必须装入数百个独立的类。相对于频繁使用的小程序,这个启动成本通常使 Java 平台更适合于长时间运行的服务器类型的应用程序。服务器应用程序还最大程度地受益于代码在运行时进行组装这种灵活性,所以对于这种开发,Java 平台正日益受宠也就不足为奇了。
在本系列文章的第 2 部分中,我将介绍使用 Java 平台动态基础的另一个方面:反射 API(Reflection API)。反射使执行代码能够访问内部类信息。这可能是构建灵活代码的极佳工具,可以不使用类之间任何源代码链接就能够在运行时将代码挂接在一起。但象使用大多数工具一样,您必须知道何时及如何使用它以获得最大利益。请阅读 Java 编程的动态性第 2 部分以了解有效反射的诀窍和利弊
Java 编程的动态性 第1 部分: 类和类装入--转载的更多相关文章
- Java 编程的动态性,第 8 部分: 用代码生成取代反射--转载
既然您已经看到了如何使用 Javassist 和 BCEL 框架来进行 classworking (请参阅 本系列以前的一组文章), 我将展示一个实际的 classworking 应用程序.这个应用程 ...
- Java 编程的动态性,第 7 部分: 用 BCEL 设计字节码--转载
在本系列的最后三篇文章中,我展示了如何用 Javassist 框架操作类.这次我将用一种很不同的方法操纵字节码——使用 Apache Byte Code Engineering Library (BC ...
- Java编程 的动态性,第 2部分: 引入反射--转载
在“ Java编程的动态性,第1部分,”我为您介绍了Java编程类和类装入.该篇文章介绍了一些Java二进制类格式的相关信息.这个月我将阐述使用Java反射API来在运行时接入和使用一些相同信息的基础 ...
- Java 编程的动态性,第 5 部分: 动态转换类--转载
在第 4 部分“ 用 Javassist 进行类转换”中,您学习了如何使用 Javassist 框架来转换编译器生成的 Java 类文件,同时写回修改过的类文件.这种类文件转换步骤对于做出持久变更是很 ...
- Java 编程的动态性, 第4部分: 用 Javassist 进行类转换--转载
讲过了 Java 类格式和利用反射进行的运行时访问后,本系列到了进入更高级主题的时候了.本月我将开始本系列的第二部分,在这里 Java 类信息只不过是由应用程序操纵的另一种形式的数据结构而已.我将这个 ...
- Java 编程的动态性,第 6 部分: 利用 Javassist 进行面向方面的更改--转载
本系列的 第 4 部分和 第 5 部分讨论了如何用 Javassist 对二进制类进行局部更改.这次您将学习以一种更强大的方式使用该框架,从而充分利用 Javassist 对在字节码中查找所有特定方法 ...
- Java 编程的动态性,第3部分: 应用反射--转载
在 上个月的文章中,我介绍了Java Reflection API,并简要地讲述了它的一些基本功能.我还仔细研究了反射的性能,并且在文章的最后给出了一些指导方针,告诉读者在一个应用程序中何时应该使用反 ...
- 《Java编程思想》笔记 第七章 复用类
1.组合 将其他类的对象引用置于新的类中. 3.继承 extends 从已知的一个类中派生出新的一个类,叫子类.子类实现了父类所有 非私有化 非静态 的属性和方法,并能根据自己的实际需求扩展出新的行为 ...
- Java编程思想里的泛型实现一个堆栈类
觉得作者写得太好了,不得不收藏一下. 对这个例子的理解: //类型参数不能用基本类型,T和U其实是同一类型. //每次放新数据都成为新的top,把原来的top往下压一级,通过指针建立链接. //末端哨 ...
随机推荐
- 抓取锁的sql语句-第五次修改
CREATE OR REPLACE PROCEDURE SOLVE_LOCK AS V_SQL VARCHAR2(3000); --定义 v_sql 接受抓取锁的sql语句V_SQL02 VARCHA ...
- iOSUI基础——懒加载
1.懒加载基本 懒加载——也称为延迟加载,即在需要的时候才加载(效率低,占用内存小).所谓懒加载,写的是其get方法. 注意:如果是懒加载的话则一定要注意先判断是否已经有了,如果没有那么再去进行实例化 ...
- UISegmentControl 、UIStepper
UISegmentControl .UIStepper UISegmentControl 1. UISegmentedControl *segmentControl = [[UISegmentedCo ...
- underscorejs-findWhere学习
2.8 findWhere 2.8.1 语法: _.findWhere(list, predicate) 2.8.2 说明: 对list集合的每个对象依次与predicate对象进行匹配,匹配成功则立 ...
- Js点餐加减数量
<button class="add-on" onclick="chgNum(1,'del')" ><i class="icon-m ...
- thinkphp 内置函数详解
D() 加载Model类M() 加载Model类 A() 加载Action类L() 获取语言定义C() 获取配置值 用法就是 C("这里填写在配置文件里数组的下标")S( ...
- Scrapy使用以及Xpath的一些坑, 再入剁手
scrapy爬虫: https:www.scrapy.org 本篇博客依托的项目: https://github.com/viciousstar/BitcointalkSpider/ 一. Scrap ...
- Java基础学习第一天
================每日必读==================== 写代码: 1.明确需求.我需要实现什么需求? 2.分析思路.我需要怎么实现需求? 3.确定步骤.我的每一部分思路需要使 ...
- 多线程+fork 引发的bug查找
1. 问题描述 某个server SA是一个多线程服务器,主线程会调用fork,再exec生成工作进程SB. 实际上,SA的主线程fork出了一个子线程,但没有执行exec. # ps ajxf | ...
- SCALA中类的继承
慢慢的,回想起以前学习JAVA和C#当中的的类的特性了. 感觉大同小异吧... package com.hengheng.scala class OOPInScala { } class Studen ...