類加載過程和類加載器
在Java中,類加載都是在運行期間執行的,這種策略雖然令類加載稍微增加一些性能,但是會給java應用程序提供高度的靈活性。
類加載的過程
和其他語言一樣,java編譯器同樣能夠將.java文件編譯成.class,但是對於JVM來講,它並不關心,是哪種語言經過編譯形成的。
JVM類加載工作原理:就是把類的class文件加載到內存中,並對數據進行校驗、轉換解析和初始化,最終形成被虛擬機使用的java類型。
類加載的生命周期包括以下幾個部分:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading),其中驗證、準備、解析三個部分統稱鏈接。
加載(裝載)、驗證、準備、初始化和卸載這五個階段順序是固定的,類加載過程必須按照這種順序開始,而解析階段不一定,它在某些情況下可以在初始化之後再開始,這是為了運行時動態綁定特性(JIT例如接口只在調用的時候才知道具體的實現的是哪個子類)。值得註意的是:這些階段通常都是交叉的混合式進行的,通常會在一個階段執行的過程中調用或激活另一個階段。
加載
加載這個階段通常也被稱為“裝載”,它的主要任務主要有一下幾點:
1、通過“類全名”來獲取定義此類的二進制字節流
2、將字節流所代表的靜態存儲結構轉換為方法區的運行時數據結構
3、在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口
相對於類加載過程的其他階段,加載階段是通過類加載(ClassLoader)來完成的,而類加載器也可以由用戶自定義完成,因此,開發人員可以通過定義類加載器去控制字節流的獲取方式。加載之後,二進制文件會被讀入到虛擬機所需的格式存儲在方法區中,方法區中存儲格式由虛擬機自行定義,然後在java堆中實例化一個java.lang.Class類對象,通過這個對象就可以訪問方法區中的數據。
驗證
驗證階段是鏈接階段的第一步,目的就是確保class文件的字節流中包含的信息符合虛擬機的要求,不能危害虛擬機自身安全。驗證階段主要包括四個檢驗過程:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
1、文件格式驗證
驗證class文件格式規範
2、元數據驗證
就是對字節碼描述的信息進行語義分析,保證描述的信息符合java語言規範。驗證點可能包括(這個類是否有父類(除Object)、這個類是否繼承了不允許被繼承的類(final修飾的)、如果這個類的父類是抽象類,是否實現了父類或接口中要求實現的方法)。
3、字節碼驗證
進行數據流和控制流分析,這個階段對類的方法體進行校驗,保證被校驗的方法在運行時不會做出危害虛擬機的行為。
4、符號引用驗證
符號引用中通過字符串描述的權限定名是否能找到對應的類、符號引用類中的類,字段和方法的訪問性(private protected public default)是否能被當前類訪問。
準備
這個階段就是為類變量分配內存並設置類變量初始值的階段,這些內存將在方法區中進行分配。要註意的是,進行分配內存的只是包括類變量,而不包括實例變量,實例變量是在對象實例化時隨著對象一起分配在java堆中的。通常情況下,初始值為零值,假設public static int value=2;那麽value在準備階段過後的初始值為0,不為2,這時候只是開辟了內存空間,並沒有運行java代碼,value賦值為2的指令是程序被編譯後,存放於類構造器<clinit>()方法之中,所以value被賦值為2是在初始化階段才會執行。對於一些特殊情況,如果類字段屬性表中存在ConstantValue屬性,那在準備階段變量value就會被初始化為ContstantValue屬性所指的值,那麽對於上面value,編譯時javac將會為value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue將value設置為2
解析
解析階段是虛擬機常量池的符號引用替換為直接引用的過程。
符號引用:符號引用是一組符號來描述所引用的對象,符號可以是任何形式的字面量,只要使用時能定位到目標即可,符號引用與虛擬機實現的內存布局無關,引用的目標對象並不一定已經加載到內存中。
直接引用:直接引用可以是直接指向目標對象的指針、相對偏移量或是一個能間接定位到目標的句柄(一種特殊的智能指針)。直接引用是與虛擬機內存布局實現相關,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同,如果有了直接引用,那引用的目標必定存在內存中。
解析過程主要針對類或接口、字段、類方法、接口方法四類符號引用進行。
初始化
類的初始化階段是加載過程的最後一步,在準備階段,類變量已賦過一次系統要求的初始值,而在初始化階段,則是根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度表達:初始化階段是執行類構造器<clinit>()方法的過程。在以下四種情況下初始化過程會被觸發執行:
1.遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需先觸發其初始化。生成這4條指令的最常見的java代碼場景是:使用new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用類的靜態方法的時候。
2.使用java.lang.reflect包的方法對類進行反射調用的時候。
3.當初始化一個類的時候,如果發現其父類還沒有進行過初始化、則需要先出發其父類的初始化。
4.jvm啟動時,用戶指定一個執行的主類(包含main方法的那個類),虛擬機會先初始化這個類。
*類構造器<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static塊)中的語句合並產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句快可以賦值,但是不能訪問。
*類構造器<clinit>()方法與類的構造函數(實例構造函數<init>()方法)不同,它不需要顯式調用父類構造,虛擬機會保證在子類<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。因此在虛擬機中的第一個執行的<clinit>()方法的類肯定是java.lang.Object。
*由於父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句快要優先於子類的變量賦值操作。
*<clinit>()方法對於類或接口來說並不是必須的,如果一個類中沒有靜態語句,也沒有變量賦值的操作,那麽編譯器可以不為這個類生成<clinit>()方法。
*接口中不能使用靜態語句塊,但接口與類不太能夠的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也一樣不會執行接口的<clinit>()方法。
*虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確加鎖和同步,如果多個線程同時去初始化一個類,那麽只會有一個線程執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果一個類的<clinit>()方法中有耗時很長的操作,那就可能造成多個進程阻塞。
類加載器
JVM設計者把類加載階段中的通過類全名來獲取此類的二進制字節流這個動作放到java虛擬機外部去實現,以便讓應用程序決定如何獲取所需要的類。實現這個動作的代碼模塊成為“類加載器”。
類與類加載器
對於任何一個類,都需要由加載它的類加載器和這個類來確定其在JVM中的唯一性。也就是說,兩個類來源於同一個Class文件,並且被同一個類加載器加載,這兩個類才相等。
雙親委派模型
從虛擬機的角度來說,有兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),該加載器使用C++語言實現,屬於虛擬機自身的一部分。另一部分就是所有其它的類加載器,這些類加載器是由Java語言實現,獨立於JVM外部,並且全部繼承抽象類java.lang.ClassLoader.
從java開發人員的角度看,大部分java程序會用到以下三種系統提供的類加載器:
1、啟動類加載器(Bootstrap ClassLoader):負責加載JAVA_HOME\lib目錄中並且能被虛擬機識別的類庫加載到JVM內存中,如果名稱不符合的類庫即使在lib目錄中也不會被加載。該類加載器無法被java程序直接引用。
2、擴展類加載器(Extension ClassLoader):該加載器主要負責加載JAVA_HOME\lib\ext目錄中的類庫,開發者可以使用擴展加載器。
3、應用程序類加載器(Application ClassLoader):該列加載器也稱為系統加載器,它負責加載用戶類路徑(Classpath)上所指定的類庫,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
當然除了以上三種類加載器,我們還能自己定義類加載器。這些類加載器之間的關系如下。
上面的這種模型,就稱為類加載器的雙親委派模型。該模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。子類加載器不是以繼承的關系來實現,而是通過組合關系來復用父加載器的代碼。
雙親委派模型的工作過程為:如果一個類加載器收到了類請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父加載器去完成,每一層都是如此,因此所有類加載的請求都會傳到啟動類加載器,只有當父加載器無法完成該請求時,子加載器才去自己加載。
雙親委派模型的好處就是java類隨著它的類加載器一起具備了一種帶有優先級的層次關系。例如:Object,無論那個類加載器去加載該類,最終都是由啟動類加載器進行加載的,因此Object類在程序的各種類加載環境中都是一個類。如果不用改模型,那麽java.lang.Object類存放在classpath中,那麽系統中就會出現多個Object類,程序變得很混亂。
類加載java.lang.ClassLoader類中,有一個loadClass方法源碼如下:
protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先檢查該name指定的class是否有被加載 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { // 如果parent不為null,則調用parent的loadClass進行加載 c = parent.loadClass(name, false); } else { // parent為null,則調用BootstrapClassLoader進行加載 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果仍然無法加載成功,則調用自身的findClass進行加載 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
從源代碼中,我們不難看出,先檢查是否已經加載,如果沒有,就調用父加載器的loadClass()方法,如果父加載器為空則默認使用啟動類加載器作為父加載器。如果父加載器加載失敗,拋出ClassNotFountException,然後再調用findClass()方法加載。
自定義類加載器
若要實現自定義類加載器,只需要繼承java.lang.ClassLoader類,並且重寫findClass()方法即可。java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成對應的字節碼,然後從這些字節碼中定義出一個Java類,即java.lang.Class類的一個實例。除此之外,ClassLoader還負責加載Java應用所需的資源,如圖像文件和配置文件等,ClassLoader中與加載類相關的方法如下:
方法 說明
getParent() 返回該類加載器的父類加載器
loadClass(String name) 加載名稱為二進制名稱為name的類,返回的結果是java.lang.Class類的實例。
findClass(String name) 查找名稱為name的類,返回的結果是java.lang.Class類的實例。
findLoaderClass(String name) 查找名稱為name的已經被加載過的類,返回的結果是java.lang.Class類的實例。
resolveClass(Class<?> c) 鏈接指定的Java類。
---------------------
作者:A coding monkey
來源:CSDN
原文:https://blog.csdn.net/qq_36795474/article/details/79439206
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
類加載過程和類加載器