Java虛擬機-虛擬機類加載機制

虛擬機的類加載機制: 虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型;
 
在Java語言裏面,類型的加載、鏈接和初始化過程都是在程序運行期間完成的;
 
類加載的時機
類從被加載到虛擬機內存開始,到卸載出內存爲止,它的生命週期是:加載、驗證、準備、解析、初始化、使用、卸載;其中驗證、準備和解析三個部分統稱爲鏈接;
加載、驗證、準備、初始化和卸載這5個階段的順序是肯定的,類的加載過程必須按照這個順序循序漸進地「 開始 」,而解析階段不必定,解析有時候會在初始化階段以後再開始;
對於初始化階段,虛擬機規範則是嚴格規定了有且只有5種狀況必須當即對類進行初始化:
一、遇到new(實例化對象)、getstatic(讀取一個類的靜態字段【被final修飾,已在編譯期把結果放入常量池的靜態字段除外】)、pubstatic(設置一個類的靜態字段 【被final修飾,已在編譯期把結果放入常量池的靜態字段除外】 )、invokestatic(調用一個類的靜態方法),這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。
二、使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。
三、當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
四、當虛擬機啓動時,用戶須要指定一個要執行的主類,虛擬機會先初始化這個主類;
五、當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethosHandle實例最後的解析結果REF_getStatic、REF_pubStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。
以上五種場景中的行爲稱爲對一個類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。
 
類加載的過程
加載
在加載階段,虛擬機須要完成的3件事情:
一、經過一個類的全限定名來獲取定義此類的二進制字節流;(讀取來源:一、從ZIP包中讀取;二、從網絡中讀取;三、運行時計算生成;四、由其餘文件生成;五、從數據庫中讀取等)
二、將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
三、在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口;
對於非數組類的加載階段,加載階段既可使用系統提供的引導類加載器完成,也能夠由用戶自定義的類加載器去完成,可控。
對於數組類的,數組類自己不經過類加載器,它是由虛擬機直接建立的。可是與類加載器仍有聯繫,由於數組類的元素類型最終是要靠類加載器去建立
 
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區中,方法區中的數據存儲格式由虛擬機實現自行定義,虛擬機規範並未規定此區域的具體數據結構。
而後在內存中實例化一個java.lang.Class類的對象,這個對象將做爲程序訪問方法區中的這些類型數據的外部接口。
 
驗證
驗證是鏈接階段的第一步,主要目的是爲了確保Class文件的字節流中所包含的信息是否符合虛擬機的要求,而且不會危害虛擬機自身的安全;
驗證階段大體會完成如下4個階段的檢驗動做:
一、文件格式驗證:
主要目的:保證輸入的字節流能正確地解析並存儲與方法區以內,格式上符合描述一個Java類型信息的要求;
方式:這個階段是基於 二進制字節流 進行的,只有經過這個階段的驗證後,字節流纔會進入內存的方法去中進行存儲;
檢查點:魔數、主次版本號、常量池的常量中是否有不被支持的常量類型... ...
二、元數據驗證:
對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求;
目的:對類的元數據信息進行 語義校驗 ,保證不存在不符合Java語言規範的元數據信息;
檢查點:這個類是不是父類、這個類的父類是否集成了不容許被繼承的類... ...
三、字節碼驗證:
目的:經過 數據流和控制流 分析,肯定程序語義是合法的、符合邏輯的。
這個階段對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件;
檢查點:保證方法體中的類型轉換是有效的... ...
四、符號引用驗證:
這個階段的校驗是發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動做將在 解析階段發生
符號引用驗證能夠看做對類自身之外的信息進行匹配性檢查。
目的:確保解析動做能正常執行,若是沒法經過符號引用驗證,那麼將拋出異常;
檢查點:符號引用中的類、字段、方法的訪問性是否能夠被當前類訪問... ...
 
準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中分配。
 
解析
解析階段是虛擬機將常量池內的符號引用替換成直接引用的過程;
符號引用:以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。與虛擬機實現的內存佈局無關。引用的目標不必定已經加載到內存中。
直接引用:能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。與虛擬機實現的內存佈局有關。有了直接引用,那引用的目標一定已經在內存中存在。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
類或接口解析
字段解析:要解析一個未被解析過的字段符號引用,首先將會對字段內的class_index項中索引的CONSTATNT_Class_info符號引用進行解析,也就是字段所屬類或接口的符號引用;
類方法解析:須要先解析類方法表內的class_index項中索引的方法所屬的類或接口的符號引用。
接口方法解析:須要先解析出方法表class_index項中索引的方法所屬的類或接口的符號引用。
 
初始化
到了初始化階段,才真正開始執行類中定義的java程序代碼。初始化階段是執行類構造器<clinit>()方法的過程。
對於<clinit>()方法具體介紹以下:
一、 <clinit>()方法是由編譯器自動收集類中的 全部類變量的賦值動做和靜態語句塊中語句 合併產生的。
編譯器收集的順序由語句在源文件中出現的順序所決定的。
靜態語句塊中只能訪問定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊中只能賦值,不能訪問。
二、 <clinit>()方法與類的構造函數不一樣,它不須要顯式的調用父類構造器,虛擬機會保證在子類的 <clinit>()方法執行以前,父類的 <clinit>()方法已經執行完畢,所以虛擬機中第一個執行的 <clinit>()的類必定是java.lang.Object。
三、因爲父類 <clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先與子類的變量複製操做。
四、 <clinit>()方法對於類或接口來講並非必需的,若是一個類中沒有靜態語句塊也沒有對變量的賦值操做,那麼編譯器能夠部位不爲這個類生成 <clinit>()方法。
五、接口中不能有靜態語句塊,可是仍然會有變量初始化的賦值操做。執行接口的 <clinit>()方法不須要先執行父接口的 <clinit>()方法,只有父接口中定義的變量使用時,父類接口才會被初始化。接口實現類在初始化時也同樣不會執行接口的 <clinit>()方法。
六、虛擬機會保證一個類的 <clinit>()方法在多線程環境中被正確的加鎖、同步。
 
類加載器
實現經過一個類的全限定名來獲取描述此類的二進制字節流的代碼模塊是」類加載器「
類與類加載器
對於任意一個類,都須要由加載它的類加載器和這個類自己一同肯定其在Java虛擬機中的惟一性。
比較兩個類是否」相等「,只有在這兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這兩個類來源與同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不一樣,那麼這兩個類就一定不相等;
 
從Java虛擬機的角度來看,只存在2種不一樣的類加載器:
一、啓動類加載器,用C++語言實現,虛擬機自身的一種實現;
二、全部其餘的類加載,用Java語言實現,獨立於虛擬機外部,所有都繼承自抽象類java.lang.ClassLoader;
 
從Java開發人員的角度來看,類加載器還能夠劃分爲一下3中系統提供的類加載器:
一、啓動類加載器:啓動類加載器沒法被java程序直接引用。扶着將放在<JAVA_HOME>\lib目錄中的或被-Xbootclasspath參數指定的路徑中的而且是虛擬機識別的類庫加載到虛擬機內存中;
二、擴展類加載器:負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫;由sun.misc.Launcher$ExtClassLoader實現;
三、應用程序類加載器:負責加載用戶路徑所指定的類庫;這個類加載是ClassLoader中的getSystemClassLoader()方法的返回值,因此通常也稱它爲系統類加載器;由sun.misc.Launcher$AppClassLoader實現;
 
雙親委派模型:

除了頂層的啓動類加載器,其他的類加載器都應當有本身的父類加載器。
父子關係不是以繼承關係來實現,而都是使用組合關係來複用父加載器的代碼;
 
雙親委派模型的工做過程 :若是一個類加載器收到類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,由於全部的加載請求最終都應該傳遞給頂層的啓動類加載器中,只有當父類加載器反饋本身沒法完成這個加載請求時,子加載器纔會嘗試去加載。
 
雙親委派模型的實現 :先檢查是否已經被加載過了,若是沒有加載則調用父加載器的loadClass()方法,若是父加載器爲空則默認使用啓動類加載器做爲父加載器。若是父類加載失敗,拋出ClassNotFoundException異常後,再調用本身的findClass()方法進行加載;
 
破壞雙親委派模型:
一、在JDK1.2以前,雙親委派模型出現以前,面對已經存在的用戶自定義類加載的實現代碼,在java.lang.ClassLoader添加了一個新的protected的loadClass()方法。在此以前用戶繼承java.lang.ClassLoader惟一目的就是爲了重寫loadClass()方法。
二、由於自身的缺陷,雙親委派很好的解決了各個類加載器的基礎類的統一問題,可是對於基礎類又要掉回用戶的代碼,Java設計團隊增長了一個「線程上下文類加載器」;這個類加載器能夠經過java.lang.Thread類的setContextClassLoader()方法進行設置,若是建立線程時還未設置,它將會從父線程中繼承一個,若是在應用程序的全局範圍內都沒有設置,那麼這個類加載器就默認爲應用程序類加載器;如:JNDI、JDBC、JCE等
三、因爲用戶對程序動態性的追求,代碼熱替換、模塊熱部署。OSGI實現模塊化熱部署的關鍵則是它自定義的類加載器機制的實現。每一個程序模塊都有一個本身的類加載器,當須要更換一個模塊的時候,就把模塊連同類加載器一塊兒換掉以實現代碼的熱替代。
相關文章
相關標籤/搜索