說(shuō)說(shuō) JVM 的類(lèi)加載機(jī)制『非專(zhuān)業(yè)』
類(lèi)加載機(jī)制
類(lèi)是在運(yùn)行期間第一次使用時(shí)動(dòng)態(tài)加載的,而不是一次性加載所有類(lèi)。因?yàn)槿绻淮涡约虞d,那么會(huì)占用很多的內(nèi)存。
類(lèi)的生命周期
包括以下 7 個(gè)階段:
- 「加載(Loading)」
- 「驗(yàn)證(Verification)」
- 「準(zhǔn)備(Preparation)」
- 「解析(Resolution)」
- 「初始化(Initialization)」
- 使用(Using)
- 卸載(Unloading)
類(lèi)加載過(guò)程 --- new 一個(gè)對(duì)象的過(guò)程
包含加載、驗(yàn)證、準(zhǔn)備、解析和初始化這 5 個(gè)階段。
1.加載
加載過(guò)程完成以下三件事:
其中二進(jìn)制字節(jié)流可以從以下方式中獲?。?/p>
- 從 ZIP 包讀取,成為 JAR、EAR、WAR 格式的基礎(chǔ)。
- 從網(wǎng)絡(luò)中獲取,最典型的應(yīng)用是 Applet。
- 運(yùn)行時(shí)計(jì)算生成,例如動(dòng)態(tài)代理技術(shù),在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理類(lèi)的二進(jìn)制字節(jié)流。
- 由其他文件生成,例如由 JSP 文件生成對(duì)應(yīng)的 Class 類(lèi)。
- 通過(guò)類(lèi)的完全限定名稱(chēng)獲取定義該類(lèi)的二進(jìn)制字節(jié)流。
- 將該字節(jié)流表示的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)存儲(chǔ)結(jié)構(gòu)。
- 在內(nèi)存中生成一個(gè)代表該類(lèi)的 Class 對(duì)象,作為方法區(qū)中該類(lèi)各種數(shù)據(jù)的訪(fǎng)問(wèn)入口。
2.驗(yàn)證
格式驗(yàn)證:驗(yàn)證是否符合class文件規(guī)范 語(yǔ)義驗(yàn)證:檢查一個(gè)被標(biāo)記為final的類(lèi)型是否包含子類(lèi);檢查一個(gè)類(lèi)中的final方法是否被子類(lèi)進(jìn)行重寫(xiě);確保父類(lèi)和子類(lèi)之間沒(méi)有不兼容的一些方法聲明(比如方法簽名相同,但方法的返回值不同) 操作驗(yàn)證:在操作數(shù)棧中的數(shù)據(jù)必須進(jìn)行正確的操作,對(duì)常量池中的各種符號(hào)引用執(zhí)行驗(yàn)證(通常在解析階段執(zhí)行,檢查是否可以通過(guò)符號(hào)引用中描述的全限定名定位到指定類(lèi)型上,以及類(lèi)成員信息的訪(fǎng)問(wèn)修飾符是否允許訪(fǎng)問(wèn)等)
3.準(zhǔn)備
類(lèi)變量是被 static 修飾的變量,準(zhǔn)備階段為類(lèi)變量分配內(nèi)存并設(shè)置初始值,使用的是方法區(qū)的內(nèi)存。
- public static int value = 123;
如果類(lèi)變量是常量,那么它將初始化為表達(dá)式所定義的值而不是 0。例如下面的常量 value 被初始化為 123 而不是 0。
- public static final int value = 123;
實(shí)例變量不會(huì)在這階段分配內(nèi)存,它會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起被分配在堆中。
4.解析
將常量池中的符號(hào)引用轉(zhuǎn)為直接引用(得到類(lèi)或者字段、方法在內(nèi)存中的指針或者偏移量,以便直接調(diào)用該方法),這個(gè)可以在初始化之后再執(zhí)行,可以支持 Java 的動(dòng)態(tài)綁定。
以上2、3、4三個(gè)階段又合稱(chēng)為鏈接階段,鏈接階段要做的是將加載到JVM中的二進(jìn)制字節(jié)流的類(lèi)數(shù)據(jù)信息合并到JVM的運(yùn)行時(shí)狀態(tài)中。
5.初始化
初始化階段是虛擬機(jī)執(zhí)行類(lèi)構(gòu)造器
虛擬機(jī)會(huì)保證一個(gè)類(lèi)的
上述步驟簡(jiǎn)單來(lái)說(shuō)就是分為以下兩步:
- 類(lèi)變量的賦值操作
- 執(zhí)行static代碼塊。static代碼塊只有jvm能夠調(diào)用。如果是多線(xiàn)程需要同時(shí)初始化一個(gè)類(lèi),僅僅只能允許其中一個(gè)線(xiàn)程對(duì)其執(zhí)行初始化操作,其余線(xiàn)程必須等待,只有在活動(dòng)線(xiàn)程執(zhí)行完對(duì)類(lèi)的初始化操作之后,才會(huì)通知正在等待的其他線(xiàn)程。
最終,方法區(qū)會(huì)存儲(chǔ)當(dāng)前類(lèi)的類(lèi)信息,包括類(lèi)的靜態(tài)變量、類(lèi)初始化代碼(定義靜態(tài)變量時(shí)的賦值語(yǔ)句和靜態(tài)初始化代碼塊)、實(shí)例變量定義、實(shí)例初始化代碼(定義實(shí)例變量時(shí)的賦值語(yǔ)句實(shí)例代碼塊和構(gòu)造方法)和實(shí)例方法,還有父類(lèi)的類(lèi)信息引用。
創(chuàng)建對(duì)象
假設(shè)是第一次使用一個(gè)類(lèi)的話(huà),那么需要經(jīng)過(guò)上述的類(lèi)加載的過(guò)程,之后才是創(chuàng)建對(duì)象。
「1、在堆區(qū)分配對(duì)象需要的內(nèi)存」
分配的內(nèi)存包括本類(lèi)和父類(lèi)的所有實(shí)例變量,但不包括任何靜態(tài)變量
「2、對(duì)所有實(shí)例變量賦默認(rèn)值」
將方法區(qū)內(nèi)對(duì)實(shí)例變量的定義拷貝一份到堆區(qū),然后賦默認(rèn)值
「3、執(zhí)行實(shí)例初始化代碼」
初始化順序是先初始化父類(lèi)再初始化子類(lèi),初始化時(shí)先執(zhí)行實(shí)例代碼塊然后是構(gòu)造方法。(第一執(zhí)行類(lèi)中的靜態(tài)代碼,包括靜態(tài)成 員變量的初始化和靜態(tài)語(yǔ)句塊的執(zhí)行;第二執(zhí)行類(lèi)中的非靜態(tài)代碼,包括非靜態(tài)成員變量的初始化和非靜態(tài)語(yǔ)句塊的執(zhí)行,最后執(zhí) 行構(gòu)造函數(shù)。在繼承的情況下,會(huì)首先執(zhí)行父類(lèi)的靜態(tài)代碼,然后執(zhí)行子類(lèi)的靜態(tài)代碼;之后執(zhí)行父類(lèi)的非靜態(tài)代碼和構(gòu)造函數(shù); 最后執(zhí)行子類(lèi)的非靜態(tài)代碼和構(gòu)造函數(shù))
「4、如果有類(lèi)似于Child c = new Child()形式的c引用的話(huà),在棧區(qū)定義Child類(lèi)型引用變量c,然后將堆區(qū)對(duì)象的地址賦值給它」
需要注意的是,「每個(gè)子類(lèi)對(duì)象持有父類(lèi)對(duì)象的引用」,可在內(nèi)部通過(guò)super關(guān)鍵字來(lái)調(diào)用父類(lèi)對(duì)象,但在外部不可訪(fǎng)問(wèn)。
存在繼承的情況下,初始化順序?yàn)椋?/p>
- 父類(lèi)(靜態(tài)變量、靜態(tài)語(yǔ)句塊)
- 子類(lèi)(靜態(tài)變量、靜態(tài)語(yǔ)句塊)
- 父類(lèi)(實(shí)例變量、普通語(yǔ)句塊)
- 父類(lèi)(構(gòu)造函數(shù))
- 子類(lèi)(實(shí)例變量、普通語(yǔ)句塊)
- 子類(lèi)(構(gòu)造函數(shù))
類(lèi)初始化的情況
主動(dòng)引用
虛擬機(jī)規(guī)范中并沒(méi)有強(qiáng)制約束何時(shí)進(jìn)行加載,但是規(guī)范嚴(yán)格規(guī)定了有且只有下列五種情況必須對(duì)類(lèi)進(jìn)行初始化(加載、驗(yàn)證、準(zhǔn)備都會(huì)隨之發(fā)生):
- 遇到 new、getstatic、putstatic、invokestatic 這四條字節(jié)碼指令時(shí),如果類(lèi)沒(méi)有進(jìn)行過(guò)初始化,則必須先觸發(fā)其初始化。最常見(jiàn)的生成這 4 條指令的場(chǎng)景是:使用 new 關(guān)鍵字實(shí)例化對(duì)象的時(shí)候;讀取或設(shè)置一個(gè)類(lèi)的靜態(tài)字段(被 final 修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時(shí)候;以及調(diào)用一個(gè)類(lèi)的靜態(tài)方法的時(shí)候。
- 使用 java.lang.reflect 包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用的時(shí)候,如果類(lèi)沒(méi)有進(jìn)行初始化,則需要先觸發(fā)其初始化。
- 當(dāng)初始化一個(gè)類(lèi)的時(shí)候,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)其父類(lèi)的初始化。
- 當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)(包含 main() 方法的那個(gè)類(lèi)),虛擬機(jī)會(huì)先初始化這個(gè)主類(lèi)。
被動(dòng)引用
以上的行為稱(chēng)為對(duì)一個(gè)類(lèi)進(jìn)行主動(dòng)引用。除此之外,所有引用類(lèi)的方式都不會(huì)觸發(fā)初始化,稱(chēng)為被動(dòng)引用。被動(dòng)引用的常見(jiàn)例子包括:
- 通過(guò)子類(lèi)引用父類(lèi)的靜態(tài)字段,不會(huì)導(dǎo)致子類(lèi)初始化。
- System.out.println(SubClass.value); // value 字段在 SuperClass 中定義
通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)此類(lèi)的初始化。該過(guò)程會(huì)對(duì)數(shù)組類(lèi)進(jìn)行初始化,數(shù)組類(lèi)是一個(gè)由虛擬機(jī)自動(dòng)生成的、直接繼承自 Object 的子類(lèi),其中包含了數(shù)組的屬性和方法。
- SuperClass[] sca = new SuperClass[10];
- 常量在編譯階段會(huì)存入調(diào)用類(lèi)的常量池中,本質(zhì)上并沒(méi)有直接引用到定義常量的類(lèi),因此不會(huì)觸發(fā)定義常量的類(lèi)的初始化。
- System.out.println(ConstClass.HELLOWORLD);
類(lèi)與類(lèi)加載器
兩個(gè)類(lèi)相等,需要類(lèi)本身相等,并且使用同一個(gè)類(lèi)加載器進(jìn)行加載。這是因?yàn)槊恳粋€(gè)類(lèi)加載器都擁有一個(gè)獨(dú)立的類(lèi)名稱(chēng)空間。那么最終的相等包括了類(lèi)的 Class 對(duì)象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結(jié)果為 true,也包括使用 instanceof 關(guān)鍵字做對(duì)象所屬關(guān)系判定結(jié)果為 true。
從 Java 虛擬機(jī)的角度來(lái)講,只存在以下兩種不同的類(lèi)加載器:
- 啟動(dòng)類(lèi)加載器(Bootstrap ClassLoader),使用 C++ 實(shí)現(xiàn),是虛擬機(jī)自身的一部分;
- 所有其它類(lèi)的加載器,使用 Java 實(shí)現(xiàn),獨(dú)立于虛擬機(jī),繼承自抽象類(lèi) java.lang.ClassLoader。
那么上述又可以分為以下三種類(lèi)加載器:BootstrapClassLoader、ExtensionClassLoader、App ClassLoader
- 啟動(dòng)類(lèi)加載器(BootstrapClassLoader)是嵌在JVM內(nèi)核中的加載器,該加載器是用C++語(yǔ)言寫(xiě)的,主要負(fù)載加載JAVA_HOME/lib下的類(lèi)庫(kù),或者被 -Xbootclasspath 參數(shù)所指定的路徑中的,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別,如 rt.jar,名字不符合的類(lèi)庫(kù)即使放在 lib 目錄中也不會(huì)被加載)。
啟動(dòng)類(lèi)加載器無(wú)法被 Java 程序直接引用,用戶(hù)在編寫(xiě)自定義類(lèi)加載器時(shí),如果需要把加載請(qǐng)求委派給啟動(dòng)類(lèi)加載器,直接使用 null 代替即可。
- 擴(kuò)展類(lèi)加載器(ExtensionClassLoader)是用JAVA編寫(xiě),由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實(shí)現(xiàn)的。它負(fù)責(zé)將
/lib/ext 或者被 java.ext.dir 系統(tǒng)變量所指定路徑中的所有類(lèi)庫(kù)加載到內(nèi)存中,開(kāi)發(fā)者可以直接使用擴(kuò)展類(lèi)加載器。
它的父類(lèi)加載器是Bootstrap。
- 應(yīng)用程序類(lèi)加載器(Application ClassLoader)這個(gè)類(lèi)加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的。一般負(fù)責(zé)加載應(yīng)用程序classpath目錄下的所有jar和class文件。
開(kāi)發(fā)者可以直接使用這個(gè)類(lèi)加載器,如果應(yīng)用程序中沒(méi)有自定義過(guò)自己的類(lèi)加載器,一般情況下這個(gè)就是程序中默認(rèn)的類(lèi)加載器。
它的父加載器為ExteClassLoader。
雙親委派模型
應(yīng)用程序是由三種類(lèi)加載器互相配合從而實(shí)現(xiàn)類(lèi)加載,除此之外還可以加入自己定義的類(lèi)加載器。下圖展示了類(lèi)加載器之間的層次關(guān)系,稱(chēng)為雙親委派模型(Parents Delegation Model)。這里的父子關(guān)系一般通過(guò)委托來(lái)實(shí)現(xiàn),而不是繼承關(guān)系(Inheritance)。
- 工作流程
如果一個(gè)類(lèi)加載器收到了一個(gè)類(lèi)加載請(qǐng)求,它不會(huì)自己去嘗試加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求轉(zhuǎn)交給父類(lèi)加載器去完成。每一個(gè)層次的類(lèi)加載器都是如此。因此所有的類(lèi)加載請(qǐng)求都應(yīng)該傳遞到最頂層的啟動(dòng)類(lèi)加載器中,只有到父類(lèi)加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(在它的搜索范圍沒(méi)有找到這個(gè)類(lèi))時(shí),子類(lèi)加載器才會(huì)嘗試自己去加載。
- 好處
使得 Java 類(lèi)隨著它的類(lèi)加載器一起具有一種帶有優(yōu)先級(jí)的層次關(guān)系,從而使得基礎(chǔ)類(lèi)得到統(tǒng)一。
例如 java.lang.Object 存放在 rt.jar 中,如果編寫(xiě)另外一個(gè) java.lang.Object 并放到 ClassPath 中,程序可以編譯通過(guò)。由于雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優(yōu)先級(jí)更高,這是因?yàn)? rt.jar 中的 Object 使用的是啟動(dòng)類(lèi)加載器,而 ClassPath 中的 Object 使用的是應(yīng)用程序類(lèi)加載器。rt.jar 中的 Object 優(yōu)先級(jí)更高,那么程序中所有的 Object 都是這個(gè) Object。
- demo
以下是抽象類(lèi) java.lang.ClassLoader 的代碼片段,其中的 loadClass() 方法運(yùn)行過(guò)程如下:先檢查類(lèi)是否已經(jīng)加載過(guò),如果沒(méi)有則讓父類(lèi)加載器去加載。當(dāng)父類(lèi)加載器加載失敗時(shí)拋出 ClassNotFoundException,此時(shí)嘗試自己去加載。
- public abstract class ClassLoader {
- // The parent class loader for delegation
- private final ClassLoader parent;
- public Class<?> loadClass(String name) throws ClassNotFoundException {
- return loadClass(name, false);
- }
- protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
- synchronized (getClassLoadingLock(name)) {
- // First, check if the class has already been loaded
- Class<?> c = findLoadedClass(name);
- if (c == null) {
- try {
- if (parent != null) {
- c = parent.loadClass(name, false);
- } else {
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- c = findClass(name);
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- throw new ClassNotFoundException(name);
- }
- }
- 自定義類(lèi)加載器實(shí)現(xiàn)
以下代碼中的 FileSystemClassLoader 是自定義類(lèi)加載器,繼承自 java.lang.ClassLoader,用于加載文件系統(tǒng)上的類(lèi)。它首先根據(jù)類(lèi)的全名在文件系統(tǒng)上查找類(lèi)的字節(jié)代碼文件(.class 文件),然后讀取該文件內(nèi)容,最后通過(guò) defineClass() 方法來(lái)把這些字節(jié)代碼轉(zhuǎn)換成 java.lang.Class 類(lèi)的實(shí)例。
java.lang.ClassLoader 的 loadClass() 實(shí)現(xiàn)了雙親委派模型的邏輯,自定義類(lèi)加載器一般不去重寫(xiě)它,但是需要重寫(xiě) findClass() 方法。
- public class FileSystemClassLoader extends ClassLoader {
- private String rootDir;
- public FileSystemClassLoader(String rootDir) {
- this.rootDir = rootDir;
- }
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- byte[] classData = getClassData(name);
- if (classData == null) {
- throw new ClassNotFoundException();
- } else {
- return defineClass(name, classData, 0, classData.length);
- }
- }
- private byte[] getClassData(String className) {
- String path = classNameToPath(className);
- try {
- InputStream ins = new FileInputStream(path);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- int bufferSize = 4096;
- byte[] buffer = new byte[bufferSize];
- int bytesNumRead;
- while ((bytesNumRead = ins.read(buffer)) != -1) {
- baos.write(buffer, 0, bytesNumRead);
- }
- return baos.toByteArray();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return null;
- }
- private String classNameToPath(String className) {
- return rootDir + File.separatorChar
- + className.replace('.', File.separatorChar) + ".class";
- }
- }
巨人的肩膀
程序鍋春招筆記的摘記
https://github.com/CyC2018/CS-Notes
本文轉(zhuǎn)載自微信公眾號(hào)「多選參數(shù)」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系多選參數(shù)公眾號(hào)。