Java 類的加載機制
什麼是類的加載
1.過程
類的加載就是指
- 將類的.class 文件生成爲二進制數據
- 將二進制數據放置到內存memory中,並放置在運行時數據區的方法區中
- 在堆內從創建java.lang.Class 對象,用來封裝在方法區內的數據結構。
2. 作用
- 類的加載的最終產品是位於堆內的Class對象。
- Class對象具有封裝方法區數據結構的能力,並且向Java程序員提供了訪問方法區內的數據結構的接口。
3.注意點
- 類加載器並不需要等到類被第一次主動使用的時候纔會主動加載它,JVM規範裏面允許類加載器在預料某個類將要被使用的時候預先加載它。
- 如果出現了 類class存在文件丟失或損壞的情況,只有類被首次使用的時候纔會報錯。
類的生命週期
包含以下五個階段:
- 加載
- 驗證
- 準備
- 解析
- 初始化
Attention:
加載,驗證,準備,初始化 是按照順序開始,並不一定按照順序進行或結束
解析的順序不固定
1.加載
加載時 類的加載是第一步,虛擬機需要完成以下部分的操作:
1、通過一個類的全限定名來獲取其定義的二進制字節流。
2、將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
3、在Java堆中生成一個代表這個類的java.lang.Class對象,作爲對方法區中這些數據的訪問入口
2.連接
-
驗證:確保被加載類的正確性
驗證是連接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致會完成4個階段的檢驗動作:-
文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍之內、常量池中的常量是否有不被支持的類型。
-
元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object之外。
-
字節碼驗證:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。
-
符號引用驗證:確保解析動作能正確執行。
驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。
-
-
準備:爲類的靜態變量分配內存,並將其初始化爲默認值
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:
-
這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中。
-
這裏所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
假設一個類變量的定義爲:
public static int value = 3;
那麼變量value在準備階段過後的初始值爲0,而不是3,因爲這時候尚未開始執行任何Java方法,而把value賦值爲3的put static指令是在程序編譯後,存放於類構造器()方法之中的,所以把value賦值爲3的動作將在初始化階段纔會執行。
這裏還需要注意如下幾點: · 對基本數據類型來說,對於類變量(static)和全局變量,如果不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來說,在使用前必須顯式地爲其賦值,否則編譯時不通過。 · 對於同時被static和final修飾的常量,必須在聲明的時候就爲其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在聲明時顯式地爲其賦值,也可以在類初始化時顯式地爲其賦值,總之,在使用前必須爲其顯式地賦值,系統不會爲其賦予默認零值。 · 對於引用數據類型reference來說,如數組引用、對象引用等,如果沒有對其進行顯式地賦值而直接使用,系統都會爲其賦予默認的零值,即null。 · 如果在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值。
- 如果類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那麼在準備階段變量value就會被初始化爲ConstValue屬性所指定的值。
假設上面的類變量value被定義爲: public static final int value = 3;
編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲3。回憶上一篇博文中對象被動引用的第2個例子,便是這種情況。我們可以理解爲static final常量在編譯期就將其結果放入了調用它的類的常量池中
-
-
解析:把類中的符號引用轉換爲直接引用
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。符號引用就是一組符號來描述目標,可以是任何字面量。
直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
3.初始化
初始化,爲類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:
① 聲明類變量是指定初始值
② 使用靜態代碼塊爲類變量指定初始值
JVM初始化步驟
1、假如這個類還沒有被加載和連接,則程序先加載並連接該類
2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3、假如類中有初始化語句,則系統依次執行這些初始化語句
類初始化時機:只有當對類的主動使用的時候纔會導致類的初始化,類的主動使用包括以下六種:
-
創建類的實例,也就是new的方式
-
訪問某個類或接口的靜態變量,或者對該靜態變量賦值
-
調用類的靜態方法
-
反射(如Class.forName(“com.shengsiyuan.Test”))
-
初始化某個類的子類,則其父類也會被初始化
-
Java虛擬機啓動時被標明爲啓動類的類(Java Test),直接使用java.exe命令來運行某個主類
結束生命週期
在如下幾種情況下,Java虛擬機將結束生命週期
-
執行了System.exit()方法
-
程序正常執行結束
-
程序在執行過程中遇到了異常或錯誤而異常終止
-
由於操作系統出現錯誤而導致Java虛擬機進程終止
3.類的加載
類加載有三種方式:
1、命令行啓動應用時候由JVM初始化加載
2、通過Class.forName()方法動態加載
3、通過ClassLoader.loadClass()方法動態加載
package com.neo.classloader;
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()來加載類,不會執行初始化塊
loader.loadClass("Test2");
//使用Class.forName()來加載類,默認會執行初始化塊
// Class.forName("Test2");
//使用Class.forName()來加載類,並指定ClassLoader,初始化時不執行靜態塊
// Class.forName("Test2", false, loader);
}
}
分別切換加載方式,會有不同的輸出結果。
Class.forName()和ClassLoader.loadClass()區別
Class.forName():將類的.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊;
ClassLoader.loadClass():只幹一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance纔會去執行static塊。
注:
Class.forName(name, initialize, loader)帶參函數也可控制是否加載static塊。並且只有調用了newInstance()方法採用調用構造函數,創建類的對象 。