新聞中心
理解類加載機制
Class文件是各種編譯器編譯生成的二進制文件,在Class文件中描述了各種與該類相關的信息,但是Class文件本身是一個靜態(tài)的東西,想要使用某個類的話,需要java虛擬機將該類對應的Class文件加載進虛擬機中之后才能進行運行和使用。
舉個例子,Class文件就好比是各個玩具設計商提供的設計方案,這些方案本身是不能直接給小朋友玩的,需要玩具生產(chǎn)商根據(jù)方案的相關信息制造出具體的玩具才可以給小朋友玩。那么不同的設計商有他們自己的設計思路,只要最終設計出來的方案符合生產(chǎn)商生產(chǎn)的要求即可。生產(chǎn)商在生產(chǎn)玩具時,首先會根據(jù)自己的生產(chǎn)標準對設計商提交來的方案進行閱讀,審核,校驗等一系列步驟,如果該方案符合生產(chǎn)標準,則會根據(jù)方案創(chuàng)建出對應的模具,當經(jīng)銷商需要某個玩具時,生產(chǎn)商則拿出對應的模具生產(chǎn)出具體的玩具,然后把玩具提交給經(jīng)銷商。
對于java而言,虛擬機就是玩具生產(chǎn)商,設計商提交過來的方案就是一個個的Class文件,方案創(chuàng)建的模具就
總的來說,類的加載過程,包括卸載在內(nèi)的整個生命周期共有以下7個階段:
加載、驗證、準備、初始化、卸載這5個階段的順序是確定的,但是解析階段不一定,在某些情況下解析可以在初始化之后再執(zhí)行,為了支持java的運行時綁定,也成為動態(tài)綁定或晚期綁定。invokedynamic指令就是用于動態(tài)語言支持,這里“動態(tài)”的含義是必須等到城市實際運行到這條指令的時候,解析動作才開始執(zhí)行。
加載
“加載”是“類加載”過程中的一個階段,在加載階段,虛擬機需要做以下3件事情:
通過類的全限定名獲得該類的二進制字節(jié)流
將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換成方法區(qū)中的某個運行時數(shù)據(jù)結(jié)構(gòu)
- 在方法區(qū)內(nèi)存(對于HotSpot虛擬機)中生成一個代表該類的java.lang.Class對象,作為訪問方法區(qū)中該類的運行時數(shù)據(jù)結(jié)構(gòu)的外部接口
加載階段中“通過類的全限定名獲得該類的二進制字節(jié)流”這個動作,被放到java虛擬機外部實現(xiàn),目的是大限度的讓應用程序去決定該如何獲取所需的類,而實現(xiàn)該動作的代碼模塊就是類加載器(ClassLoader),JVM提供了3種類加載器:
啟動類加載器(Bootstrap ClassLoader):負責加載 JAVAHOME\lib 目錄中的,或通過-Xbootclasspath參數(shù)指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。
擴展類加載器(Extension ClassLoader):負責加載 JAVAHOME\lib\ext 目錄中的,或通過java.ext.dirs系統(tǒng)變量指定路徑中的類庫。
- 應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫。
加載階段完成后,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)中了。
驗證
加載完成后,緊接著(更確切的說是交叉執(zhí)行)虛擬機會對加載的字節(jié)流進行驗證。虛擬機如果不檢查輸入的字節(jié)流,對其安全信任的話,很可能會因為載入了有害的字節(jié)流而導致系統(tǒng)崩潰。驗證階段大致會完成4中不同的檢驗動作:
文件格式驗證
文件格式驗證主要是校驗該字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當前版本的虛擬機所接受。這個階段包括但不限于以下驗證點:
- 是否以魔數(shù)0xCAFEBABE開頭
- 主、次版本號是否在當前虛擬機處理的范圍之內(nèi)
- 常量池中是否有不支持的常量類型(通過tag校驗)
- 常量的索引是否有指向不存在或不符合類型的常量
- ...
元數(shù)據(jù)驗證
元數(shù)據(jù)驗證主要是對字節(jié)流中的描述信息(描述符)進行語義分析,以確保其描述的信息符合java語言規(guī)范的要求。這個階段包括但不限于以下驗證點:
- 這個類是否有父類,除了java.lang.Object,所有的類都應該有父類
- 這個類的父類是否繼承了不允許被繼承的類,如被final修飾的類
- 如果這個類不是抽象類,是否實現(xiàn)了父類或接口中要求的所有的方法
- ...
字節(jié)碼驗證
字節(jié)碼驗證主要是對類的方法體進行分析,確保在方法運行時不會有危害虛擬機的事件發(fā)生。這個階段包括但不限于以下驗證點:
- 操作數(shù)棧的數(shù)據(jù)類型與指令碼中所需類型是否相符
- 校驗跳轉(zhuǎn)指令是否會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
- 校驗方法體中類型轉(zhuǎn)換是否是有效的
- ...
符號引用驗證
符號引用驗證主要是對類自身以外的信息進行匹配新校驗,包括常量池中的各種符號引用。這個階段包括但不限于以下驗證點:
- 符號引用中通過字符串描述的全限定類名是否能找到對應的類
- 在指定的類中是否存在符合方法的字段描述符以及簡單名稱所描述的字段和方法
- ...
準備
準備階段是為類變量(static)在方法區(qū)中分配內(nèi)存并設置初始值(默認值,如int的默認值為0)的階段。實例變量將會在對象實例化時隨著對象一起分配在java堆中。為類變量設置初始值跟該變量是否有final修飾符有關系。 如果沒用final進行修飾,如下列的代碼:
// 準備階段執(zhí)行完成后,value變量的值為int的“零值”,即:0
// 把value賦值為10的putstatic指令是程序被編譯后,
// 存放于類構(gòu)造器()方法中的,所以具體賦值的操作會在初始化階段執(zhí)行
public static int value = 10;
如果使用了final進行修飾,如下列的代碼:
// 如果類字段的字段屬性表中有ConstantValue屬性,
// 則會在準備階段將變量的值初始化為ConstantValue所指定的值
// 準備階段執(zhí)行完成后,VALUE變量的值被賦值為20
public final static int VALUE = 20;
解析
解析階段是虛擬機將常量池中的符號引用替換為直接引用的過程,在該階段將會進行符號引用的校驗。 符號引用是Class文件中用來描述所引用目標的符號,可以是任何形式的字面量。 直接引用是虛擬機在內(nèi)存中引用具體類或接口的,可以是直接指向目標的指針,相對偏移量或者是一個能間接定位到目標的句柄。 簡單的來說,符號引用是Class類文件用來定位目標的,直接引用是虛擬機用來在內(nèi)存中定位目標的。
初始化
初始化階段是執(zhí)行類的構(gòu)造器方法
()方法對于類或接口不是必須的,如果一個類中沒有靜態(tài)代碼塊,也沒有靜態(tài)變量的賦值操作,那么編譯器不會生成 ()方法 ()方法與實例構(gòu)造器方法 ()不同,不需要顯式的調(diào)用父類的方法,虛擬機會保證父類的優(yōu)先執(zhí)行 - 為了防止多次執(zhí)行,虛擬機會確保方法在多線程環(huán)境下被正確的加鎖同步執(zhí)行,如果有多個線程同時初始化一個類,那么只有一個線程能夠執(zhí)行方法,其它線程進行阻塞等待,直到執(zhí)行完成
- 執(zhí)行接口的方法是不需要先執(zhí)行父接口的,只有使用父接口中定義的變量時,才會執(zhí)行。
java虛擬機規(guī)范嚴格規(guī)定了有且只有一下5中情況必須立即對類進行初始化:
- 遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時,生成這4條指令的最常見的java代碼場景是:使用new實例化對象時,讀取或設置類的靜態(tài)字段時,調(diào)用一個類的靜態(tài)方法時
- 使用java.lang.reflect對類進行反射調(diào)用時,如通過Class.forName()創(chuàng)建對象時
- 當初始化一個類時,如果父類還沒有初始化,則要先觸發(fā)父類的初始化,即先要執(zhí)行父類的構(gòu)造器方法
() - 啟動虛擬機時,需要初始化包含main方法的類
- 在JDK1.7中,如果java.lang.invoke.MethodHandler實例最后的解析結(jié)果REFgetStatic、REFputStatic、REF_invokeStatic的方法句柄,并且這個方法句柄對應的類沒有進行初始化
以下幾種情況,不會觸發(fā)類初始化:
- 通過子類引用父類的靜態(tài)字段,只會觸發(fā)父類的初始化,而不會觸發(fā)子類的初始化。
class Parent { static int a = 100; static { System.out.println("parent init!"); } } class Child extends Parent { static { System.out.println("child init!"); } } public class Init{ public static void main(String[] args){ // 只會執(zhí)行父類的初始化,不會執(zhí)行子類的初始化 // 將打?。簆arent init! System.out.println(Child.a); } }
- 定義對象數(shù)組,不會觸發(fā)該類的初始化。
public class Init{ public static void main(String[] args){ // 不會有任何輸出 Parent[] parents = new Parent[10]; } }
- 常量在編譯期間會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會觸發(fā)定義常量所在的類的初始化。
class Const { static final int A = 100; static { System.out.println("Const init"); } } public class Init{ public static void main(String[] args){ // Const.A會存入Init類的常量池中,調(diào)用時并不會觸發(fā)Const類的初始化 // 將打?。?00 System.out.println(Const.A); } }
- 通過類名獲取Class對象,不會觸發(fā)類的初始化。
class Cat { private string name; static { System.out.println("Cat is loaded"); } } public class Init{ public static void main(String[] args){ // 不會打印任何信息 Class catClazz = Class.class; } }
- 通過Class.forName加載指定類時,如果指定參數(shù)initialize為false時,也不會觸發(fā)類初始化,其實這個參數(shù)是告訴虛擬機,是否要對類進行初始化。
class Cat { private string name; static { System.out.println("Cat is loaded"); } } public class Init{ public static void main(String[] args) throws ClassNotFoundException{ // 不會打印任何信息 Class catClazz = Class.forName("com.test.Cat",false,Cat.class.getClassLoader()); } }
- 通過ClassLoader默認的loadClass方法,也不會觸發(fā)初始化動作
class Cat { private string name; static { System.out.println("Cat is loaded"); } } public class Init{ public static void main(String[] args) throws ClassNotFoundException{ // 不會打印任何信息 new ClassLoader(){}.loadClass("com.test.Cat"); } }
最后,附上一幅Class類加載過程的思維導圖:
是Class文件加載進虛擬機中的類,生產(chǎn)的玩具就是類的實例對象。
因此,從Class文件到對象需要經(jīng)過的步驟大致為: Class文件-->類-->實例對象 而類的加載機制,就是負責將Class文件轉(zhuǎn)換成虛擬機中的類的一個過程。
總的來說,類的加載過程,包括卸載在內(nèi)的整個生命周期共有以下7個階段:
加載、驗證、準備、初始化、卸載這5個階段的順序是確定的,但是解析階段不一定,在某些情況下解析可以在初始化之后再執(zhí)行,為了支持java的運行時綁定,也成為動態(tài)綁定或晚期綁定。invokedynamic指令就是用于動態(tài)語言支持,這里“動態(tài)”的含義是必須等到城市實際運行到這條指令的時候,解析動作才開始執(zhí)行。
加載
“加載”是“類加載”過程中的一個階段,在加載階段,虛擬機需要做以下3件事情:
通過類的全限定名獲得該類的二進制字節(jié)流
將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換成方法區(qū)中的某個運行時數(shù)據(jù)結(jié)構(gòu)
- 在方法區(qū)內(nèi)存(對于HotSpot虛擬機)中生成一個代表該類的java.lang.Class對象,作為訪問方法區(qū)中該類的運行時數(shù)據(jù)結(jié)構(gòu)的外部接口
加載階段中“通過類的全限定名獲得該類的二進制字節(jié)流”這個動作,被放到java虛擬機外部實現(xiàn),目的是大限度的讓應用程序去決定該如何獲取所需的類,而實現(xiàn)該動作的代碼模塊就是類加載器(ClassLoader),JVM提供了3種類加載器:
啟動類加載器(Bootstrap ClassLoader):負責加載 JAVAHOME\lib 目錄中的,或通過-Xbootclasspath參數(shù)指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。?
擴展類加載器(Extension ClassLoader):負責加載 JAVAHOME\lib\ext 目錄中的,或通過java.ext.dirs系統(tǒng)變量指定路徑中的類庫。?
- 應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫。
加載階段完成后,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)中了。
驗證
加載完成后,緊接著(更確切的說是交叉執(zhí)行)虛擬機會對加載的字節(jié)流進行驗證。虛擬機如果不檢查輸入的字節(jié)流,對其安全信任的話,很可能會因為載入了有害的字節(jié)流而導致系統(tǒng)崩潰。驗證階段大致會完成4中不同的檢驗動作:
文件格式驗證?
文件格式驗證主要是校驗該字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當前版本的虛擬機所接受。這個階段包括但不限于以下驗證點:
- 是否以魔數(shù)0xCAFEBABE開頭
- 主、次版本號是否在當前虛擬機處理的范圍之內(nèi)
- 常量池中是否有不支持的常量類型(通過tag校驗)
- 常量的索引是否有指向不存在或不符合類型的常量
- ...
元數(shù)據(jù)驗證?
元數(shù)據(jù)驗證主要是對字節(jié)流中的描述信息(描述符)進行語義分析,以確保其描述的信息符合java語言規(guī)范的要求。這個階段包括但不限于以下驗證點:
- 這個類是否有父類,除了java.lang.Object,所有的類都應該有父類
- 這個類的父類是否繼承了不允許被繼承的類,如被final修飾的類
- 如果這個類不是抽象類,是否實現(xiàn)了父類或接口中要求的所有的方法
- ...
字節(jié)碼驗證?
字節(jié)碼驗證主要是對類的方法體進行分析,確保在方法運行時不會有危害虛擬機的事件發(fā)生。這個階段包括但不限于以下驗證點:
- 操作數(shù)棧的數(shù)據(jù)類型與指令碼中所需類型是否相符
- 校驗跳轉(zhuǎn)指令是否會跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上
- 校驗方法體中類型轉(zhuǎn)換是否是有效的
- ...
符號引用驗證?
符號引用驗證主要是對類自身以外的信息進行匹配新校驗,包括常量池中的各種符號引用。這個階段包括但不限于以下驗證點:
- 符號引用中通過字符串描述的全限定類名是否能找到對應的類
- 在指定的類中是否存在符合方法的字段描述符以及簡單名稱所描述的字段和方法
- ...
準備
準備階段是為類變量(static)在方法區(qū)中分配內(nèi)存并設置初始值(默認值,如int的默認值為0)的階段。實例變量將會在對象實例化時隨著對象一起分配在java堆中。為類變量設置初始值跟該變量是否有final修飾符有關系。 如果沒用final進行修飾,如下列的代碼:
// 準備階段執(zhí)行完成后,value變量的值為int的“零值”,即:0
// 把value賦值為10的putstatic指令是程序被編譯后,
// 存放于類構(gòu)造器()方法中的,所以具體賦值的操作會在初始化階段執(zhí)行
public static int value = 10;
如果使用了final進行修飾,如下列的代碼:
// 如果類字段的字段屬性表中有ConstantValue屬性,
// 則會在準備階段將變量的值初始化為ConstantValue所指定的值
// 準備階段執(zhí)行完成后,VALUE變量的值被賦值為20
public final static int VALUE = 20;
解析
解析階段是虛擬機將常量池中的符號引用替換為直接引用的過程,在該階段將會進行符號引用的校驗。 符號引用是Class文件中用來描述所引用目標的符號,可以是任何形式的字面量。 直接引用是虛擬機在內(nèi)存中引用具體類或接口的,可以是直接指向目標的指針,相對偏移量或者是一個能間接定位到目標的句柄。 簡單的來說,符號引用是Class類文件用來定位目標的,直接引用是虛擬機用來在內(nèi)存中定位目標的。
初始化
初始化階段是執(zhí)行類的構(gòu)造器方法
()方法對于類或接口不是必須的,如果一個類中沒有靜態(tài)代碼塊,也沒有靜態(tài)變量的賦值操作,那么編譯器不會生成 ()方法 ()方法與實例構(gòu)造器方法 ()不同,不需要顯式的調(diào)用父類的方法,虛擬機會保證父類的優(yōu)先執(zhí)行 - 為了防止多次執(zhí)行,虛擬機會確保方法在多線程環(huán)境下被正確的加鎖同步執(zhí)行,如果有多個線程同時初始化一個類,那么只有一個線程能夠執(zhí)行方法,其它線程進行阻塞等待,直到執(zhí)行完成
- 執(zhí)行接口的方法是不需要先執(zhí)行父接口的,只有使用父接口中定義的變量時,才會執(zhí)行。
java虛擬機規(guī)范嚴格規(guī)定了有且只有一下5中情況必須立即對類進行初始化:
- 遇到new,getstatic,putstatic,invokestatic這4條字節(jié)碼指令時,生成這4條指令的最常見的java代碼場景是:使用new實例化對象時,讀取或設置類的靜態(tài)字段時,調(diào)用一個類的靜態(tài)方法時
- 使用java.lang.reflect對類進行反射調(diào)用時,如通過Class.forName()創(chuàng)建對象時
- 當初始化一個類時,如果父類還沒有初始化,則要先觸發(fā)父類的初始化,即先要執(zhí)行父類的構(gòu)造器方法
() - 啟動虛擬機時,需要初始化包含main方法的類
- 在JDK1.7中,如果java.lang.invoke.MethodHandler實例最后的解析結(jié)果REFgetStatic、REFputStatic、REF_invokeStatic的方法句柄,并且這個方法句柄對應的類沒有進行初始化
以下幾種情況,不會觸發(fā)類初始化:
- 通過子類引用父類的靜態(tài)字段,只會觸發(fā)父類的初始化,而不會觸發(fā)子類的初始化。
class Parent { static int a = 100; static { System.out.println("parent init!"); } } class Child extends Parent { static { System.out.println("child init!"); } } public class Init{ public static void main(String[] args){ // 只會執(zhí)行父類的初始化,不會執(zhí)行子類的初始化 // 將打?。簆arent init! System.out.println(Child.a); } }
- 定義對象數(shù)組,不會觸發(fā)該類的初始化。
public class Init{ public static void main(String[] args){ // 不會有任何輸出 Parent[] parents = new Parent[10]; } }
- 常量在編譯期間會存入調(diào)用類的常量池中,本質(zhì)上并沒有直接引用定義常量的類,不會觸發(fā)定義常量所在的類的初始化。
class Const { static final int A = 100; static { System.out.println("Const init"); } } public class Init{ public static void main(String[] args){ // Const.A會存入Init類的常量池中,調(diào)用時并不會觸發(fā)Const類的初始化 // 將打?。?00 System.out.println(Const.A); } }
- 通過類名獲取Class對象,不會觸發(fā)類的初始化。
class Cat { private string name; static { System.out.println("Cat is loaded"); } } public class Init{ public static void main(String[] args){ // 不會打印任何信息 Class catClazz = Class.class; } }
- 通過Class.forName加載指定類時,如果指定參數(shù)initialize為false時,也不會觸發(fā)類初始化,其實這個參數(shù)是告訴虛擬機,是否要對類進行初始化。
class Cat { private string name; static { System.out.println("Cat is loaded"); } } public class Init{ public static void main(String[] args) throws ClassNotFoundException{ // 不會打印任何信息 Class catClazz = Class.forName("com.test.Cat",false,Cat.class.getClassLoader()); } }
- 通過ClassLoader默認的loadClass方法,也不會觸發(fā)初始化動作
class Cat { private string name; static { System.out.println("Cat is loaded"); } } public class Init{ public static void main(String[] args) throws ClassNotFoundException{ // 不會打印任何信息 new ClassLoader(){}.loadClass("com.test.Cat"); } }
最后,附上一幅Class類加載過程的思維導圖:
另外有需要云服務器可以了解下創(chuàng)新互聯(lián)scvps.cn,海內(nèi)外云服務器15元起步,三天無理由+7*72小時售后在線,公司持有idc許可證,提供“云服務器、裸金屬服務器、高防服務器、香港服務器、美國服務器、虛擬主機、免備案服務器”等云主機租用服務以及企業(yè)上云的綜合解決方案,具有“安全穩(wěn)定、簡單易用、服務可用性高、性價比高”等特點與優(yōu)勢,專為企業(yè)上云打造定制,能夠滿足用戶豐富、多元化的應用場景需求。
網(wǎng)站題目:一文讓你理解Class類加載機制-創(chuàng)新互聯(lián)
新聞來源:http://www.dlmjj.cn/article/jcppd.html