第四章 類加載機制,第四章加載機制
注:本文主要參考自《深入理解java虛擬機(第二版)》
在查看本文前,先要了解JVM內存結構,見 第一章 JVM內存結構
1、類加載流程
- 把描述類的數據從xxx.class文件加載到JVM內存
- 對這些數據進行校驗、准備、解析(這三個過程總稱為"鏈接")
- 對這些數據進行初始化,最終形成可被JVM直接使用的Class對象
注意:
2、加載
- 作用:把描述類的數據從xxx.class文件加載到JVM內存
- 此階段完成三件事
- 通過一個類的全限定類名來獲取定義此類的二進制字節流
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
- 在方法區生成一個java.lang.Class對象,作為方法區內該類的各種數據的訪問入口
- 注意:
- 類加載的完成是靠全限定類名和類加載器(ClassLoaderA實例)完成,這兩個元素也是標識一個已被加載類的標識
- 接口、類:名稱為"全限定類名+ClassLoader實例ID",這種類型的類由所在的ClassLoader負責加載
- 數組:"[基本類型"或"[L引用類型;"(eg.byte[] bytes = new byte[512];//類名為"[B";Object[] objs = new Object[10];//類名為"[Ljava.lang.Object;")
- 注意基本類型名稱都是取自首字母大寫(eg.byte:B),有兩個例外boolean:Z;long:J
- 數組類由JVM直接創建,其數組元素由類加載器加載
3、驗證
- 四部分驗證
- 文件格式驗證:當通過一個類的全限定類名獲取了定義此類的二進制字節流後,檢驗該字節流是否滿足class文件格式(例如:開頭是魔數,主次版本號是否是當前虛擬機處理范圍之內,即是否在次到主之間的版本,高於主的版本不加載(這就是低版本的jdk無法執行高版本jdk的原因),等等),檢驗過後,執行"加載"部分的第二件事,將數據放入方法區並存儲
- 格式不符合:java.lang.VerifyError
- 元數據驗證:保證類的元數據信息符合java語言規范(例如:這個類若是普通類,是否實現了其實現接口下的所有類)
- 關於類的元數據有哪些,查看 第二章 Javac編譯原理
- 驗證方法體的字節碼指令,確定指令順序符合邏輯
- 符號引用驗證:
- 發生在解析階段:將符號引用轉化為直接引用的過程
- 對符號引用的驗證,保證這些符號引用(屬性、方法等)存在,並且具備相應的權限(eg.保證private的方法不可被其他類訪問)
- NoSuchMethodError、NoSuchFieldError、IllegalAccessError
- 注意:
- 驗證階段可能會穿插在類加載的其他階段
- -Xverify:none:關閉大部分的驗證操作,這個我們一般不會配置這個參數(即我們一般還是會去做驗證的),因為類加載雖然發生運行期,但是大部分的類加載行為是在服務器啟動的時候就發生了,實際上不影響我們的程序運行。當然,如果我們手動調用了Class.forName(),this.getClass.getClassLoader().loadClass(),這樣就會執行到這句代碼的時候加載類,但畢竟是少數。通常我們會使用Class.forName()來引入MySQL驅動,這也就是在maven中引入MySQL驅動包的時候,其scope是runtime的原因。
4、准備
- 作用:為類變量分配內存(這些內存在方法區分配)、設置類變量零值
- 注意:
- 為類變量分配內存(為static對象分配內存,這也就是static變量存儲在方法區的原理)
- 類中的實例變量會隨著對象實例的創建一起分配在堆中,當然若是基本數據類型的話,會隨著對象的創建直接壓入操作數棧
- 為static變量分配零值(默認零值)(eg.static int x = 123;//這時候x = 0,而x = 123是在"初始化"階段發生的)
- 為final變量分配初始值(期望值)(eg.final int y = 456;//這時候y = 456)
5、解析
- 作用:符號引用轉化為直接引用
- 符號引用(編譯期):存儲於class文件的常量池中(見 第三章 類文件結構與javap的使用),只有一些名稱,沒有實際的地址
- 直接引用(運行期):在該階段,會為符號引用分配實際的內存,之後符號引用就轉化為了直接引用,存儲於運行時常量池
- 常量池:class文件中的概念
- 運行時常量池:JVM內存結構中方法區內的一個組成部分
- 注意
- 該步驟在"初始化"階段之前不一定發生,但是一定要在一個符號引用被使用前解析。
6、初始化
- 作用:執行靜態塊(static{})、初始化(按意願)static變量(eg.static int x = 123;//"准備"階段後,x = 0;"初始化"後,x = 123)、執行構造器
- 發生的時機:
- new:執行構造器
- 子類調用了初始化,若父類沒有初始化,則父類先要初始化(eg.子類調用了"new 無參構造器",則先要執行父類的"無參構造器")
- 反射調用了類,若類沒有初始化,需要先初始化
- 虛擬機啟動時,包含main()方法的類要初始化
- 注意:static{}與static變量初始化的發生不一定會發生在服務器啟動時(即不一定發生在類加載時),若想要達到容器啟動後,就執行一段代碼xxx,可以將類實現spring的InitalizingBean,重寫該接口下的afterPropertiesSet()方法,將xxx代碼寫入該方法中。
總結:
- 類加載流程
- "加載"第一階段:通過一個類的全限定類名來獲取定義此類的二進制字節流
- "驗證"第一階段:文件格式驗證
- "加載"第二階段:將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
- "加載"第三階段:在方法區生成一個java.lang.Class對象,作為方法區內該類的各種數據的訪問入口
- "驗證"第二階段:元數據驗證
- "驗證"第三階段:驗證方法體的字節碼指令
- "准備"第一階段:類變量分配內存
- "准備"第二階段:設置類變量零值
- "驗證"第四階段:符號引用驗證(追隨於"解析",僅發生在"解析"的時候)
- "解析":符號引用轉化為直接引用(不是必須要在"初始化"之前的步驟的步驟)
- "初始化":不一定發生在何時,但是一定要在"加載"、"驗證"、"准備"之後
- 關於驗證部分,我們可能會懷疑既然javac在"語法分析"和"語義分析"部分已經做了大量的驗證,類加載的時候為什麼還要進行驗證?
- javac並沒有做在類加載部分的所有驗證,例如:魔數驗證
- 不是所有的class文件都是由javac產生的,還有第三方的jar,甚至是自己偽造的class
- 以上步驟,從"驗證"第二階段開始到"初始化"之前,都是在方法區進行,這個地方也是類加載的主要場所。