讀古今文學網 > 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章以及後面的章節時,還能引導你更加深入地瞭解平台的能力。