名著阅读 > Java程序员修炼之道 > 5.3 检查类文件 >

5.3 检查类文件

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

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

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

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

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

5.3.1 介绍javap

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

$ javap wgjd/ch04/Update.class

Compiled 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 byte C char(16位Unicode字符) Ddouble Ffloat IInt J Long L<类型名称> 引用类型(比如Ljava/lang/String; 用于字符串) S short Zboolean [ array-of

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

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

$ javap -s wgjd/ch04/Update.class
Compiled 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 定义域。引用该域的ClassNameAndType Methodref 定义方法。引用该方法的ClassNameAndType InterfaceMethodref 定义接口方法。引用该方法的Class NameAndType String 字符串常量。引用保存字符的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章以及后面的章节时,还能引导你更加深入地了解平台的能力。