日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【深入理解JVM】ClassLoader类加载机制

發(fā)布時間:2024/4/14 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【深入理解JVM】ClassLoader类加载机制 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

Java程序并不是一個原生的可執(zhí)行文件,而是由許多獨立的類文件組成,每一個文件對應一個Java類。此外,這些類文件并非立即全部裝入內存的,而是根據(jù)程序需要裝入內存。ClassLoader專門負責類文件裝入到內存。

在class文件中描述的各種信息,最終都需要被加載到虛擬機中之后,才能被運行和使用。虛擬機把描述類的數(shù)據(jù)從class文件加載到內存,并對數(shù)據(jù)進行校驗,轉換,解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制

總結起來ClassLoader的作用:

(1)加載class文件進入JVM
(2)審查每個類應該由誰加載,采用雙親委托機制
(3)將class字節(jié)碼重新解析成JVM要求的對象格式

1、JVM的四種類加載器

類加載器:通過一個類的全限定名來獲取描述此類的二進制字節(jié)流,實現(xiàn)這個動作的代碼模塊稱為“類加載器”

從上圖我們就可以看出類加載器之間的父子關系(注意不是類的集繼承關系)。

Bootstrap ClassLoader:BootStrap 是最頂層的類加載器,它是由C++編寫并且已經(jīng)內嵌到JVM中了,主要用來讀取Java的核心類庫JRE/lib/rt.jar。

Extension ClassLoader:負責加載java平臺中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包。
App ClassLoader(System ClassLoader):負責記載classpath中指定的jar包及目錄中class。

Custom ClassLoader:屬于應用程序根據(jù)自身需要自定義的ClassLoader,如tomcat、jboss都會根據(jù)j2ee規(guī)范自行實現(xiàn)ClassLoader。

如果我們要實現(xiàn)自己的類加載器,不管是直接繼承ClassLoader還是繼承URLclassLoaderlei ,它的父加載器都是AppClassLoader,因為不管調用哪個父類構造器,創(chuàng)建的對象都必須最終調用getSystemClassLoader()作為父類加載器,而該方法獲取的正是AppClassLoader。

如果應用中沒有定義其他的類加載器,那么除了java.ext.dirs下的類是由ExtClassLoader來加載,其他的都是由AppClassLoader來加載。

下面問題就來了,JVM有四種類型的類加載器,java是如何區(qū)分一個類該由哪個類加載器來完成呢

在這里java采用了雙親委托機制,這個機制簡單來講,就是“類裝載器有載入類的需求時,會先請示其Parent使用其搜索路徑幫忙載入,如果Parent 找不到,那么才由自己依照自己的搜索路徑搜索類”。具體流程如下:

1、”A類加載器”加載類時,先判斷該類是否已經(jīng)加載過了;

2、如果還未被加載,則首先委托其”A類加載器”的”父類加載器”去加載該類,這是一個向上不斷搜索的過程,當A類所有的”父類加載器”(包括bootstrap classloader)都沒有加載該類,則回到發(fā)起者”A類加載器”去加載;

3、如果還加載不了,則拋出ClassNotFoundException。

2、ClassLoader抽象類的幾個關鍵方法

loadClass()
此方法負責加載指定名字的類,ClassLoader的實現(xiàn)方法為先從已經(jīng)加載的類中尋找,如沒有則繼續(xù)從parent ClassLoader中尋找,如仍然沒找到,則從System ClassLoader中尋找,最后再調用findClass方法來尋找,如要改變類的加載順序,則可覆蓋此方法

findLoadedClass()
此方法負責從當前ClassLoader實例對象的緩存中尋找已加載的類,調用的為native的方法。

findClass()
此方法直接拋出ClassNotFoundException,因此需要通過覆蓋loadClass或此方法來以自定義的方式加載相應的類。

findSystemClass()
此方法負責從SystemClassLoader中尋找類,如未找到,則繼續(xù)從Bootstrap ClassLoader中尋找,如仍然為找到,則返回null。

defineClass()
此方法負責將二進制的字節(jié)碼轉換為Class對象

resolveClass()
此方法負責完成Class對象的鏈接,如已鏈接過,則會直接返回。

Class.forName()與ClassLoader.loadClass()
這兩方法都可以通過一個給定的類名去定位和加載這個類名對應的 java.lang.Class 類對象,區(qū)別如下:

  • 初始化
    Class.forName()會對類初始化,而loadClass()只會裝載或鏈接。ClassLoader.loadClass()加載的類對象是在第一次被調用時才進行初始化的。可以利用上述的差異。比如,要加載一個靜態(tài)初始化開銷很大的類,就可以選擇提前加載該類(以確保它在classpath下),但不進行初始化,直到第一次使用該類的域或方法時才進行初始化。

  • 類加載器可能不同
    Class.forName(String)方法(只有一個參數(shù)),使用調用者的類加載器來加載, 也就是用加載了調用forName方法的代碼的那個類加載器。當然,它也有個重載的方法,可以指定加載器。相應的,ClassLoader.loadClass()方法是一個實例方法(非靜態(tài)方法)調用時需要自己指定類加載器,那么這個類加載器就可能是也可能不是加載調用代碼的類加載器(調用代碼類加載器通getClassLoader0()獲得)

  • 3、類的加載過程

    類的加載要經(jīng)過三步:裝載(Load),鏈接(Link),初始化(Initializ)。其中鏈接又可分為校驗(Verify),準備(Prepare),解析(Resolve)三步。

    三個階段

    (1)第一階段:找到class文件并把這個文件包含的字節(jié)碼加載到內存

    (2)第二階段:字節(jié)碼驗證,class類數(shù)據(jù)結構分析以及相應內存分配,符號表的鏈接。

    (3)第三階段:類中靜態(tài)屬性初始化賦值以及靜態(tài)代碼塊的執(zhí)行


    總結起來,類從被加載到虛擬機內存開始,到卸載出內存為止,其生命周期包括:加載(loading),驗證(verification),準備(preparation),解析(resolution),初始化(initialization),使用(using),卸載(unloading)

    3.1 加載

    ClassLoader就是用來裝載的。通過指定的className,找到二進制碼,生成Class實例,放到JVM中。
    在加載階段,虛擬機需要完成以下三件事(虛擬機規(guī)范對這三件事的要求并不具體,因此虛擬機實現(xiàn)與具體應用的靈活度相當大):

    1,通過一個類的全限定名獲取定義這個類的二進制流
    可以從jar包、ear包、war包中獲取,可以從網(wǎng)絡中獲取(Applet),可以運行時生成(動態(tài)代理),可以通過其它文件生成(Jsp)等。

    2,將這個字節(jié)流代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時數(shù)據(jù)結構。

    3,在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口。

    在抽象類ClassLoader中沒有定義如何去加載,如何去找到指定類并加載到內存中需要子類中去實現(xiàn),即實現(xiàn)findClass()方法。如在URLClassLoader中通過一個URLClassPath類幫組取得要加載的class文件字節(jié)流,而這個URLClassPath定義了到哪里去找這個class文件,如果找到這個class文件,再讀取byte字節(jié)流通過調用defineClass方法來創(chuàng)建類對象。

    JVM加載Class文件到內存的兩種方式:

    (1)隱式加載:在代碼中不通過調用ClassLoader來加載需要的的類,而是通過JVM自動加載需要的類到內存方式。如我們類繼承或類中引用其他類的時候,JVM在解析當前這個類的時候發(fā)現(xiàn)引用類不在內存中就會去自動的將這些類加載進入到內存。

    (2)顯示加載:在代碼中通過調用ClassLoader類來加載類。如this.getClass().getClassLoader().loadClass()或者Class.forName()或者我們自己實現(xiàn)的findClass()方法。

    比如:數(shù)組類的 Class 對象不是由類加載器創(chuàng)建的,而是由 Java 運行時根據(jù)需要自動創(chuàng)建。數(shù)組類的類加載器由 Class.getClassLoader() 返回,該加載器與其元素類型的類加載器是相同的;如果該元素類型是基本類型,則該數(shù)組類沒有類加載器。

    3.2 鏈接

    鏈接就是把load進來的class合并到JVM的運行時狀態(tài)中。可以把它分成三個主要階段:

    3.2.1 校驗
    對二進制字節(jié)碼的格式進行校驗,以確保格式正確、行為正確。這一階段主要是為了確保Class文件的字節(jié)流中包含的信息復合當前虛擬機的要求,并且不會危害虛擬機自身的安全。主要驗證過程包括:

    1.文件格式驗證:驗證字節(jié)流文件是否符合Class文件格式的規(guī)范,并且能被當前虛擬機正確的處理。

    2.元數(shù)據(jù)驗證:是對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合Java語言的規(guī)范。

    3.字節(jié)碼驗證:主要是進行數(shù)據(jù)流和控制流的分析,保證被校驗類的方法在運行時不會危害虛擬機。

    4.符號引用驗證:符號引用驗證發(fā)生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在解析階段中發(fā)生。

    3.2.2 準備
    準備類中定義的字段、方法和實現(xiàn)接口所必需的數(shù)據(jù)結構。比如會為類中的靜態(tài)變量賦默認值(int等:0, reference:null, char:’\u0000’)。
    準備階段正式為類變量分配內存并設置初始值。這里的初始值并不是初始化的值,而是數(shù)據(jù)類型的默認零值。這里提到的類變量是被static修飾的變量,而不是實例變量。關于準備階段為類變量設置零值的唯一例外就是當這個類變量同時也被final修飾,那么在編譯時,就會直接為這個常量賦上目標值。

    如:
    pirvate static int size = 12;
    那么在這個階段,size的值為0,而不是12。 final修飾的類變量將會賦值成真實的值。

    3.2.3 解析
    虛擬機將常量池內的符號引用替換為直接引用的過程。裝入類所引用的其他所有類,虛擬機將常量池中的符號引用替換為直接引用。可以用許多方式引用類:超類、接口、字段、方法簽名、方法中使用的本地變量。

    3.3 初始化
    到初始化階段,才真正開始執(zhí)行類中定義的java程序代碼;

    在準備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始值,在初始化階段,則是根據(jù)程序員通過程序的主觀計劃區(qū)初始化類變量和其他資源

    類初始化前,它的直接父類一定要先初始化(遞歸),但它實現(xiàn)的接口不需要先被初始化。類似的,接口在初始化前,父接口不需要先初始化。

    有且只有4種情況必須立即對類進行初始化

    1,遇到new(使用new關鍵字實例化對象)、getstatic(獲取一個類的靜態(tài)字段,final修飾符修飾的靜態(tài)字段除外)、putstatic(設置一個類的靜態(tài)字段,final修飾符修飾的靜態(tài)字段除外)和invokestatic(調用一個類的靜態(tài)方法)這4條字節(jié)碼指令時,如果類還沒有初始化,則必須首先對其初始化

    2,使用java.lang.reflect包中的方法對類進行反射調用時,如果類還沒有初始化,則必須首先對其初始化

    3,當初始化一個類時,如果其父類還沒有初始化,則必須首先初始化其父類

    4,當虛擬機啟動時,需要指定一個主類(main方法所在的類),虛擬機會首選初始化這個主類
    除了上面這4種方式,所有引用類的方式都不會觸發(fā)初始化,稱為被動引用。如:通過子類引用父類的靜態(tài)字段,不會導致子類初始化;通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化;引用類的靜態(tài)常量不會觸發(fā)定義常量的類的初始化,因為常量在編譯階段已經(jīng)被放到常量池中了。

    附錄一:Java字節(jié)碼(Java bytecode)

    為了保證WORA,JVM使用Java字節(jié)碼這種介于Java和機器語言之間的中間語言。字節(jié)碼是部署Java代碼的最小單位。

    在解釋Java字節(jié)碼之前,我們先通過實例來簡單了解它。這個案例是一個在開發(fā)環(huán)境出現(xiàn)的真實案例的總結。

    現(xiàn)象:一個一直運行正常的應用突然無法運行了。在類庫被更新之后,返回下面的錯誤。

    Exception in thread "main" java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V at com.nhn.service.UserService.add(UserService.java:14) at com.nhn.service.UserService.main(UserService.java:19)

    應用的代碼如下,而且它沒有被改動過:

    // UserService.java public void add(String userName) { admin.addUser(userName); }

    更新后的類庫的源代碼和原始的代碼如下:

    // UserAdmin.java - Updated library source code public User addUser(String userName) { User user = new User(userName); User prevUser = userMap.put(userName, user); return prevUser; } // UserAdmin.java - Original library source code public void addUser(String userName) { User user = new User(userName); userMap.put(userName, user); }

    簡而言之,之前沒有返回值的addUser()被改修改成返回一個User類的實例的方法。不過,應用的代碼沒有做任何修改,因為它沒有使用addUser()的返回值。咋一看,com.nhn.user.UserAdmin.addUser()方法似乎仍然存在,如果存在的話,那么怎么還會出現(xiàn)NoSuchMethodError的錯誤呢?

    原因:上面問題的原因是在于應用的代碼沒有用新的類庫來進行編譯。換句話來說,應用代碼似乎是調了正確的方法,只是沒有使用它的返回值而已。不管怎樣,編譯后的class文件表明了這個方法是有返回值的。你可以從下面的錯誤信息里看到答案。

    java.lang.NoSuchMethodError:com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V

    NoSuchMethodError出現(xiàn)的原因是“com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V”方法找不到。注意一下”Ljava/lang/String;”和最后面的“V”。在Java字節(jié)碼的表達式里,”L;”表示的是類的實例。這里表示addUser()方法有一個java/lang/String的對象作為參數(shù)。在這個類庫里,參數(shù)沒有被改變,所以它是正常的。最后面的“V”表示這個方法的返回值。在Java字節(jié)碼的表達式里,”V”表示沒有返回值(Void)。綜上所述,上面的錯誤信息是表示有一個java.lang.String類型的參數(shù),并且沒有返回值的com.nhn.user.UserAdmin.addUser方法沒有找到。

    因為應用是用之前的類庫編譯的,所以返回值為空的方法被調用了。但是在修改后的類庫里,返回值為空的方法不存在,并且添加了一個返回值為“Lcom/nhn/user/User”的方法。因此,就出現(xiàn)了NoSuchMethodError。

    注:這個錯誤出現(xiàn)的原因是因為開發(fā)者沒有用新的類庫來重新編譯應用。不過,出現(xiàn)這種問題的大部分責任在于類庫的提供者。這個public的方法本來沒有返回值的,但是后來卻被修改成返回User類的實例。很明顯,方法的簽名被修改了,這也表明了這個類庫的后向兼容性被破壞了。因此,這個類庫的提供者應該告知使用者這個方法已經(jīng)被改變了。

    我們再回到Java字節(jié)碼上來。Java字節(jié)碼是JVM很重要的部分。JVM是模擬執(zhí)行Java字節(jié)碼的一個模擬器。Java編譯器不會直接把高級語言(例如C/C++)編寫的代碼直接轉換成機器語言(CPU指令);它會把開發(fā)者可以理解的Java語言轉換成JVM能夠理解的Java字節(jié)碼。因為Java字節(jié)碼本身是平臺無關的,所以它可以在任何安裝了JVM(確切地說,是相匹配的JRE)的硬件上執(zhí)行,即使是在CPU和OS都不相同的平臺上(在Windows PC上開發(fā)和編譯的字節(jié)碼可以不做任何修改就直接運行在Linux機器上)。編譯后的代碼的大小和源代碼大小基本一致,這樣就可以很容易地通過網(wǎng)絡來傳輸和執(zhí)行編譯后的代碼。

    Java class文件是一種人很難去理解的二進文件。為了便于理解它,JVM提供者提供了javap,反匯編器。使用javap產(chǎn)生的結果是Java匯編語言。在上面的例子中,下面的Java匯編代碼是通過javap -c對UserServiceadd()方法進行反匯編得到的。

    public void add(java.lang.String); Code: 0: aload_0 1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin; 4: aload_1 5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)V 8: return

    invokeinterface:調用一個接口方法在這段Java匯編代碼中,addUser()方法是在第四行的“5:invokevitual#23″進行調用的。這表示對應索引為23的方法會被調用。索引為23的方法的名稱已經(jīng)被javap給注解在旁邊了。invokevirtual是Java字節(jié)碼里調用方法的最基本的操作碼。在Java字節(jié)碼里,有四種操作碼可以用來調用一個方法,分別是:invokeinterface,invokespecial,invokestatic以及invokevirtual。操作碼的作用分別如下:

    invokespecial: 調用一個初始化方法,私有方法或者父類的方法
    invokestatic:調用靜態(tài)方法
    invokevirtual:調用實例方法

    Java字節(jié)碼的指令集由操作碼和操作數(shù)組成。類似invokevirtual這樣的操作數(shù)需要2個字節(jié)的操作數(shù)。

    用更新的類庫來編譯上面的應用代碼,然后反編譯它,將會得到下面的結果。

    public void add(java.lang.String); Code: 0: aload_0 1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin; 4: aload_1 5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User; 8: pop 9: return

    你會發(fā)現(xiàn),對應索引為23的方法被替換成了一個返回值為”Lcom/nhn/user/User”的方法。在上面的反匯編代碼里,代碼前面的數(shù)字代碼什么呢?

    它表示的是字節(jié)數(shù)。大概這就是為什么運行在JVM上面的代碼成為Java“字節(jié)”碼的原因。簡而言之,Java字節(jié)碼指令的操作碼,例如aload_0,getfield和invokevirtual等,都是用一個字節(jié)的數(shù)字來表示的(aload_0=0x2a,getfield=0xb4,invokevirtual=0xb6)。由此可知Java字節(jié)碼指令的操作碼最多有256個。

    aload_0和aload_1這樣的指令不需要任何操作數(shù)。因此,aload_0指令的下一個字節(jié)是下一個指令的操作碼。不過,getfield和invokevirtual指令需要2字節(jié)的操作數(shù)。因此,getfiled的下一條指令是跳過兩個字節(jié),寫在第四個字節(jié)的位置上的。十六進制編譯器里查看字節(jié)碼的結果如下所示。

    2a b4 00 0f 2b b6 00 17 57 b1

    表一:Java字節(jié)碼中的類型表達式在Java字節(jié)碼里,類的實例用字母“L;”表示,void 用字母“V”表示。通過這種方式,其他的類型也有對應的表達式。下面的表格對此作了總結。

    下面的表格給出了字節(jié)碼表達式的幾個實例。

    表二:Java字節(jié)碼表達式范例

    附錄二 Class文件格式

    在講解Java class文件格式之前,我們先看看一個在Java Web應用中經(jīng)常出現(xiàn)的問題。

    現(xiàn)象:當我們編寫完jsp代碼,并且在Tomcat運行時,Jsp代碼沒有正常運行,而是出現(xiàn)了下面的錯誤。

    Servlet.service() for servlet jsp threw exception org.apache.jasper.JasperException: Unable to compile class for JSP Generated servlet error: The code of method _jspService(HttpServletRequest, HttpServletResponse) is exceeding the 65535 bytes limit"

    原因:在不同的Web服務器上,上面的錯誤信息可能會有點不同,不過有有一點肯定是相同的,它出現(xiàn)的原因是65535字節(jié)的限制。這個65535字節(jié)的限制是JVM規(guī)范里的限制,它規(guī)定了一個方法的大小不能超過65535字節(jié)。

    下面我會更加詳細地講解這個65535字節(jié)限制的意義以及它出現(xiàn)的原因。

    Java字節(jié)碼里的分支和跳轉指令分別是”goto”和”jsr”。

    goto [branchbyte1] [branchbyte2] jsr [branchbyte1] [branchbyte2]

    這兩個指令都接收一個2字節(jié)的有符號的分支跳轉偏移量做為操作數(shù),因此偏移量最大只能達到65535。不過,為了支持更多的跳轉,Java字節(jié)碼提供了”goto_w”和”jsr_w”這兩個可以接收4字節(jié)分支偏移的指令。

    goto_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4] jsr_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]

    有了這兩個指令,索引超過65535的分支也是可用的。因此,Java方法的65535字節(jié)的限制就可以解除了。不過,由于Java class文件的更多的其他的限制,使得Java方法還是不能超過65535字節(jié)。

    為了展示其他的限制,我會簡單講解一下class 文件的格式。

    Java class文件的大致結構如下:

    ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count];}

    之前反匯編的UserService.class文件反匯編的結果的前16個字節(jié)在十六進制編輯器中如下所示:
    ca fe ba be 00 00 00 32 00 28 07 00 02 01 00 1b

    通過這些數(shù)值,我們可以來看看class文件的格式

    magic:class文件最開始的四個字節(jié)是魔數(shù)。它的值是用來標識Java class文件的。從上面的內容里可以看出,魔數(shù) 的值是0xCAFEBABE。簡而言之,只有一個文件的起始4字節(jié)是0xCAFEBABE的時候,它才會被當作Java class文件來處理。

    minor_version,major_version:接下來的四個字節(jié)表示的是class文件的版本。UserService.class文件里的是0x00000032,所以這個class文件的版本是50.0。JDK 1.6編譯的class文件的版本是50.0,JDK 1.5編譯出來的class文件的版本是49.0。JVM必須對低版本的class文件保持后向兼容性,也就是低版本的class文件可以運行在高版本的JVM上。不過,反過來就不行了,當一個高版本的class文件運行在低版本的JVM上時,會出現(xiàn)java.lang.UnsupportedClassVersionError的錯誤。

    constant_pool_count,constant_pool[]:在版本號之后,存放的是類的常量池。這里保存的信息將會放入運行時常量池(Runtime Constant Pool)中去,這個后面會講解的。在加載一個class文件的時候,JVM會把常量池里的信息存放在方法區(qū)的運行時常量區(qū)里。UserService.class文件里的constant_pool_count的值是0x0028,這表示常量池里有39(40-1)個常量。

    access_flags:這是表示一個類的描述符的標志;換句話說,它表示一個類是public,final還是abstract以及是不是接口的標志。

    fields_count,fields[]:當前類的成員變量的數(shù)量以及成員變量的信息。成員變量的信息包含變量名,類型,修飾符以及變量在constant_pool里的索引。

    methods_count,methods[]:當前類的方法數(shù)量以及方法的信息。方法的信息包含方法名,參數(shù)的數(shù)量和類型,返回值的類型,修飾符,以及方法在constant_pool里的索引,方法的可執(zhí)行代碼以及異常信息。

    attributes_count,attributes[]:attribution_info結構包含不同種類的屬性。field_info和method_info里都包含了attribute_info結構。

    javap簡要地給出了class文件的一個可讀形式。當你用”java -verbose”命令來分析UserService.class時,會輸出如下的內容:

    Compiled from "UserService.java" public class com.nhn.service.UserService extends java.lang.Object SourceFile: "UserService.java" minor version: 0 major version: 50 Constant pool:const #1 = class #2; // com/nhn/service/UserService const #2 = Asciz com/nhn/service/UserService; const #3 = class #4; // java/lang/Object const #4 = Asciz java/lang/Object; const #5 = Asciz admin; const #6 = Asciz Lcom/nhn/user/UserAdmin;;// … omitted - constant pool continued … { // … omitted - method information … public void add(java.lang.String); Code: Stack=2, Locals=2, Args_size=2 0: aload_0 1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin; 4: aload_1 5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User; 8: pop 9: return LineNumberTable: line 14: 0 line 15: 9 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/nhn/service/UserService; 0 10 1 userName Ljava/lang/String; // … Omitted - Other method information … }

    javap輸出的內容太長,我這里只是提出了整個輸出的一部分。整個的輸出展示了constant_pool里的不同信息,以及方法的內容。

    關于方法的65565字節(jié)大小的限制是和method_info struct相關的。method_info結構包含Code,LineNumberTable,以及LocalViriable attribute幾個屬性,這個在“javap -verbose”的輸出里可以看到。Code屬性里的LineNumberTable,LocalVariableTable以及exception_table的長度都是用一個固定的2字節(jié)來表示的。因此,方法的大小是不能超過LineNumberTable,LocalVariableTable以及exception_table的長度的,它們都是65535字節(jié)。

    許多人都在抱怨方法的大小限制,而且在JVM規(guī)范里還說名了”這個長度以后有可能會是可擴展的“。不過,到現(xiàn)在為止,還沒有為這個限制做出任何動作。從JVM規(guī)范里的把class文件里的內容直接拷貝到方法區(qū)這個特點來看,要想在保持后向兼容性的同時來擴展方法區(qū)的大小是非常困難的。

    如果因為Java編譯器的錯誤而導致class文件的錯誤,會怎么樣呢?或者,因為網(wǎng)絡傳輸?shù)腻e誤導致拷貝的class文件的損壞呢?

    為了預防這種場景,Java的類裝載器通過一個嚴格而且慎密的過程來校驗class文件。在JVM規(guī)范里詳細地講解了這方面的內容。

    注意:我們怎樣能夠判斷JVM正確地執(zhí)行了class文件校驗的所有過程呢?我們怎么來判斷不同提供商的不同JVM實現(xiàn)是符合JVM規(guī)范的呢?

    為了能夠驗證以上兩點,Oracle提供了一個測試工具TCK(Technology Compatibility Kit)。這個TCK工具通過執(zhí)行成千上萬的測試用例來驗證一個JVM是否符合規(guī)范,這些測試里面包含了各種非法的class文件。只有通過了TCK的測試的JVM才能稱作JVM。

    和TCK相似,有一個組織JCP(Java Community Process;http://jcp.org)負責Java規(guī)范以及新的Java技術規(guī)范。對于JCP而言,如果要完成一項Java規(guī)范請求(Java Specification Request, JSR)的話,需要具備規(guī)范文檔,可參考的實現(xiàn)以及通過TCK測試。任何人如果想使用一項申請JSR的新技術的話,他要么使用RI提供許可的實現(xiàn),要么自己實現(xiàn)一個并且保證通過TCK的測試。

    總結

    以上是生活随笔為你收集整理的【深入理解JVM】ClassLoader类加载机制的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內容還不錯,歡迎將生活随笔推薦給好友。