摘要:在加載階段,虛擬機要完成件事情通過一個類的全限定名來獲取定義此類的二進制字節流。前面的階段中,除了加載的時候,可以由用戶指定自定義類加載器之外,別的都是由虛擬機主導控制。
java類加載機制
代碼編譯的結果從本地機器碼轉變為字節碼,是存儲格式發展的一小步,確實編程語言發展的一大步
虛擬機把描述類的數據從class文件加載到內存,并對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的java類型,這就是虛擬機的類加載機制。
1 類的生命周期一個類從被加載到內存到卸載出內存,整個生命周期包括:
加載loading
驗證verification
準備preparation
解析resolution
初始化initialization
使用using
卸載unloading
其中驗證、準備和解析,這三步合起來又被稱為連接(liking)。
加載、驗證、準備、初始化和卸載,這五個階段的順序是確定的,而解析不一定。某些情況下,解析可能在初始化之后再開始,這就是java動態綁定。
java虛擬機規范中嚴格規定了有且只有5種情況必須對類立即進行初始化:
遇到new、getstatic、putstatic或invokestatic這四個指令時,必須進行初始化。
生成這幾個指令的場景有:
使用new實例化一個對象時;
讀取或者設置一個類的靜態字段時;
調用一個類的靜態方法時。
使用reflect包的方法對類進行反射時,也觸發初始化。
初始化一個類的時候,若父類還未初始化,則首先進行父類的初始化。
包含main方法的那個類,虛擬機啟動時會首先初始化這個主類。
當使用jdk1.7的動態語言支持時,
接口的加載和類加載的過程稍有些不同:
接口和類一樣都有初始化過程,雖然接口里面不能有static{}語句塊,但是編譯器仍然會為接口生成
java接口中的變量必須得是final靜態的,但接口里最好不要有變量。
當一個類初始化時,必須要求父類全部都已經初始化,但是接口在初始化時并不要求其父接口也全部初始化,只有在使用到父接口時才會初始化。
2 類加載的過程 2.1 加載加載是類加載的一個階段。在加載階段,虛擬機要完成3件事情:
通過一個類的全限定名來獲取定義此類的二進制字節流。
將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。
加載階段完成之后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中。
2.2 驗證驗證階段是連接階段的第一步,目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
文件格式驗證
驗證字節流是否符合Class文件格式的規范,并且能被當前版本的虛擬機處理。
元數據驗證
對字節碼描述的信息進行語義分析,以保證其描述信息符合java語言規范。
字節碼驗證
最復雜的一個階段,通過對數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
符號引用驗證
對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。目的是為了確保解析動作能正常執行。
驗證階段是非常重要的,但不是一定必要的階段。如果所運行的代碼都已經被反復使用和驗證過,就可以通過jvm參數來關閉大部分類驗證措施。
2.3 準備準備階段是給類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。
此時進行內存分配的變量僅包括類變量,而不包含實例變量,實例變量將在對象實例化時隨著對象一起分配在java堆中。
這里所說的初始值是指數據類型的零值,比如:
public static int v = 123;
那v的值在準備階段是0,而不是123。
數據類型 | 零值 | 數據類型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | "u0000" | reference | null |
byte | (byte)0 |
如果一個變量是常量,或者final類型的,那么在準備階段就被初始化為常量值,如:
public static final int v = 123;
此時v的值在準備階段是123。
2.4 解析解析階段是虛擬機將常量池的符號引用替換成直接引用的過程。
符號引用:是以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用的時候能無歧義的定位到目標即可。符號引用與虛擬機實現的內存布局無關,引用的目標并不一定加載到內存中。各種虛擬機的內存布局可以各不相同,但是能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在java虛擬機規范的Class文件格式中。
直接引用:直接引用可以是直接指向目標的指針、相對偏移量或者是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存相關的,如果有了直接引用,那么引用的目標必定已經在內存中了。
解析主要是針對類或者接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行的。
2.5 初始化類初始化時類加載過程的最后一步。前面的階段中,除了加載的時候,可以由用戶指定自定義類加載器之外,別的都是由虛擬機主導控制。初始化階段才真正執行類中定義的java代碼。
在準備階段變量已經被賦過零值,而初始化階段是根據程序里面的來初始化類變量和其他資源,可以理解為執行類構造器的
public class Test{ static{ i=0; //這句話是給變量賦值,可以編譯通過 System.out.println(i); //這句話是要訪問i,編譯器會提示“非法向前引用”編譯不過。 } static int i = 1; }
由上一條可以得出結論,父類中定義的靜態語句塊要早于子類的變量賦值操作。
前面加載的時候有說到,接口中不能有靜態語句塊,但是可以有變量的初始化賦值操作。接口和類都會生成
虛擬機會保證一個類的
類加載階段的加載階段,即“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到jvm外部實現,使得應用程序自己可以決定如何獲取所需要的類。實現這個動作的代碼模塊稱為“類加載器”。
對于任意一個類來說,需要加載它的類加載器和其類本身來保證唯一性。如果同一個Class文件,被不同的類加載器加載了,那么產生的兩個類是不相同的。
3.1 類加載器的分類對于java虛擬機來說,只有兩種不同的類加載器:
啟動類加載器 Bootstrap ClassLoader:C++實現的,虛擬機的一部分。
其他類加載器:java語言實現,獨立于jvm外部。全部繼承抽象類java.lang.ClassLoader。
從java程序員的角度來看,有三種系統提供的類加載器:
啟動類加載器 Bootstrap ClassLoader
負責將放在JAVA_HOEM/lib目錄里的,或者是被-Xbootclasspath參數指定的路徑中的,并且可以被虛擬機識別的類庫加載到虛擬機內存中。
啟動類加載器無法被java程序直接引用。如果是用戶在編寫自定義類加載器的時候,需要把加載請求委派給啟動類加載器,返回null就行了。
擴展類加載器 Extension ClassLoader
負責加載JAVA_HOEM/lib/ext目錄中的,或者被java.ext.dirs系統變量指定的所有類庫,開發者可以直接使用擴展類加載器。
應用程序類加載器 Application ClassLoader
這個類加載器是ClassLoader中的getSystemClassLoader()方法中的返回值,所以也稱為系統類加載器,負責加載用戶類路徑上指定的類庫。
開發者可以直接使用此類加載器,如果應用程序沒有自定義自己的類加載器,一般情況下這個就是程序的默認類加載器。
開發者可以自己編寫一些自定義類加載器,用來進行特定類的加載。他們的關系是:
雙親委派模型
3.1 雙親委派模型雙親委派模型要求除了最頂層的啟動類加載器外,其余的加載器都得有自己的父類加載器。這里的類加載器的父子關系不是通過繼承來實現,而是使用組合關系來復用復加載器的代碼。
雙親委派模型的工作過程是:
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是這樣。因此,所有的類加載請求最終都會傳送到最頂層的啟動類加載器,只有當父加載器反饋自己無法加載這個加載請求的時候,子加載器才會嘗試自己去加載。
使用這個模型的好處就是java類隨著它的加載器一起具備了一種帶有優先級的層次關系。比如java.lang.Object,無論哪個類加載器要加載這個類的時候,最終都是委派給最頂端的啟動類加載器進行加載,因此Object類在程序的各個類加載器環境中都是同一個類。如果不使用這個模型的話,由各個類加載器自己加載,就會出現多個Object類。
雙親委派模型的邏輯實現代碼很簡單:
protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException{ //首先檢查請求的類是否已經被加載過 Class c = findLoadedClass(name); if(c==null){ if(parent!=null){ c=parent.loadClass(name, false); }else{ c=findBootstrapClassOrNull(name); } //如果父類加載器無法加載的時候,就調用本身的方法去加載 if(c==null){ c=findClass(name); } } if(resolve){ resolveClass(c); } return c; }3.2 破壞雙親委派模型
雙親委派模型并不是一個強制性的約束模型,在java世界中,大部分加載器都遵循這個模型,在java歷史上有三種比較大的被破壞情況。
第一次是jdk1.2發布的時候。由于雙親委派模型是在1.2才引入的,java.lang.ClassLoader是在1.0的時候就存在了,面對在此之前的用戶自定義類加載器的代碼,java設計者添加了一個findClass方法來作為妥協。
第二次是JNDI服務。雙親委派模型很好地解決了各個類加載器的基礎類統一問題,但是當基礎類又回來調用用戶的代碼就沒辦法了。所以引入了線程上下文 類加載器(Thread Context ClassLaoder)。
第三次就是熱更新熱部署的時候。代表就是OSGi,每一個程序模塊都有一個自己的類加載器,當需要更換一個模塊的時候,就把模塊連同其加載器一起換掉。此時的類加載器的結構成了網狀結構了。
4 寫在最后把書從后面往前面看還是挺有意思的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70758.html
摘要:如果需要支持類的動態加載或需要對編譯后的字節碼文件進行解密操作等,就需要與類加載器打交道了。雙親委派模型,雙親委派模型,約定類加載器的加載機制。任何之類的字節碼都無法調用方法,因為該方法只能在類加載的過程中由調用。 jvm系列 垃圾回收基礎 JVM的編譯策略 GC的三大基礎算法 GC的三大高級算法 GC策略的評價指標 JVM信息查看 GC通用日志解讀 jvm的card table數據...
摘要:前面提到,對于數組類來說,它并沒有對應的字節流,而是由虛擬機直接生成的。對于其他的類來說,虛擬機則需要借助類加載器來完成查找字節流的過程。驗證階段的目的,在于確保被加載類能夠滿足虛擬機的約束條件。 Java 虛擬機將字節流轉化為 Java 類的過程。這個過程可分為加載、鏈接以及初始化 三大步驟。 加載是指查找字節流,并且據此創建類的過程。加載需要借助類加載器,在 Java 虛擬機中,類...
摘要:當程序使用某個類時,如果該類還沒被初始化,加載到內存中,則系統會通過加載連接初始化三個過程來對該類進行初始化。一旦一個類被加載到中之后,就不會再次載入了。它既可以從本地文件系統獲取二進制文件來加載類,也可以遠程主機獲取二進制文件來加載類。 當程序使用某個類時,如果該類還沒被初始化,加載到內存中,則系統會通過加載、連接、初始化三個過程來對該類進行初始化。該過程就被稱為類的初始化 類加載 ...
摘要:學習能更深入的理解這門語言,能理解語言底層的執行過程,深入到字節碼層次。 目錄 ? 前言 程序的運行 1.JVM類加載機制 ①一般在什么情況下會去加載一個類?也就是說,什么時候.class字節碼文件中加載這個類到JVM內存里來? ②驗證、準備、初始化 ③初始化 2.類加載器和雙親委派機制 ...
摘要:以上文中的類的加載過程為例,它的加載器為系統類加載器。自定義加載器編寫自定義加載器并不困難,只要繼承抽象類并覆蓋方法就行了。源碼來自參考資料類加載機制與類加載器架構深入探討類加載器 序 我是在關于Java的面試題里了解到類加載器的,在這之前從未想過Java里類是如何被加載、解析的,一直以為只要Import就好了。事實上Java類加載器是一塊非常重要的內容,可以用在類層次劃分、OSGi、...
閱讀 2785·2021-10-14 09:42
閱讀 3608·2021-10-11 10:59
閱讀 2941·2019-08-30 11:25
閱讀 3074·2019-08-29 16:25
閱讀 3224·2019-08-26 17:40
閱讀 1225·2019-08-26 13:30
閱讀 1143·2019-08-26 11:46
閱讀 1329·2019-08-23 15:22