2025-02-09

Java動態的程序設計
第一部分 類和類的裝載

我們來看一下類以及它們被JVM裝載的時候做瞭些什麼?

在這個新的有關動態的Java編程特征的系列文章中,將會看到在正在執行的Java應用程序的背後發生瞭些什麼。企業級Java專傢Dennis Sosnoski給出瞭Java二進制格式和發生在JVM內部的類中的事情。遵循這條路線,他介紹正在裝載的類所影響的范圍(從正在運行的一簡單的Java應用程序所必須的大量的類到在J2EE和類似的復雜的框架結構中類裝載器沖突所可能導致的問題)。

這篇文章揭示瞭Java動態編程這組主題所包含的一系列的新的知識。這些主題包括從Java二進制類文件格式的結構到使用反射訪問運行時的元數據,以及所有的在運行時編輯和構造新的類的方法。貫穿這個材料的全部基本路線是Java平臺的編程思想,是比用其它直接編譯成本地代碼的語言更加動態的工作。如果你理解瞭這些動態的特征,你就可用Java語言做一些用其它的主流的編程語言所不能做的事情。

在這篇文章中。我介紹瞭位於Java平臺的動態特征之下的一些基本概念。這些概念圍繞用於描述Java類的二進制格式,包括類被裝載進JVM(Java虛擬機)時所發生的事情。這篇文件不僅為理解這個系列主題的其它文章提供基礎,同時也演示瞭一些非常實際的在Java平臺上工作的開發人員所關心的事情。

一個類的二進制形式
用Java語言的開發人員通常不必關心通過編譯器運行他們的源代碼時所發生的一些細節問題。在這個系列主題中。我會介紹許多有關從源代碼到可執行的程序這個過程的背後細節,因此,我們先來看一下編譯器所產生的二進制類。

二進制類的格式實際上是被JVM(Java虛擬機)規范定義的。正常的類的描述是一個編譯器利用Java語言的源代碼生成的,並且通常被保存在一以.class為擴展名的文件中。但是這些特征都不是本質的。其它的一些編程語言已經被開發使用Java的二進制類的格式,並且,因為一些目的,新的類的描述被創建並且被直接裝載進一個正在執行的JVM中。但是JVM所關心的,重要的不是這些源代碼或它是怎樣被存儲的,而是這個格式自身。

因此,先來這種類格式看上去象什麼呢?下面(List 1.)列出瞭一個非常短的類的源代碼,緊跟著是用編譯器輸出的這個類文件的一部分十六進制的顯示:
List 1.Hello.java的源代碼和(部分)二進制表示
public class Hello
{
    public static void main(String[] args) {
        System.out.println(“Hello, World!”);
    }
}

0000: cafe babe 0000 002e 001a 0a00 0600 0c09  …………….
0010: 000d 000e 0800 0f0a 0010 0011 0700 1207  …………….
0020: 0013 0100 063c 696e 6974 3e01 0003 2829  …..<init>…()
0030: 5601 0004 436f 6465 0100 046d 6169 6e01  V…Code…main.
0040: 0016 285b 4c6a 6176 612f 6c61 6e67 2f53  ..([Ljava/lang/S
0050: 7472 696e 673b 2956 0c00 0700 0807 0014  tring;)V……..
0060: 0c00 1500 1601 000d 4865 6c6c 6f2c 2057  ……..Hello, W
0070: 6f72 6c64 2107 0017 0c00 1800 1901 0005  orld!………..
0080: 4865 6c6c 6f01 0010 6a61 7661 2f6c 616e  Hello…java/lan
0090: 672f 4f62 6a65 6374 0100 106a 6176 612f  g/Object…java/
00a0: 6c61 6e67 2f53 7973 7465 6d01 0003 6f75  lang/System…ou


二進制的內部
List1中所顯示的二進制類的表示的第一件事情是標識Java二進制類的格式的“café babe”簽名,這個簽名隻是一種確認實際請求的Java類的格式的一個實例的數據塊的簡易方法。每個Java的二進制類,即使在不同的文件系統上,也需要用這四個字節開始。
數據的其它部分不是很有趣。跟在簽名後面是一對類格式的版本號(在這個例子中,用1.4.1javac編譯生成的時候,會產生次版本為0、主版本為46——十六進制的形式是0x2e的版本號),然後是常量池中的條目的計數。跟在條目計數(在這個例子中是26,或0x001a)後面的是實際的常量池數據。這是保存所有類定義所使用的常量的地方。它包括類和方法的名字、簽名以及字符串(這些字符串是你能夠認可的在十六制的存放處的正確性的文本解釋)、以及連同在一起的各種二進制值。
在常量池中項目是可變長度的,每個項目的第一個字節標識瞭項目的類型和它應該怎樣被解碼。我不打算對這些內容做詳細介紹,如果你有興趣以實際的JVM規范開始,這裡有許多有用參考。關鍵點是常量池包含瞭所有的對其它類和這個類所使用的方法的引用,還有這個類自身以及它的方法的實際定義。盡管平均值可能會少一些,但是常量池的大小很容易的超過二進制類的在小的一半或更多。

跟在常量池後面是幾個引用常量池條目的項目,它們是類本身,它的超類以及接口。這些項目的後面是有關字段和方法的信息,這些信息是做為復合結構來描述自己的。對於方法的可執行代碼以代碼屬性(code attributes)的形式被包含在方法的定義中。這種代碼是JVM的指令形式,通常叫做字節碼(bytecode),這是下一節的主題之一。

在Java類的格式中屬性(Attributes)用來做為幾種定義的用途,包括已經提到的字節碼(bytecode),用於字段的常量值,異常處理,以及調試信息。但是,屬性(Attributes)不隻有這些可能的用途。從一開始,JVM規范要求JVMs(Java虛擬機)忽略未知類型的屬性。這種要求對於屬性的使用提供瞭靈活性,使得它在將來能夠服務於其它的用途,例如提供與用戶類一起工作的框架所需要的元信息——這是一種Java源於C#語言所廣泛使用的方法。不幸的是,no hook have yet been provided for making of this flexibility at the user level.

字節碼和堆棧
組成類文件的可執行部分的字節碼是適應特定類型計算機(JVM)是的實際的機器碼,這所以叫做虛擬機是因為它是用軟件來設計實現的,而不是硬件。每個運行在JVM上的應用程序都是建立在這種機器的一種實現。

虛擬機實際上相當的簡單,它使用堆棧結構,這就意味著它們在被使用之前指令操作要被裝載進一個內部的堆棧。指令集包括所有的一般的算術運算和邏輯操作,還有有條件和無條轉移,裝載/存儲,調用/返回,堆棧的維護,以及幾種特殊的指令類型。包括立即數的一些指令被直接編碼進指令,另外一些直接從常量池引用值。

雖然虛擬機是簡單的,但執行起來卻不是這樣的,第一代JVM基本上是虛擬機的字節碼的解析器,相對而言,比較簡單,但卻遇到嚴重的性能問題———解析代碼總是要比執行本地代碼花費更長的時間。為瞭減少這些性能問題,第二代JVM添加瞭即時(JIT)翻譯。JIT技術是在Java字節碼第一次執行之前把它編譯成本地代碼,從而為重復執行提供瞭更好的性能。當前的JVM做的更好,它使用相應的技術來監控程序的執行並且選擇性使使用代碼得到優化。

裝載類
把源代碼編譯成本地代碼的語言(如C和C++)在源代碼被編譯之後通常需要鏈接這樣的步驟。這種鏈接過程把獨立編譯的源文件連同共享類庫的代碼合並到一起,從而形成一個可執行的程序。Java語言是不同的,使用Java語言,編譯器生成的類文件一般情況下單獨保存的,直到它們裝載進一個JVM為止,即使是建立一個JAR文件也不會改變這種情況———JAR文件隻是類文件的一個容器。

優於一個分開的步驟,JVM把類裝載進內存的時候,鏈接類成為JVM所要執行的工作的一部分。這樣就可以在初始化裝載的時候增加一些系統開銷,但是也為Java應用程序提供瞭高級的靈活性。例如,應用程序可以使用直到運行時才知道的實際實現的接口來編寫。這種後期綁定(late binding)的方法來裝配一個應用程序在Java平臺中被廣泛使用,servlets就是一個普通的例子。

對於裝載類的規則在JVM規范的細節中被清楚的說明瞭。基本原則是類隻有在需要的時候才被裝載(或者至少是顯示的裝載,JVM的這種方法在實際裝載過程中有一些靈活性,但是必需保持一個固定的類初始化的順序)。每個被裝載的類可以有其它的它所依賴的類,因此裝載過程是遞歸的。在Listing 2中的類顯示瞭這種遞歸裝載是怎樣工作的。這個Demo類包含瞭一個簡單的創建Greeter類的一個實例並且調用這個類的greet方法的main方法。Greeter類的構造器創建瞭一個Message的實例,然後它在greet方法中使用這個Message實例。

Listing 2用於類裝載演示的源碼
public class Demo
{
    public static void main(String[] args) {
        System.out.println(“**beginning execution**”);
        Greeter greeter = new Greeter();
        System.out.println(“**created Greeter**”);
  

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *