官網描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html
虛拟機的類加載機制:虛拟機将class檔案中的二進制資料讀入到記憶體,按先後順序對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型。
類在虛拟機中的生命周期如下圖:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、解除安裝(Unloading)7個階段。
類在虛拟機中的生命周期
類加載過程必須按照順序按部就班開始:加載、驗證、準備、初始化和解除安裝,但是解析階段可以在初始化後開始,這是為了支援java的動态綁定。
類加載過程之【加載】
類加載階段的操作:
- 擷取類的二進制位元組流
- 将位元組流中的靜态存儲結構轉為方法區的的運作時資料結構
- 在記憶體中(HotSpot虛拟機中,Class對象存放在方法區中)生成對應的java.lang.Class對象,作為方法區這個類的各種資料的通路入口
數組類的加載:
數組類本身不通過類加載器建立,而由Java虛拟機直接建立。
- 如果數組中的元素類型是引用類型,則遞歸類加載過程來加載這個元素,數組C将在加載這個元素的類加載器的類名稱空間上被辨別
- 如果數組中的元素不是引用類型(如int[]),數組C标記為與引導類加載器關聯
- 數組類的可見性與其中的元素的可見性一緻,如果其中元素不是可見類型,數組類的可見性預設為public
二進制的位元組流來源:
- 從本地系統中直接加載
- 通過網絡下載下傳class檔案
- 從jar,zip等歸檔的檔案中加載class檔案
- 從資料庫中提取class檔案
- 動态編譯java源檔案為class檔案
類加載器ClassLoader:
ClassLoader隻負責class檔案的加載。
類加載器并不是在等到某個類需要的時候才去加載它,jvm規範是允許類加載器在預料到某個類将要被使用時就預先将其加載,如果預加載時class檔案缺失或存在錯誤,類加載器必須在首次主動使用時報錯LinkageError,如果此類一直沒被程式主動使用,類加載器不會報錯。
類加載器
啟動類加載器(Bootstrap ClassLoader)
加載JAVA_HOMElib目錄中的,及通過-Xbootclasspath 參數指定路徑中的,且被虛拟機認可(按檔案名識别,如rt.jar)的類。
擴充類加載器(Extension ClassLoader)
加載 JAVA_HOMElibext目錄中的,或通過-Djava.ext.dirs系統變量指定路徑中的類庫。
應用程式類加載器(Application ClassLoader)
負責加載使用者路徑(classpath),及-Djava.class.path系統變量指定路徑中的類庫
自定義的類加載器:
通過java.lang.ClassLoader的子類自定義加載class檔案,如tomcat。
如何實作自定義類加載器:
繼承ClassLoader,重寫findClass()方法,在findClass()方法中加載目标類,在loadClass(…)方法中,父加載器加載失敗後會調用findClass()方法來進行加載。
public class ClassLoaderCustomer extends ClassLoader { @Override protected Class> findClass(String name) throws ClassNotFoundException { //加載clazz檔案 Class clazz = this.findLoadedClass(className); if (null == clazz) { try { String classFile = getClassFile(className); FileInputStream fis = new FileInputStream(classFile); FileChannel fileC = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel outC = Channels.newChannel(baos); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); 省略部分代碼 fis.close(); byte[] bytes = baos.toByteArray(); clazz = defineClass(className, bytes, 0, bytes.length); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }} return clazz; }}
以上四種類加載器,除了啟動類加載器是由C++實作的外,剩餘的都會有父類加載器(使用組合實作)。
雙親委派機制
雙親委派模型工作過程:如果一個類加載器收到了類加載的請求,它不會自己嘗試加載這個類,而是先把請求交給父類加載器去完成,每個層次的類加載器都是如此,那麼所有的加載請求最終都會到達頂層的啟動類加載器中,隻有當父類加載器無法完成加載任務時,子類加載器才會嘗試自己加載。
雙親委派模型的優勢:
Java類随着它的類加載器一起具備的帶有優先級的層次關系。如rt.ar中的java.lang.String類,最終都是由啟動類加載器加載,在程式中無論使用何種類加載器環境都是使用的一個類。如果沒有雙親委派,系統中會出現多個String類,程式将變得混亂。
破壞雙親委派模型:
- Tomcat的WebappClassLoader 就會先加載自己的Class,找不到再委托parent
- OSGi的ClassLoader形成網狀結構,根據需要自由加載Class
- JNDI服務中,為了對自己進行集中管理和查找,需要調用由獨立廠商實作并部署在程式的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)代碼。
如果啟動類加載器加載不了這些代碼,使用線程上下文類加載器(Thread Context ClassLoader),通過Thread類的方法setContextClassLoader(ClassLoader)進行設定,如果建立線程時未設定則從父線程中的ClassLoader,如果程式全局範圍沒有設定過,則設定未系統類加載器。
JNDI服務使用線程上下文類加載器加載所需的SPI代碼,即父類加載器請求子類加載器來完成類加載的動作。
Java中的SPI加載如JDBC,如下代碼可解釋如何通過設定Thread.setContextClassLoader(ClassLoader)破壞雙親委派。
//使用JDBC建立資料庫連接配接Class.forName(driver);//加載驅動Connection conn = DriverManager.getConnection(url,user,password);//建立連接配接
調用類DriverManager的靜态方法getConnection (…)之前會先初始化靜态代碼塊,方法loadInitialDrivers()中調用ServiceLoader.load(Driver.class)。
類Driver在jre/lib/rt.jar中,由啟動類加載器加載,但是Driver的實作類在不同的廠商提供的jar包中,需要由子類加載器來加載Driver的實作類。調用調用ServiceLoader.load(Driver.class)會加載檔案META-INF/services/java.sql.Driver中的類到jvm,完成Driver的實作類的自動加載。
public class DriverManager { static { loadInitialDrivers(); println("JDBC DriverManager initialized");} private static void loadInitialDrivers() { //代碼省略 AccessController.doPrivileged(new PrivilegedAction() { public Void run() { ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); //其他代碼省略 } }); //其他代碼省略}}
類加載過程之【連結】
1. 驗證(Verification)
目的:確定加載類的正确性,并且不會危害虛拟機自身的安全
驗證階段包含四個檢驗:
- 檔案格式驗證:確定位元組流符合class檔案格式的規範,并且能被目前版本的虛拟機處理
- 中繼資料驗證:確定位元組碼的描述資訊符合Java語言規範的要求
- 位元組碼驗證:通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。保證位元組碼流可以被java虛拟機安全地執行。
- 符号引用驗證:對類自身以外的資訊進行比對性校驗,確定解析動作能正常執行,否則報錯如java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。此驗證階段發生在解析階段(符号引用轉化為直接引用時進行驗證)。
2. 準備(Preparation)
為類變量配置設定記憶體(方法區)并設定預設的初始值(基本資料類型的預設值參考官網:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.3)。
類變量的預設的初始值設定的兩種情況:
-
例如:public static int value = 10;
static修飾的變量value在準備階段過後的值為0,此時未執行任何java方法,将10指派給value是指令putstatic 被程式編譯後,存放在類構造器()方法中,就是說在初始化階段将value指派為10。
-
例如:public static final int value = 10;
如果類字段的字段屬性表中存在ConstantValue屬性,那麼在準備階段變量的預設值設定為ConstantValue屬性所指的值。
final修飾的value屬性,在編譯時會生成ConstantValue屬性,準備階段會将ConstantValue的值指派給value。
3. 解析(Resolution)
将常量池内的符号引用替換為直接引用,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符這7類符号引用。
符号引用:用一組符号來描述所引用的目标,符号引用以字面量形式明确定義在Java虛拟機規範的class檔案格式中。在類檔案結構中已被提及,存于常量池中。
直接引用:指向目标的指針、相對偏移量後一個能間接定位到目标的檔案句柄。
類加載過程之【初始化】
Java虛拟機執行類的初始化語句,為類的靜态變量賦予初始值。
在準備階段,系統已為類變量賦予初始值,而在初始化階段将根據程式代碼為類變量指派。
初始化階段是執行類構造器()方法的過程。
類的靜态變量初始化有兩種方式:
- 在靜态變量的聲明出進行初始化
- 在靜态代碼塊中程序初始化
Java虛拟機會根據初始化語句在類檔案中的先後順序依次執行。
類的初始化步驟:
- 假如這個類還沒有被加載和連接配接,就先進行加載和連接配接;
- 假如這個類存在直接的父類且沒有被初始化,則先初始化直接的父類;
- 假如類中存在初始化語句,則按順序依次執行;
Java程式中對類的使用有兩種:
主動使用和被動使用。所有的類或接口在被程式 “首次主動使用”時才對它們進行初始化。
主動使用場景如下:
- 執行個體化對象、讀取或設定類或接口的靜态變量(被final修飾、已在編譯器把結果放入常量池的靜态字段除外)、調用類的靜态方法的時候,如果類沒有初始化過,需先進行初始化;
- 使用類的反射調用,如果類沒有初始化過,要先進行初始化
- 當初始化一個子類的時候,發現父類還沒初始化過,要先對父類進行初始化
- 虛拟機啟動時,先初始化被辨別為啟動類的的類(有main()方法的類)
- 在使用jdk1.7的動态語言支援時,如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且方法句柄對應的類沒有初始化過,需要先進行初始化
除了上述5中情況外的使用java類都是對類的被動使用,不會進行類的初始化。
java虛拟機生命周期結束的幾種情況:
- 執行了方法System.exit()
- 程式正常結束
- 程式運作過程中遇到異常或錯誤而導緻的
- 作業系統出現問題而導緻java虛拟機終止