posts - 188,comments - 176,trackbacks - 0

          第一部分 類和類的裝載

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

          在這個新的有關(guān)動態(tài)的Java編程特征的系列文章中,將會看到在正在執(zhí)行的Java應(yīng)用程序的背后發(fā)生了些什么。企業(yè)級Java專家Dennis Sosnoski給出了Java二進(jìn)制格式和發(fā)生在JVM內(nèi)部的類中的事情。遵循這條路線,他介紹正在裝載的類所影響的范圍(從正在運行的一簡單的Java應(yīng)用程序所必須的大量的類到在J2EE和類似的復(fù)雜的框架結(jié)構(gòu)中類裝載器沖突所可能導(dǎo)致的問題)。

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

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

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

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

          因此,先來這種類格式看上去象什么呢?下面(List 1.)列出了一個非常短的類的源代碼,緊跟著是用編譯器輸出的這個類文件的一部分十六進(jìn)制的顯示:
          List 1.Hello.java的源代碼和(部分)二進(jìn)制表示
          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  ........()
          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
          ...

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

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

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

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

          虛擬機實際上相當(dāng)?shù)暮唵危褂枚褩=Y(jié)構(gòu),這就意味著它們在被使用之前指令操作要被裝載進(jìn)一個內(nèi)部的堆棧。指令集包括所有的一般的算術(shù)運算和邏輯操作,還有有條件和無條轉(zhuǎn)移,裝載/存儲,調(diào)用/返回,堆棧的維護(hù),以及幾種特殊的指令類型。包括立即數(shù)的一些指令被直接編碼進(jìn)指令,另外一些直接從常量池引用值。

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

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

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

          對于裝載類的規(guī)則在JVM規(guī)范的細(xì)節(jié)中被清楚的說明了。基本原則是類只有在需要的時候才被裝載(或者至少是顯示的裝載,JVM的這種方法在實際裝載過程中有一些靈活性,但是必需保持一個固定的類初始化的順序)。每個被裝載的類可以有其它的它所依賴的類,因此裝載過程是遞歸的。在Listing 2中的類顯示了這種遞歸裝載是怎樣工作的。這個Demo類包含了一個簡單的創(chuàng)建Greeter類的一個實例并且調(diào)用這個類的greet方法的main方法。Greeter類的構(gòu)造器創(chuàng)建了一個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**");
                  greeter.greet();
              }
          }

          public class Greeter
          {
              private static Message s_message = new Message("Hello, World!");
              
              public void greet() {
                  s_message.print(System.out);
              }
          }

          public class Message
          {
              private String m_text;
              
              public Message(String text) {
                  m_text = text;
              }
              
              public void print(java.io.PrintStream ps) {
                  ps.println(m_text);
              }
          }

          設(shè)置java命令的命令行參數(shù)為-verbose:class,這樣就可打印類裝載過程的軌跡。Listing 3顯示了使用這個參數(shù)的來運行Listing 2時的部分輸出:
          [Opened /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
          [Opened /usr/java/j2sdk1.4.1/jre/lib/sunrsasign.jar]
          [Opened /usr/java/j2sdk1.4.1/jre/lib/jsse.jar]
          [Opened /usr/java/j2sdk1.4.1/jre/lib/jce.jar]
          [Opened /usr/java/j2sdk1.4.1/jre/lib/charsets.jar]
          [Loaded java.lang.Object from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
          [Loaded java.io.Serializable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
          [Loaded java.lang.Comparable from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
          [Loaded java.lang.CharSequence from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
          [Loaded java.lang.String from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
          ...
          [Loaded java.security.Principal from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
          [Loaded java.security.cert.Certificate 
            from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
          [Loaded Demo]
          **beginning execution**
          [Loaded Greeter]
          [Loaded Message]
          **created Greeter**
          Hello, World!
          [Loaded java.util.HashMap$KeySet 
            from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]
          [Loaded java.util.HashMap$KeyIterator 
            from /usr/java/j2sdk1.4.1/jre/lib/rt.jar]

          這里只列出了最重要的部分———全部的裝載軌跡由294行組成。試圖裝載Demo類所要觸發(fā)的需要裝載的初始的類的有279個(在這個例子中),這些是每個Java程序所使用的核心類,不管這個類的代碼如何的小,即使去掉Demo類main方法中的所有的代碼,也不會影響這個初始化的裝載順序。但是,裝載類的數(shù)量和名字與使用類庫的版本不同有關(guān)。

          在Demo類被裝載之后所列出的那一部分更加有趣。這里所顯示的順序是Greeter類只有這個類要被創(chuàng)建的時候才被裝載,但是Greeter類使用了Message類的一個靜態(tài)實例,所以,在創(chuàng)建一個Greeter類的實例之前,后面的Message類也需要被裝載。

          當(dāng)一個Java類被裝載和初始化時。在JVM內(nèi)部發(fā)生很事情,包括對二進(jìn)制類格式進(jìn)行解碼,檢查與其它類的兼容性,確認(rèn)字節(jié)碼操作的順序,并最終創(chuàng)建一個java.lang.class的實例來描述這個新類。這個類對象成為JVM創(chuàng)建的這個新類的所有實例的基礎(chǔ)。同時它也標(biāo)識了被裝載類的自身———你可以在一個JVM中裝載同樣的二進(jìn)制類的多個副本,每個都擁有它們本身的類的實例。盡管這些副本共享著同樣的類名,但是對于JVM來說他們獨立的類。

          Off the beaten (class) path

          裝載進(jìn)JVM中的類是通過類裝載器來控制的,有一個bootstrap類裝載器建立在JVM的內(nèi)部,它負(fù)責(zé)裝載基本的Java類庫中的類。這個特殊的類裝載器有一些專用的特征。首先,它只基于根類路徑來裝載類。因為這些是系統(tǒng)類所信賴的,bootstap裝載器跳過那些確認(rèn)為不信賴的類。
          Bootstrap不僅是一個類裝載器。.對于一個起動器,JVM為了從標(biāo)準(zhǔn)的Java擴(kuò)展API中裝載類定義了一個擴(kuò)展的類裝載器,并且為了從一般的類路徑中(包括應(yīng)用程序的類)裝載類還定義了一個系統(tǒng)類裝載器。應(yīng)用程序為了特殊的目的也可定義它們自己的類裝載器(例如運行時類的重載)。這種被添加的類裝載器繼承于java.lang.ClassLoader類(也可能是間接的),這種方式為用一個字節(jié)數(shù)組來建立一個內(nèi)部的類描述(一個java.lang.Class的實例)提供了核心的支持。每人被創(chuàng)建的類都能夠做到被裝載它的類裝載器所感知擁有。類裝載器通常保持一個它們所裝載的類的映射,如果這些類被再次請求,通過名字就能夠找到它。
          每個類裝載器也保持著一個對父類類裝載器的引用,以bootstrap裝載器為根定義了類裝載器的樹。當(dāng)一個特殊的類的實例被請求時(通過定義名字來請求),最初,無論哪一個類裝載器處理這個請求,通常情況下,在首次嘗試直接裝載這個類之前,都要與它們的父類裝載器進(jìn)行協(xié)商。如果有多層類裝載器,就要這樣遞歸申請,這就意味著一個類在裝載它的類裝載器中將是不可見的,而且對它所有的子類裝載器也是不可見的。它也意味著如果在一個鏈中的一個類能夠被多個類裝載器裝載,那么距這個類裝載的樹最上層的那個類才是實際裝載這個類的類裝載器。

          有許多多個應(yīng)用程序類裝載器被Java程序使用的情況,一個例子就是在J2EE框架內(nèi),每個能框架來裝載的應(yīng)用程序,都需要一個獨立的類裝載器來防止應(yīng)用程序間類的干擾。框架代碼自身也會使用一個或多其它的類裝載器,來防止應(yīng)用程序間的沖突。一套完整的類裝載器組成了一個樹形結(jié)構(gòu)的層次以便在不同的層次上裝載不同類型的類。

          裝載器的樹形結(jié)構(gòu)
          做為一個類裝載器層次描述的例子,F(xiàn)igure 1顯示了Tomcat的servlet引擎所定義的類裝載器的層次結(jié)構(gòu)。Common類裝載器從Tomcat的安裝目錄中的JAR文件中裝載那些打算在服務(wù)器和Web應(yīng)用程序之間共享的代碼。Catalina裝載器是Tomcat自已的類,Share裝載器用于Web應(yīng)用間共享的類。最后,每個Web應(yīng)用程序都有它們自己的裝載器做為它們的私有類。
          Figure 1.Tomcat 類裝載器
           
          在這種環(huán)境類型下,保存所使用的正確的裝載器的軌跡對于正在請求的一個新類來說可能是雜亂無章的。因為這樣,所以在Java2平臺中的java.lang.Thread類添加了setContextClassLoader和getContextClassLoader方法,這些方法讓框架可以為每個應(yīng)用程序在運行來自應(yīng)用程序的代碼時設(shè)置所使用的類裝載器。

          能夠裝載獨立的類的集合的靈活性是Java平臺的一個重要特征。盡管這個特征是有益的,但它卻能在一些實例中產(chǎn)生混亂。其中之一就是連續(xù)的處理JVM的類路徑的問題。例如,在Figure 1中所顯示的Tomcat中的類裝載器的層次關(guān)系中,被Common類裝載器所裝載的類將不能夠直接被Web application裝載的類來訪問(通過名字)。把這兩個類裝載器結(jié)合到一起的僅的方法是通過使用接口使用雙方的類的集合彼此可見,在這人案例中,通過java servlets實現(xiàn)的java.servlet.Servlet類包含了這種方法。

          當(dāng)因為一些原因在類裝載器之間移動代碼時,就可能產(chǎn)生一些問題,例如,當(dāng)J2SE1.4把處理XML的JAXP API    移到標(biāo)準(zhǔn)的發(fā)布版中時,對于那些先前的信賴它們自己XML API的實現(xiàn)應(yīng)用程序來說,就會產(chǎn)生一些問題。在使用J2SE1.3的情況下,能夠通過在用戶的類路徑中包含相應(yīng)的JAR文件就能訪問自己的API,但在J2SE1.4中,當(dāng)前的APIs的標(biāo)準(zhǔn)版中這個裝載器是在擴(kuò)展類路徑中,因些一般情況下,它將會不管出現(xiàn)在用戶類路徑的任何實現(xiàn)。

          當(dāng)使用多個類裝載器時,也可能有其它類型的混亂。Figure 2顯示了一個identity crisis類在一個接口和相關(guān)聯(lián)的實現(xiàn)獨立的類裝載器分別裝載時所出現(xiàn)的結(jié)果。盡管接口和類的名字以及接口是相同的,但是,但是來自于一個裝載器的類的實例卻不能被來自另一個裝載器的正在執(zhí)行的接口所承認(rèn)。這種混亂能夠通過(如Figure 2中所示)把類I的接口移入System類的裝載器空間來解決,雖然這樣依然有兩個獨立的類A的實例,但是它們都實現(xiàn)相同的接口I。

          Figure 2類identity crisis
           

          Java類定義和JVM規(guī)范一起為運行時的代碼匯編定義了一個非常強大的框架。通過使用類裝載器Java應(yīng)用程序能夠和類的多個版本一起工作,否則就會產(chǎn)生沖突。類裝載器的這種靈活性甚至充許在一個應(yīng)用程序連續(xù)執(zhí)行的時候動態(tài)的重新裝載被編輯的代碼。
          Java平臺在這方面的靈活所付出的代價是在啟動應(yīng)用程序時要付出更的系統(tǒng)開銷。在應(yīng)用程序(即使是最小的應(yīng)用程序代碼)開始執(zhí)行之前,幾百個獨立的類需要JVM來裝載。一般情況下,這種起動成本使得Java平臺更加適應(yīng)長時間運行的服務(wù)器類型的應(yīng)用程序,而不適應(yīng)于那些經(jīng)常使用的小程序。服務(wù)器應(yīng)用程序也從運行時的代碼匯編的靈活性中獲取最大的好處,因此,Java平臺成為日益流行的開發(fā)平臺是不足為奇的。

          在這個系列的第2部分中,將包含一個使用Java平臺的動態(tài)機制基礎(chǔ)的另一個特點的介紹:這就是Reflection API。Reflection讓你的正在執(zhí)行的代碼訪問內(nèi)部的類的信息。這種機制是創(chuàng)建靈活的代碼的一個強大的工具,它能夠在運行時沒有必需的任何連接類之間的源代碼的前提下把這些代碼鏈接在一起。但是做是最有價值的工具,你需要知道什么時候和怎樣來使用以獲取最大的收益。

          轉(zhuǎn)http://www.csai.cn/

          posted on 2007-05-24 11:02 cheng 閱讀(199) 評論(0)  編輯  收藏 所屬分類: JBS
          主站蜘蛛池模板: 陇南市| 阜平县| 昌图县| 祥云县| 贵州省| 东平县| 兖州市| 略阳县| 呼伦贝尔市| 永定县| 盐津县| 凤凰县| 阳江市| 商城县| 济南市| 来安县| 新河县| 威宁| 郸城县| 东乡县| 怀来县| 南溪县| 凤城市| 油尖旺区| 龙南县| 宜君县| 铁力市| 万源市| 长白| 胶南市| 澄迈县| 皋兰县| 霸州市| 双鸭山市| 应城市| 云和县| 海阳市| 双柏县| 皮山县| 宣汉县| 蓬安县|