類加載子系統
類文件首先需要經過類加載子系統,進行加載,進類信息等加載到運行時數據區,生成Klass的實例。
在類加載子系統中有以下3個階段操作(廣義上的加載):
- 加載階段
- Bootstrap ClassLoader:引導類加載器,主要加載JDK里面的核心類
- Extension ClassLoader:拓展類加載器
- Application ClassLoader:應用加載器
- 鏈接階段
- 驗證
- 鏈接
- 解析
- 初始化階段
如果加載的時候失敗了,則不會執行后面的鏈接等操作。
類加載子系統的作用:
- 類加載器子系統可以從本地文件或者網絡中加載Class文件,Class文件開頭有特定標識“CAFEBABY”(魔數)。
- 類加載器只負責將文件加載到運行時數據區,但是否可以運行,是執行引擎管的。
- 加載的類信息存放在方法區中,除了類信息以外,方法區還存放了運行時產量池信息,可能HIA包括字符串字面量和數字常量(這部分常量是Class文件中常量池部分的內存映射)。
譬如反編譯后,會產生常量信息,里面包括常量以及符號引用等:
類加載器ClassLoader的角色,以下面的People.class為例:
通過類信息實例,可以通過new 實例化對象,也可以通過getClassLoader()獲取類加載器,也可以通過實例getClass()獲取類信息實例。
- People.class 存在本地硬盤上,相當於一個模板,最終可以實例化出n個同一個類但是屬性不同的實例。
- People.class加載到JVM中,被稱為DNA元數據模板,存放在方法區,也就是類信息。類信息也是對象。
- 從.class文件,到加載到JVM中,稱為元數據模板,這個過程需要一個轉換工具,這個工具就是類加載器(Class Loader)。
加載(Loading)
此處的加載,指的是類加載過程中的第一個階段(環節),主要工作包括:
- 1.通過類的全限定名獲取定義此類的二進制字節流。
- 2.將這個二進制字節流所代表的靜態存儲結構轉化為方法區(JDK7以及之前叫永久代,JDK8之后成為元空間)的運行時數據結構。
- 3.在內存中生成一個該類的
java.lang.Class
對象,作為方法區該類的各種數據的訪問入口,也就是類信息對象。
類的.class文件來源方式包括以下:
- 本地系統直接加載
- 網絡傳輸獲取
- 從zip壓縮包讀取
- 運行的時候計算生成,譬如動態代理技術
- 由其他文件生成,譬如場景:JSP
- 從加密文件中解密獲得
鏈接
鏈接階段又分為3個階段:
- 驗證:
- 目的是校驗安全和法,確保Class文件的字節流中包含信息符合當前虛擬機要求,保證加載的類的正確性,不會危害到虛擬機的安全。
- 主要包括4種驗證:
- 文件格式驗證(譬如文件開頭是"CAFEBABY")
- 元數據驗證
- 字節碼驗證
- 符號引用驗證
- 准備:
- 為類變量(static)分配內存並且設置該變量的默認初始值,即零值
- 不包含final修飾的static,因為final在編譯的時候已經分配了,准備階段會顯示初始化。
- 不會為實例變量分配初始化,類變量會分配在方法區,但是實例變量是跟隨對象一起分配在Java堆里面(一般情況)
- 解析:
- 將常量池的符號引用轉化成為直接引用的過程
- 事實上,解析操作往往會伴隨JVM在執行完初始化之后再執行
- 符號引用就是一組符號來描述所引用的目標,《Java虛擬機規范》的Class文件格式中,直接引用就是直接指向目標的指針,相對偏移量或者一個間接定位到目標的句柄。
- 解析這個階段,主要是針對類或者接口,字段,類方法,接口方法,方法類型等等,對應的常量池中的CONSTANT_Class_info,CONSTANT_Fieldred_info,CONSTANT_Methodref_info等。
初始化
初始化,就是執行類的構造器<clinit>()
的過程,注意<clinit>()
是類的構造器,不是對象的。<clinit>()
是初始化類的,就是把類裝到JVM里的初始化,不是運行時對象的初始化。
<clinit>()
這個方法不需要顯式定義,而是javac
編譯器自動收集類中的所有變量的賦值動作,加上靜態代碼塊,合並成的一個方法。
<clinit>()
中代碼的順序和我們在類文件寫的順序一致。
執行子類的<clinit>()
方法之前,JVM會保證先執行其父類的<clinit>()
,默認父類是Object
。
仔細觀察上面的代碼,會發現,final的屬性,即使是static修飾的,在<clinit>()
里面都不會存在,這是為什么呢?
這是因為final修飾的是常量,常量不會在初始化的時候執行賦值!!!常量在編譯的時候已經分配了,准備階段會顯示初始化。
如果我們將final去掉,就可以發現,去掉final修飾,字節碼就會加上該字段的賦值:(下面的ldc是指常量池的意思,從常量池編號為#6的地方,加載該常量)
虛擬機在初始化的時候,已經保證了類的<clinit>()
方法,即使在多線程的環境下,也只會執行一次,其底層的邏輯就是默認同步加鎖了。
【作者簡介】:
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java源碼解析,JDBC,Mybatis,Spring,redis,分布式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花里胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。
平日時間寶貴,只能使用晚上以及周末時間學習寫作,關注我,我們一起成長吧~