java类验证和装载顺序_Java类的加载机制和双亲委派模型
Java類的加載機制和雙親委派模型
1類的加載機制
類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括了:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸載(Unloading)七個階段。其中驗證、準備和解析三個部分統稱為連接(Linking),這七個階段的發生順序如下圖所示:
如上圖所示,加載、驗證、準備、初始化和卸載這五個階段的順序是確定的,類的加載過程必須按照這個順序來按部就班地開始,而解析階段則不一定,它在某些情況下可以在初始化階段后再開始。
類的生命周期的每一個階段通常都是互相交叉混合式進行的,通常會在一個階段執行的過程中調用或激活另外一個階段。
1.1裝載階段
在裝載階段,虛擬機主要完成三件事:
1、通過一個類的全限定名來獲取定義此類的二進制字節流。
2、將這個字節流所代表的靜態存儲結構轉化為方法區域的運行時數據結構。
3、在Java堆中生成一個代表這個類的java.lang.Class(HelloWorld.class)對象,作為方法區域數據的訪問入口
1.2驗證階段
驗證階段作用是保證Class文件的字節流包含的信息符合JVM規范,不會給JVM造成危害。如果驗證失敗,就會拋出一個java.lang.VerifyError異常或其子類異常。驗證過程分為四個階段
1、
文件格式驗證
驗證字節流文件是否符合Class文件格式的規范,并且能被當前虛擬機正確的處理。
2、元數據驗證
是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言的規范。
3、字節碼驗證
主要是進行數據流和控制流的分析,保證被校驗類的方法在運行時不會危害虛擬機。
4、符號引用驗證
符號引用驗證發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在解析階段中發生。
1.3準備
準備階段是正式為類變量分配內存并設置類變量(static修飾的變量)初始值的階段,這些內存都將在方法區中進行分配。
注釋:
1、這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。
2、這里所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
3、對已非final的變量,JVM會將其設置成“零值”,而不是其賦值語句的值:
pirvate static int size = 12;
那么在這個階段,size的值為0,而不是12。 final修飾的類變量將會賦值成真實的值。
1.4解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。
符號引用:符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,符號引用與虛擬機實現的內存布局無關,引用的目標并不一定已經在內存中。
直接引用:直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存布局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般都不相同,如果有了直接引用,那引用的目標必定已經在內存中存在。
1、類或接口的解析:判斷所要轉化成的直接引用是對數組類型,還是普通的對象類型的引用,從而進行不同的解析。
2、字段解析:對字段進行解析時,會先在本類中查找是否包含有簡單名稱和字段描述符都與目標相匹配的字段,如果有,則查找結束;如果沒有,則會按照繼承關系從上往下遞歸搜索該類所實現的各個接口和它們的父接口,還沒有,則按照繼承關系從上往下遞歸搜索其父類,直至查找結束。
3、類方法解析:對類方法的解析與對字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對類方法的匹配搜索,是先搜索父類,再搜索接口。
4、接口方法解析:與類方法解析步驟類似,只是接口不會有父類,因此,只遞歸向上搜索父接口就行了。
1.5初始化
類初始化階段是類加載過程的最后一步,前面的類加載過程中,除了加載(Loading)階段用戶應用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。
初始化階段是執行類構造器()方法的過程。
1、()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合并產生的,編譯器收集的順序由語句在源文件中出現的順序所決定。
2、()方法與類的構造函數不同,它不需要顯式地調用父類構造器,虛擬機會保證在子類的()方法執行之前,父類的()方法已經執行完畢,因此在虛擬機中第一個執行的()方法的類一定是java.lang.Object。
3、由于父類的()方法先執行,也就意味著父類中定義的靜態語句塊要優先于子類的變量賦值操作。
4、()方法對于類或者接口來說并不是必需的,如果一個類中沒有靜態語句塊也沒有對變量的賦值操作,那么編譯器可以不為這個類生成()方法。
5、接口中可能會有變量賦值操作,因此接口也會生成()方法。但是接口與類不同,執行接口的()方法不需要先執行父接口的()方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也不會執行接口的()方法。
6、虛擬機會保證一個類的()方法在多線程環境中被正確地加鎖和同步。如果有多個線程去同時初始化一個類,那么只會有一個線程去執行這個類的()方法,其它線程都需要阻塞等待,直到活動線程執行()方法完畢。如果在一個類的()方法中有耗時很長的操作,那么就可能造成多個進程阻塞。
1.6使用
Class初始化過程完后就可以被任意調用。
1.7卸載
JVM中的Class只有滿足以下三個條件,才能被GC回收,也就是該Class被卸載:
1、該類所有的實例都已經被GC。
2、加載該類的ClassLoader實例已經被GC。
3、該類的java.lang.Class對象沒有在任何地方被引用。
1.8研究類加載的意義
類加載是Java程序運行的第一步,研究類的加載有助于了解JVM執行過程,并指導開發者采取更有效的措施配合程序執行。
研究類加載機制的第二個目的是讓程序能動態的控制類加載,比如熱部署等,提高程序的靈活性和適應性。
1.9加載類的開放性
類加載器(ClassLoader)是Java語言的一項創新,也是Java流行的一個重要原因。在類加載的第一階段“加載”過程中,需要通過一個類的全限定名來獲取定義此類的二進制字節流,完成這個動作的代碼塊就是類加載器。這一動作是放在Java虛擬機外部去實現的,以便讓應用程序自己決定如何獲取所需的類。
虛擬機規范并沒有指明二進制字節流要從一個Class文件獲取,或者說根本沒有指明從哪里獲取、怎樣獲取。這種開放使得Java在很多領域得到充分運用,例如:
1、從ZIP包中讀取,這很常見,成為JAR,EAR,WAR格式的基礎。
2、從網絡中獲取,最典型的應用就是Applet。
3、運行時計算生成,最典型的是動態代理技術,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來為特定接口生成形式為“*$Proxy”的代理類的二進制字節流。
4、有其他文件生成,最典型的JSP應用,由JSP文件生成對應的Class類。
1.10類加載器與類的唯一性
類加載器雖然只用于實現類的加載動作,但是對于任意一個類,都需要由加載它的類加載器和這個類本身共同確立其在Java虛擬機中的唯一性。通俗的說,JVM中兩個類是否“相等”,首先就必須是同一個類加載器加載的,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要類加載器不同,那么這兩個類必定是不相等的。
這里的“相等”,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做對象所屬關系判定等情況。
2類加載的時機
2.1主動引用
一個類被主動引用之后會觸發初始化過程(加載,驗證,準備需再此之前開始)。
1、遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令最常見的Java代碼場景是:使用new關鍵字實例化對象時、讀取或者設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)時、以及調用一個類的靜態方法的時候。
2、使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
3、當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要觸發父類的初始化。
4、當虛擬機啟動時,用戶需要指定一個執行的主類(包含main()方法的類),虛擬機會先初始化這個類。
5、當使用jdk7+的動態語言支持時,如果java.lang.invoke.MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發器 初始化。(更多java動態類型語言資料:http://www.infoq.com/cn/articles/jdk-dynamically-typed-language)
2.2被動引用
一個類如果是被動引用的話,該類不會觸發初始化過程。
1、通過子類引用父類的靜態字段,不會導致子類初始化。對于靜態字段,只有直接定義該字段的類才會被初始化,因此當我們通過子類來引用父類中定義的靜態字段時,只會觸發父類的初始化,而不會觸發子類的初始化。
2、通過數組定義來引用類,不會觸發此類的初始化。
3、常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。
3雙親委派模型
3.1從Java虛擬機的角度來說,只存在兩種不同的類加載器
1、啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(HotSpot虛擬機中),是虛擬機自身的一部分。
2、所有其他的類加載器,這些類加載器都有Java語言實現,獨立于虛擬機外部,并且全部繼承自java.lang.ClassLoader。
3.2從開發者的角度,類加載器可以細分為
1、啟動(Bootstrap)類加載器:負責將 Java_Home/lib下面的類庫加載到內存中(比如rt.jar)。由于引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。
2、標準擴展(Extension)類加載器:是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將Java_Home /lib/ext或者由系統變量 java.ext.dir指定位置中的類庫加載到內存中。開發者可以直接使用標準擴展類加載器。
3、應用程序(Application)類加載器:是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑(CLASSPATH)中指定的類庫加載到內存中。開發者可以直接使用系統類加載器。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,因此一般稱為系統(System)加載器。
4、自定義(Custom
ClassLoader)類加載器:應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規范自行實現ClassLoader加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。
3.3雙親委派模型
啟動、標準擴展、應用程序和自定義類加載器,它們之間的層次關系被稱為類加載器的雙親委派模型。該模型要求除了頂層的啟動類加載器外,其余的類加載器都應該有自己的父類加載器,而這種父子關系一般通過組合(Composition)關系來實現,而不是通過繼承(Inheritance)。
3.4雙親委派模型的過程
某個特定的類加載器在接到加載類的請求時,首先將加載任務委托給父類加載器,依次遞歸,如果父類加載器可以完成類加載任務,就成功返回;只有父類加載器無法完成此加載任務時,才自己去加載。
使用雙親委派模型的好處在于Java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存在在rt.jar中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的Bootstrap ClassLoader進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有雙親委派模型而是由各個類加載器自行加載的話,如果用戶編寫了一個java.lang.Object的同名類并放在ClassPath中,那系統中將會出現多個不同的Object類,程序將混亂。因此,如果開發者嘗試編寫一個與rt.jar類庫中重名的Java類,可以正常編譯,但是永遠無法被加載運行。
參考鏈接:
總結
以上是生活随笔為你收集整理的java类验证和装载顺序_Java类的加载机制和双亲委派模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux php生产环境搭建,linu
- 下一篇: Java-逻辑运算符、位运算符