類文件是二進制塊,所以想直接和它打交道不太容易。但有很多時候你會發現必須和類文件交手。
比如說,為了在運行時更好地監控(比如通過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
會顯示訪問權限為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.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
定義域。引用該域的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章以及後面的章節時,還能引導你更加深入地瞭解平台的能力。