类文件是二进制块,所以想直接和它打交道不太容易。但有很多时候你会发现必须和类文件交手。
比如说,为了在运行时更好地监控(比如通过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
会显示访问权限为public
、protected
和默认(即包级protected
)级别的方法。加上-p
选项后还可以显示private
方法和域。
5.3.2 方法签名的内部形式
JVM内部用的方法签名和javap
显示出来供人阅读的形式不太一样。随着我们对JVM的不断深入,这些内部名称出现将更加频繁。如果你赶时间,可以跳过这一节。但请记住它,因为你可能还要回来参考这些内容。
在紧凑形式中,类型名称是经过压缩的。比如int
是用I
表示的。这些紧凑形式有时被称为类型描述符。表5-2中是类型描述符的完整列表。
表5-2 类型描述符
B
byte
C
char
(16位Unicode字符)D
double
F
float
I
Int
J
Long
L
<类型名称> 引用类型(比如Ljava/lang/String
; 用于字符串)S
short
Z
boolean
[
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
定义域。引用该域的Class
和 NameAndType
Methodref
定义方法。引用该方法的Class
和NameAndType
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——一个名称为inst
的ScratchImpl
变量。所以综合来看,#10指向ScratchImpl
类内部的自身静态变量inst
(你可能已经从清单5-6的输出中猜出来了)。
在类加载过程中的验证环节,有一步是检查类文件中的静态信息是否一致的。前面的例子是运行时在加载新类时要做的完整性检查。
对于类文件的基本结构,我们已经讨论的差不多了。接下来要进入下一话题——字节码。理解源码如何变成字节码会对你理解代码如何运行有很大的帮助。在学习第6章以及后面的章节时,还能引导你更加深入地了解平台的能力。