首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》5.3 检查类文件

关灯直达底部

类文件是二进制块,所以想直接和它打交道不太容易。但有很多时候你会发现必须和类文件交手。

比如说,为了在运行时更好地监控(比如通过JMX)应用程序,你需要加上额外的公共方法。重新编译和再次部署看起来顺利完成了,但检查管理API时却发现没有那些方法。又进行了几次构建和部署还是没有发现。

为了找出部署问题,你需要检查一下javac产生的类文件是不是你想要的那个。还有时侯你需要研究那些没有源码的类文件,以验证文档中是不是真有你所怀疑的错误。

对于类似的任务,你必须用工具检查类文件的内容。好在标准的Oracle JVM中有javap这个工具,用它来探视类文件内部和反汇编类文件非常得心应手。

我们一开始会先介绍javap,以及为检查类文件而设置的各种基本参数。接下来会讨论方法名称和类型在JVM内部的一些表示方式。然后看一下常量池,它是JVM的“藏宝箱”,对于理解字节码如何工作非常重要。

5.3.1 介绍javap

javap的用处很多,既能看类声明了什么方法,又能输出字节码。我们来看一下javap最简单的用途,在第4章讨论的微博Update上试一下。

$ javap wgjd/ch04/Update.classCompiled from /"Update.java/"public class wgjd.ch04.Update extends java.lang.Object {  public wgjd.ch04.Author getAuthor;  public java.lang.String getUpdateText;  public int hashCode;  public boolean equals(java.lang.Object);  public java.lang.String toString;  wgjd.ch04.Update(wgjd.ch04.Update$Builder, wgjd.ch04.Update);}  

默认情况下,javap会显示访问权限为publicprotected和默认(即包级protected)级别的方法。加上-p选项后还可以显示private方法和域。

5.3.2 方法签名的内部形式

JVM内部用的方法签名和javap显示出来供人阅读的形式不太一样。随着我们对JVM的不断深入,这些内部名称出现将更加频繁。如果你赶时间,可以跳过这一节。但请记住它,因为你可能还要回来参考这些内容。

在紧凑形式中,类型名称是经过压缩的。比如int是用I表示的。这些紧凑形式有时被称为类型描述符。表5-2中是类型描述符的完整列表。

表5-2 类型描述符

描述符 类型B byteC char(16位Unicode字符)DdoubleFfloatIIntJ LongL<类型名称> 引用类型(比如Ljava/lang/String; 用于字符串)S shortZboolean[ array-of

某些情况下,类型描述符可能比类型名称还要长(比如Ljava/lang/Object就比Object长),但类型描述符是完全限定的,所以可以直接解析。

javap还有一个有用的选项-s,可以输出签名的类型描述符,所以你没必要用那个表自己做转换。你可以使用javap高级一些的方法来显示我们之前看过的一些方法的签名:

$ javap -s wgjd/ch04/Update.classCompiled from /"Update.java/"public class wgjd.ch04.Update extends java.lang.Object {  public wgjd.ch04.Author getAuthor;  Signature: Lwgjd/ch04/Author;  public java.lang.String getUpdateText;  Signature: Ljava/lang/String;  public int compareTo(wgjd.ch04.Update);  Signature: (Lwgjd/ch04/Update;)I  public int hashCode;  Signature: I  ...}  

如你所见,方法签名中的所有类型都是用类型描述符表示的。

在下一节中你会看到类型描述符的另一个用途。它会出现在类文件中非常重要的部分——常量池。

5.3.3 常量池

常量池是为类文件中的其他(常量)元素提供快捷访问方式的区域。如果你研究过C或Perl之类的语言,应该知道符号表,对于JVM来说,常量池就类似于符号表。但和其他语言不同,Java没有完全开放对常量池中信息的访问。

为了不纠缠于过多的细节,我们用一个非常简单的例子来演示常量池。下面是一个简单的“游戏围栏”或者叫“演算本”类。我们在这个类的 run里面写一点代码,就可以快速测试Java的语法特性或类库。

代码清单5-5 游戏围栏样例类

package wgjd.ch04;public class ScratchImpl {  private static ScratchImpl inst = null;  private ScratchImpl {  }  private void run {  }  public static void main(String args) {    inst = new ScratchImpl;    inst.run;  }}  

要查看常量池中的信息,可以用javap-v。这个命令还会输出很多其他信息,不过我们只关注常量池中的条目。

如下所示:

#1 = Class             #2                // wgjd/ch04/ScratchImpl#2 = Utf8              wgjd/ch04/ScratchImpl#3 = Class             #4                // java/lang/Object#4 = Utf8              java/lang/Object#5 = Utf8              inst#6 = Utf8              Lwgjd/ch04/ScratchImpl;#7 = Utf8              <clinit>#8 = Utf8               V#9 = Utf8              Code#10 = Fieldref         #1.#11           // wgjd/ch04/ScratchImpl.inst:Lwgjd/ch04/ScratchImpl;#11 = NameAndType      #5:#6            // instance:Lwgjd/ch04/ScratchImpl;#12 = Utf8             LineNumberTable#13 = Utf8             LocalVariableTable#14 = Utf8             <init>#15 = Methodref        #3.#16          // java/lang/Object./"<init>/":V#16 = NameAndType      #14:#8          // /"<init>/":V#17 = Utf8             this#18 = Utf8             run#19 = Utf8             ([Ljava/lang/String;)V#20 = Methodref        #1.#21          // wgjd/ch04/ScratchImpl.run:V#21 = NameAndType      #18:#8          // run:V#22 = Utf8             args#23 = Utf8             [Ljava/lang/String;#24 = Utf8             main#25 = Methodref        #1.#16         // wgjd/ch04/ScratchImpl./"<init>/":V#26 = Methodref        #1. #27        // wgjd/ch04/ScratchImpl.run:([Ljava/lang/String;)V#27 = NameAndType      #18:#19        // run:([Ljava/lang/String;)V#28 = Utf8             SourceFile#29 = Utf8             ScratchImpl.java  

如你所见,常量池中的条目是带有类型的。它们还会相互引用,比如说,一个类型为Class的条目会引用类型为Utf8的条目。而Utf8的条目是个字符串,所以Class条目引用的Utf8条目应该是类的名称。

表5-3是可能出现在常量池中的条目集。在讨论常量池中的条目时,有时会用CONSTANT_前缀,比如CONSTANT_Class

表5-3 常量池条目

名称描述 Class 类常量。引用类的名称(Utf8 条目)Fieldref 定义域。引用该域的ClassNameAndTypeMethodref 定义方法。引用该方法的ClassNameAndTypeInterfaceMethodref 定义接口方法。引用该方法的Class NameAndTypeString 字符串常量。引用保存字符的Utf8常量Integer 整型常量(4字节)Float 浮点常量(4字节)Long 长整型常量(8字节)Double 双精度浮点型常量(8字节)NameAndType 描述名称和类型对。类型引用一个保存类型描述符的Utf8条目Utf8一个表示以Utf8编码的字符的二进制字节流InvokeDynamic (Java 7中新引入的)见5.5节MethodHandle (Java 7中新引入的)描述MethodHandle常量MethodType (Java 7中新引入的)描述MethodType常量

你可以用这个表格从演算类的常量池中看到常量解析的例子。比如条目#10中的Fieldref

要解析一个域,你需要名称、类型,还有它所在的类:#10的值是#1.#11,这就是说常量#11来自类#1。在输出中可以很容易看出#1确实是一个Class类型的常量,并且#11是NameAndType。#1指向ScratchImpl类本身,#11是#5:#6——一个名称为instScratchImpl变量。所以综合来看,#10指向ScratchImpl类内部的自身静态变量inst(你可能已经从清单5-6的输出中猜出来了)。

在类加载过程中的验证环节,有一步是检查类文件中的静态信息是否一致的。前面的例子是运行时在加载新类时要做的完整性检查。

对于类文件的基本结构,我们已经讨论的差不多了。接下来要进入下一话题——字节码。理解源码如何变成字节码会对你理解代码如何运行有很大的帮助。在学习第6章以及后面的章节时,还能引导你更加深入地了解平台的能力。