虛拟機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型,這就是虛拟機的類加載機制。
類加載的規則:
全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也将由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
父類委托,先讓父類加載器試圖加載該類,隻有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
緩存機制,緩存機制将會保證所有加載過的Class都會被緩存,當程式中需要使用某個Class時,類加載器先從緩存區尋找該Class,隻有緩存區不存在,系統才會讀取該類對應的二進制資料,并将其轉換成Class對象,存入緩存區。這就是為什麼修改了Class後,必須重新開機JVM,程式的修改才會生效。
類加載的過程:
包括加載、連結(含驗證、準備、解析)、初始化
如下圖所示:

1、加載:
類加載指的是将類的class檔案讀入記憶體,并為之建立一個java.lang.Class對象,作為方法區這個類的資料通路的入口。
也就是說,當程式中使用任何類時,系統都會為之建立一個java.lang.Class對象。具體包括以下三個部分:
(1)通過類的全名産生對應類的二進制資料流。(根據early load原理,如果沒找到對應的類檔案,隻有在類實際使用時才會抛出錯誤)
(2)分析并将這些二進制資料流轉換為方法區方法區特定的資料結構
(3)建立對應類的java.lang.Class對象,作為方法區的入口(有了對應的Class對象,并不意味着這個類已經完成了加載連結)
通過使用不同的類加載器,可以從不同來源加載類的二進制資料,通常有如下幾種來源:
(1)從本地檔案系統加載class檔案,這是絕大部分程式的加載方式
(2)從jar包中加載class檔案,這種方式也很常見,例如jdbc程式設計時用到的資料庫驅動類就是放在jar包中,jvm可以從jar檔案中直接加載該class檔案
(3)通過網絡加載class檔案
(4)把一個Java源檔案動态編譯、并執行加載
2、連結:
連結指的是将Java類的二進制檔案合并到jvm的運作狀态之中的過程。在連結之前,這個類必須被成功加載。
類的連結包括驗證、準備、解析這三步。具體描述如下:
2.1 驗證:
驗證是用來確定Java類的二進制表示在結構上是否完全正确(如檔案格式、文法語義等)。如果驗證過程出錯的話,會抛出java.lang.VertifyError錯誤。
主要驗證以下内容:
檔案格式驗證
驗證位元組流是否符合class檔案格式的規範,并且能被目前虛拟機處理,如是否以魔數0xCAFEBABE開頭、主次版本号是否在目前虛拟機處理範圍内、常量池是否有不支援的常量類型等。隻有經過格式驗證的位元組流,才會存儲到方法區的資料結構,剩餘3個驗證都基于方法區的資料進行。
中繼資料驗證
對位元組碼描述的資料進行語義分析,以保證符合Java語言規範,如是否繼承了final修飾的類、是否實作了父類的抽象方法、是否覆寫了父類的final方法或final字段等。
位元組碼驗證
對類的方法體進行分析,確定在方法運作時不會有危害虛拟機的事件發生,如保證操作數棧的資料類型和指令代碼序列的比對、保證跳轉指令的正确性、保證類型轉換的有效性等。
符号引用驗證
為了確定後續的解析動作能夠正常執行,對符号引用進行驗證,如通過字元串描述的全限定名是都能找到對應的類、在指定類中是否存在符合方法的字段描述符等。
2.2 準備:
準備過程則是建立Java類中的靜态域(static修飾的内容),并将這些域的值設定為預設值,同時在方法區中配置設定記憶體空間。準備過程并不會執行代碼。
注意這裡是做預設初始化,不是做顯式初始化。例如:
<code>public static int value = 12;</code>
上面的代碼中,在準備階段,會給value的值設定為0(預設初始化)。在後面的初始化階段才會給value的值設定為12(顯式初始化)。
但是有個特殊情況:
public static final int value = 12;
在編譯階段會為value生成ConstantValue屬性,在準備階段虛拟機會根據ConstantValue屬性将value指派為100。
2.3 解析:
解析的過程就是確定這些被引用的類能被正确的找到(将符号引用替換為直接引用)。解析的過程可能會導緻其它的Java類被加載。
符号引用和直接引用有什麼不同?
1、符号引用使用一組符号來描述所引用的目标,可以是任何形式的字面常量,定義在Class檔案格式中。
2、直接引用可以是直接指向目标的指針、相對偏移量或則能間接定位到目标的句柄。
3、初始化:
初始化階段是類加載過程的最後一步。到了初始化階段,才真正執行類中定義的Java程式代碼(或者說是位元組碼)。
初始化階段是執行類構造器<clinit>方法的過程,<clinit>方法由類變量的指派動作和靜态語句塊按照在源檔案出現的順序合并而成,該合并操作由編譯器完成。
1、<clinit>方法對于類或接口不是必須的,如果一個類中沒有靜态代碼塊,也沒有靜态變量的指派操作,那麼編譯器不會生成<clinit>;
2、<clinit>方法與執行個體構造器不同,不需要顯式的調用父類的<clinit>方法,虛拟機會保證父類的<clinit>優先執行;
3、為了防止多次執行<clinit>,虛拟機會確定<clinit>方法在多線程環境下被正确的加鎖同步執行,如果有多個線程同時初始化一個類,
那麼隻有一個線程能夠執行<clinit>方法,其它線程進行阻塞等待,直到<clinit>執行完成。
4、注意:執行接口的<clinit>方法不需要先執行父接口的<clinit>,隻有使用父接口中定義的變量時,才會執行。
在以下幾種情況中,會執行初始化過程:
(1)建立類的執行個體 (2)通路類或接口的靜态變量(特例:如果是用static final修飾的常量,那就不會對類進行顯式初始化。static final 修改的變量則會做顯式初始化) (3)調用類的靜态方法 (4)反射(Class.forName(packagename.className)) (5)初始化類的子類。注:子類初始化問題:滿足主動調用,即父類通路子類中的靜态變量、方法,子類才會初始化;否則僅父類初始化。 (6)java虛拟機啟動時被标明為啟動類的類
類的初始化過程(重要)
Student s = new Student();在記憶體中做了哪些事情?
加載Student.class檔案進記憶體
在棧記憶體為s開辟空間
在堆記憶體為學生對象開辟空間
對學生對象的成員變量進行預設初始化
對學生對象的成員變量進行顯示初始化
通過構造方法對學生對象的成員變量指派
學生對象初始化完畢,把對象位址指派給s變量
初始化順序問題