一:類加載
1.類的生命週期
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備、解析3個部分統稱爲連接。其中,加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班的開始(並不是進行或完成),因爲這些階段通常都是相互交叉混合地進行,而解析階段則不一定。它在某些情況下可以在初始化之後再開始,這是爲了支持java語言的運行時綁定。
2.類加載的時機
類何時被加載,java虛擬機規範沒有進行強制約束,虛擬機可根據自身實現自由把握,但是對初始化階段,虛擬機規範有嚴格規定有且只有5種情況必須立即對類進行“初始化“(當然,加載,驗證,準備要在這之前開始)。
1.遇到new、getstatic、putstatic(讀取設置一個類的靜態字段,被final修飾,已在編譯期把結果放入常量池的靜態字段除外)或者invokestatic(調用一個類的靜態方法)這4條字節碼指令時,若還沒有初始化,就要先對類進行初始化。
2.使用java.lang.reflect包方法對類進行反射調用的時候;
3.當初始化一個類時,如果發現其父類還沒有進行初始化;
4.當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main方法的那個類),虛擬機會先初始化這個主類。
5.當使用jdk1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。
實例1:當父類中定義一個靜態塊和一個static屬性,子類定義一個靜態塊,主方法去用子類訪問父類的static屬性時,子類的靜態塊是不會被執行的,也就是說子類是不會被初始化的,只會初始化父類。
實例2:當父類中定義一個靜態塊和一個static屬性,子類定義一個靜態塊,主方法new一個父類對象,這個時候,不會初始化父類,實際上,這是觸發了另外一個名爲父類的類的初始化,對用戶而言,這是個不合法的類名稱,它是一個由虛擬機生成的,直接繼承於java.lang.object的子類。
實例3:當類中定義一個靜態塊和一個static final屬性,主方法中使用了這個靜態常量時,並不會觸發該類的初始化,因爲該static final屬性實際上在編譯階段通過常量傳播優化,已經將此常量的值存儲到了主方法所在類的常量池中,對該常量的引用實際上都被轉化爲對主方法所在類對自身常量池的引用了。也就是說,實際上主方法所在類的class文件之中並沒有該類的符號引用入口。
接口的初始化和類大致相同,只是接口中不允許有靜態塊,在類進行初始化的第3條與接口略有不同,類進行初始化時,必須保證父類全部都已經初始化過了,但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)纔會初始化。
二:類加載過程
2.1 加載階段
加載階段的三個主要步驟就是:
- 通過一個類的全限定名來獲取定義此類的二進制字節流;
- 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;
- 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。
注意:二進制字節流並不一定就是class文件,這裏並沒有明確規定:
- 從zip包中讀取,最終成爲日後的JAR,EAR,WAR格式基礎
- 從網絡中獲取,這種場景典型的應用就是Applet。
- 運行時計算生成,這種場景使用的最多的就是動態代理技術,在java.lang.reflect.proxy中,就是用了ProxyGenerateProxyClass來爲特定接口生成形式爲“*$Proxy“的代理類的二進制字節流。
- 由其他文件生成,典型場景是JSP應用,即由JSP文件生成對應的class文件;
- 從數據庫中讀取,例如有些中間件服務器可以選擇把程序安裝到數據庫中來完成程序代碼在集羣之間分發。
在加載階段進行的時候,連接階段就可以開始了。這兩個階段的開始時間仍然保持着固定的先後順序。
2.2 驗證
這一階段的主要目的就是爲了確保class文件的字節流中包含符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致會完成下面4個階段的檢驗動作:文件格式驗證、元數據驗證、字節驗證、符號引用驗證。
文件格式驗證:驗證字節流是否符合class文件格式的規範,並且能被當前版本的虛擬機處理。
元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息是否符合java語言規範的要求。
字節碼驗證:主要目的是通過數據流和控制流分析,確定程序語義是合法的,符合邏輯的。對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機的安全事件。
符號驗證:發生在虛擬機將符號引用轉化爲直接引用時,這個轉化過程在解析階段發生,符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。
2.3 準備
該階段正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中分配,注意這裏的類變量內存分配僅僅包含被static修飾的類變量,而不包含實例變量。實例變量將會在對象實例化時隨着對象一起分配在java堆中。並且初始化值一般爲0,即使靜態變量看似賦值爲其他,但這一階段實際上是先初始化爲0,之後在putstatic指令編譯之後,纔會真正賦值。但是在特殊情況下,比如類字段的字段屬性中存在ConstantValue屬性,在準備階段變量就會被初始化爲所指定的值,比如final static屬性,在編譯的時候就會爲該變量生成ConstantValue屬性,在準備階段虛擬機會根據該屬性的設置將變量賦值爲指定值。
2.4 解析
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
符號引用:以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可,符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到內存中,各種虛擬機實現內存佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因爲符號引用的字面量形式明確定義在Java虛擬機規範的class文件格式中。
直接引用:直接引用可以是直接指向目標的指針,相對便宜量或是一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局相關的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在內存中存在。
解析動作主要是針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行,分別對應於常量池中的7中常量類型。
2.5 初始化
到初始化階段,才真正開始執行類中定義的java程序代碼(或者說字節碼)。在準備階段,變量已經賦過一次系統要求的初始值了,在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源。
三:雙親委派模型
3.1 類加載器
1. 啓動類加載器:這個類加載器負責將存放在<JAVA_HOME>\lib目錄中的並且被虛擬機識別的類庫加載到虛擬機內存中。它無法被java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導類加載器,那麼直接使用null代替就可以了。
2. 擴展類加載器:這個加載器負責加載<JAVA_HOME>\lib\ext目錄中的所有類庫,開發者可以直接使用擴展類加載器。
3. 應用程序類加載器:這個加載器也稱爲系統加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器。如果應用程序沒有自定義過自己的類加載器,一般情況下這個就是程序默認的類加載器。
4. 自定義類加載器:我們的應用程序一般都是由上面3個類加載器配合進行加載的,如果有必要,還可以自己定義類加載器。
3.2 雙親委派模型以及工作過程
如圖:該圖表示以上介紹的4中類加載器之間的層次關係,這種關係稱爲類加載器的雙親委派模型。
雙親委派模型要求除了頂層的啓動類加載器之外,其餘的類加載器都應當有自己的父類加載器。這裏的類加載器之間的父子關係一般不會以繼承關係實現,而是以組合的關係來複用父加載器的代碼。它不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式。
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的類加載請求最終都應該傳送到啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍未找到所需的類)時,子加載器纔會嘗試自己去加載。
雙親委派模型的一個好處就是:Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。比如java.lang.Object類,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類,保證了java類型體系中最基礎的行爲。
雙親委派模型的實現:它的實現存在於java.lang.ClassLoader的loadClass()方法中。首先檢查是否已經被加載過,若沒有加載,則調用父加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父類加載失敗,拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。