類初始化,類加載,類加載器
- 1. 類加載
- 1.1. 類的加載
- 1.2. 類的鏈接
- 1.2.1. 驗證
- 1.2.2. 準備
- 1.2.3. 解析
- 2. 類加載器
- 2.1. 類加載器分為四種:前三種為虛擬機自帶的加載器。
- 2.2. 類加載有三種方式:
- 2.3. **JVM類加載機制**
- 2.4. 雙親委派機制
- 3. 類的初始化
- 3.1. 類的主動引用會發生類的初始化
- 3.2. 類的被動引用不會發生類的初始化
- 3.3. 類緩存
1. 類加載
1.1. 類的加載
類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然后在堆區創建一個 java.lang.Class
對象,用來封裝類在方法區內的數據結構。
-
類的加載的最終產品是位于堆區中的
Class
對象,Class
對象封裝了類在方法區內的數據結構,并且向Java程序員提供了訪問方法區內的數據結構的接口。 -
在加載階段,虛擬機需要完成以下三件事情:
- 通過一個類的全限定名來獲取其定義的二進制字節流。
- 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
- 在Java堆中生成一個代表這個類的
java.lang.Class
對象,作為對方法區中這些數據的訪問入口。
-
加載.class文件的方式
- 從本地系統中直接加載
- 通過網絡下載.class文件
- 從zip,jar等歸檔文件中加載.class文件
- 從專有數據庫中提取.class文件
- 將Java源文件動態編譯為.class文件
1.2. 類的鏈接
- 將java類的二進制代碼合并到JVM的運行狀態中的過程
1.2.1. 驗證
- 驗證:確保加載的類信息符合JVM規范,沒有安全方面的問題
- 驗證階段大致會完成4個階段的檢驗動作:
- 文件格式驗證:驗證字節流是否符合Class文件格式的規范;例如:是否以
0xCAFEBABE
開頭、主次版本號是否在當前虛擬機的處理范圍之內、常量池中的常量是否有不被支持的類型。 - 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規范的要求;例如:這個類是否有父類,除了
java.lang.Object
之外。 - 字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
- 符號引用驗證:確保解析動作能正確執行。
- 文件格式驗證:驗證字節流是否符合Class文件格式的規范;例如:是否以
1.2.2. 準備
- 準備:正式為類變量(static)分配內存并設置類變量默認初始值的階段,這些內存都將在方法區中進行分配
該階段有以下幾點需要注意:
- 1、這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。
- 2、這里所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
- 3、如果類字段的字段屬性表中存在
ConstantValue
屬性,即同時被final和static修飾,那么在準備階段變量value就會被初始化為ConstValue屬性所指定的值。
假設一個類變量的定義為: public static int value=3
;那么變量value在準備階段過后的初始值為0,而不是3,因為這時候尚未開始執行任何Java方法,而把value賦值為3的 public static
指令是在程序編譯后,存放于類構造器 <clinit>()
方法之中的,所以把value賦值為3的動作將在初始化階段才會執行
- 對基本數據類型來說,對于類變量(static)和全局變量,如果不顯式地對其賦值而直接使用,則系統會為其賦予默認的零值,而對于局部變量來說,在使用前必須顯式地為其賦值,否則編譯時不通過。
- 對于同時被static和final修飾的常量,必須在聲明的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在聲明時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統不會為其賦予默認零值。
- 對于引用數據類型reference來說,如數組引用、對象引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會為其賦予默認的零值,即null。
- 如果在數組初始化時沒有對數組中的各元素賦值,那么其中的元素將根據對應的數據類型而被賦予默認的零值。
1.2.3. 解析
- 解析:虛擬機常量池內的符號引用(常量名)替換為直接引用(地址)的過程
- 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
2. 類加載器
2.1. 類加載器分為四種:前三種為虛擬機自帶的加載器。
- 啟動類加載器(Bootstrap)C++
- 負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實現,不是ClassLoader子類
- 擴展類加載器(Extension)Java
- 負責加載java平臺中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/ *.jar或-Djava.ext.dirs指定目錄下的jar包的一些jar包
- 應用程序類加載器(AppClassLoader)Java
- 也叫系統類加載器,負責加載classpath中指定的jar包及目錄中class
- 用戶自定義加載器 Java.lang.ClassLoader的子類,用戶可以定制類的加載方式
2.2. 類加載有三種方式:
- 1、命令行啟動應用時候由JVM初始化加載
- 2、通過Class.forName()方法動態加載
- 3、通過ClassLoader.loadClass()方法動態加載
2.3. JVM類加載機制
- 全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入
- 父類委托,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
- 緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統才會讀取該類對應的二進制數據,并將其轉換成Class對象,存入緩存區。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效
2.4. 雙親委派機制
- 雙親委派機制:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上。
- 1、當AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
- 2、當ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
- 3、如果BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib里未查找到該class),會使用ExtClassLoader來嘗試加載;
- 4、若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載
- 5、如果AppClassLoader也加載失敗,則會報出異常ClassNotFoundException
- 好處:
-
防止內存中出現多份同樣的字節碼(安全性角度)
- 比如加載位于 rt.jar 包中的類 java.lang.Object,不管是哪個加載器加載這個類,最終都是委托給頂層的啟動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個 Object對象。
-
保證Java程序安全穩定運行
-
3. 類的初始化
在Java中對類變量進行初始值設定有兩種方式:①聲明類變量時指定初始值;②使用靜態代碼塊為類變量指定初始值。
- 特點
- 執行類構造器
<clinit>()
方法的過程。- 類構造器
<clinit>()
方法是由編譯器自動收集類中所有類變量的賦值動作和靜態代碼塊中的語句合并產生的。(類構造器是構造類信息的,不是構造該類對象的構造器)
- 類構造器
- 當初始化一個類時,如果其父類還沒有進行初始化,則先觸發其父類的初始化
- 虛擬機會保證一個類的
<clinit>()
方法在多線程環境中被正確加鎖和同步
- 執行類構造器
3.1. 類的主動引用會發生類的初始化
- 當虛擬機啟動,會先初始化main方法所在的類
- new一個類的對象
- 調用類的靜態成員(除了final常量)和靜態方法
- 使用Java.lang.reflect包的方法對類進行反射調用
- 初始化一個類時,其父類如果沒有初始化,則會先初始化它的父類
3.2. 類的被動引用不會發生類的初始化
- 訪問一個靜態域時,只有真正聲明這個域的類才會被初始化。如:通過子類引用父類的靜態變量,不會導致子類初始化
- 通過數組定義類引用,不會觸發此類的初始化
- 引用常量不會觸發此類的初始化(常量在鏈接階段就存入調用類的常量池中了)
3.3. 類緩存
標準的javaSE類加載器可以按要求查找類,一旦某個類 被加載到類加載器中,它將維持加載(緩存)一段時間。但是JVM垃圾回收機制可以回收這些Class對象