一個 java 文件的執行過程詳解


平時我們都使用 idea、eclipse 等軟件來編寫代碼,在編寫完之后直接點擊運行就可以啟動程序了,那么這個過程是怎么樣的?

總體過程

我們編寫的 java 文件在由編譯器編譯后會生成對應的 class 字節碼文件, 然后再將 class 字節碼文件轉給 JVM 。JVM 會處理解析 class 文件,將其內部設置的類、方法、常量等信息全部提取出來,然后找到 main 方法開始一步一步編譯成機器碼並執行,中間會根據需要調用前面提取的數據。

 

那為什么不讓 JVM 直接編譯 java 文件呢?這樣效率不是更高么? 

首先要知道 java 之所以強大,原因之一就是 JVM 的強大。

強大之一是 JVM 是 " 跨平台 " 的。無論在哪種操作系統上執行,都可以轉成對應的機器語言,不需要擔心適配問題。

第二點就是 JVM 是 " 跨語言 " 的,因為 JVM 只認 class 文件,所以其他語言只需要一個編譯器編譯成 class 文件就可以使用 JVM 來編譯執行了。

 

組件分析

根據上面的說明可以知道 java 程序執行的核心是通過 JVM 來實現的,那么就需要知道 JVM 內部是如何執行的。

JVM 內部可以分為四大部分,運行時數據區域、類加載系統、執行引擎、本地接口和本地方法庫。

類加載系統:主要就是指類加載器,用於把 class 數據文件加載到運行時數據區域,然后由數據區域來編譯執行。

運行數據區域:搭配執行引擎來編譯傳來的文件中的代碼,然后執行,並且根據需要通過本地方法接口調用本地方法。

執行引擎:主要用於代碼的編譯和 運行時對象的回收。

本地庫接口和本地方法庫:提供一些 java 無法實現,需要底層執行調用的方法,是 jvm 訪問底層的重要途徑。

 

類加載器

用於進行類的加載。

種類

一般分為啟動類加載器、擴展類加載器、應用程序類加載器、自定義類加載器。圖中的從自定義類加載器到啟動類加載器一層一層使用箭頭連接,這種箭頭並不是繼承關系,而是上下級關系。上下級的聯系是通過 ClassLoader 抽象類繼承過來的 parent 屬性設置的。

1、啟動類加載器(Bootstrap ClassLoader)(引導類加載器),加載java核心類庫(<JAVA_HOME>/jre/lib/rt.jar),無法被java程序直接引用,是用C++編寫的,用來加載其他的類加載器(類加載器本質就是類),是所有加載器的父類。

2、拓展類加載器(Extension ClassLoader),用來加載java的拓展庫(<JAVA_HOME>/jre/lib/ext)。

3、系統類加載器(System ClassLoader)(應用程序類加載器),用來加載類路徑下的Java類

4、用戶自定義類加載器,繼承java.lang.ClassLoader類的方式實現。

 

官方文檔中將類加載器分為引導類加載器和自定義類加載器,這是因為引導類加載器是使用其他語言實現的,而拓展類、系統類、自定義類加載器全部都是通過繼承 ClassLoader 抽象類實現的,所以都統一被划分為自定義類加載器。

 

裝載方式

1、隱式裝載:由加載器加載。

2、顯式裝載:自定義加載,比如使用反射Class.forName(類路徑),類加載器ClassLoader.getSystemClassLoader().loadClass("test.A");使用當前進程上下文的使用的類裝載Thread.currentThread().getContextClassLoader().loadClass("test.A")。

類加載是動態的,它不會一次性加載所有類然后運行,而是保證程序運行的基礎類(核心類庫一部分的類)完全加載到JVM中就運行,這是為了節省內存開銷。

 

類加載器的特性

主要包括全盤負責、雙親委托機制、緩存機制、可見性。

1、全盤負責:當一個 Class 類被某個類加載器所加載時,該 Class 所依賴引用的所有 Class 都會由這個加載器負責載入,除非顯式的使用另一個 ClassLoader。(當然只是這個加載器負責,並不一定就是由這個加載器加載,這是由於雙親委托機制的作用

2、緩存機制:當一個 Class 類加載完畢后,會放入緩存,在其他類需要引用這個類時就會從緩存中直接使用,這也是為什么我們在修改了文件后需要重啟服務器才能使修改生效。

3、雙親委托機制:當一個類加載器收到了類加載的請求時,它首先會將這個請求委派給父類,父類不能執行再自己嘗試執行,父類如果存在父類,也會委派給父類,這樣傳到了啟動類加載器加載,當啟動類加載器不能讀取到類時才會傳給子類加載器,然后子類加載器再嘗試加載。

好處:1、防止自定義的類篡改核心類庫中的代碼。自定義的和類路徑.類名與核心類庫一樣的類會委托給啟動類加載,啟動類加載器會根據包名.類名在內存查看是否已經加載,那么面對自定義的類啟動類加載器會認為已經加載過了。如果是給系統類加載器或者自定義類加載器加載的話可能就會產生多個類名相同的類,那么其他類在調用對應基類的話就會報錯。

   2、防止同一個類被重復加載。

4、可見性:子類加載器可以訪問父類加載器加載的類型,但是反過來是不允許的。不然,因為缺少必要的隔離,我們就沒辦法利用類加載器去實現容器的邏輯。

 

解釋器和即時編譯器(JIT)

主要用於將 Class 數據文件編譯成對應的本地機器碼執行。

解釋器

傳統的編譯工具,主要分為 字節碼解釋器 和 模版解釋器。

字節碼解釋器 是在執行時通過純軟件代碼模擬字節碼的執行,效率低下;模版解釋器則是主流使用的解釋器,原理是將每一條字節碼 和一個模版函數關聯,在 Class 字節碼轉成機器碼的過程中會通過對應的模版函數生成對應的機器碼,這樣短期來看效率還不錯,但是一旦 同一個的字節碼被多次執行,那么每次都需要通過模版函數生成機器碼,效率十分低下。

即時編譯器(JIT)

JIT 的原理是將字節碼關聯的 模版數據直接轉成機器碼,然后將機器碼緩存起來,后面如果再次執行這個字節碼時就直接返回緩存中的機器碼,省去了二次執行的時間,缺點是第一次的轉換消耗比較長,所以以單次執行來看,JIT 的效率是不如 解釋器的,但是一旦執行的字節碼重復數多,JIT 的作用就體現出來了。HotSpot 中有兩個 JIT 編譯器,分別是Client Compiler和Server Compiler,但大多數情況下我們簡稱為C1編譯器和C2編譯器。C1進行簡單的優化,耗時短。C2進行耗時長的優化,代碼執行效率更高。實際中C1和C2共同協作執行的。

實際過程

當虛擬機啟動時,解釋器可以首先發揮作用,而不必等待即時編譯器全部編譯完成再執行,這樣可以省去許多不必要的編譯時間。並且伴隨着程序運行時間的推移,即時編譯器逐漸發揮作用,根據熱點探測功能,將有價值的字節碼編譯為有價值的本地機器指令,以換取更高的程序執行效率。

熱點代碼探測的方式

1、規定熱點閥值。

每次方法調用時該方法的調用次數都會+1,當調用次數達到閥值,就會觸發JIT編譯。熱點閥值可通過 -XX:CompileThreshold= 來設定。

如果不做任何設置,方法調用計數器統計的並不是方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間之內方法被調用的次數。當超過一定的時間限度,如果方法的調用次數仍不足以讓它提交給JIT編譯,那這個方法的調用計數器就會減少一半,這個過程稱為方法調用計數器熱度的衰減,而這段時間就稱為此方法統計的半衰周期

可以使用-XX:-UseCounterDecay 來關閉熱度衰減,也可以使用-XX:CounterHalfLifeTime設置半衰周期的時間。

2、回邊計數器。

統計一個方法中循環體代碼執行的次數。在字節碼中遇到控制流向后跳轉的指令稱為 “回邊”。顯然,建立回邊計數器統計的目的是為了觸發OSR編譯(JIT編譯)。

 

運行時數據區域

程序計數器

線程私有,是當前線程執行的字節碼指示器,指示字節碼的執行順序。程序計數器的內存是單獨的,不會受到其他變量、對象的影響。所以它不會發生內存溢出。也是JVM唯一 一個沒有規定任何 OOM 的區域。也不存在GC

 

Java虛擬機棧

線程私有。先進后出,是代碼執行的核心位置,一個方法在執行前會生成這個方法對應的棧幀,棧幀包括局部變量表(保存局部變量)、操作數棧(進行局部變量的操作)、動態鏈接(其他對象、方法的引用)、方法返回值以及一些附加信息。然后進行壓棧操作,開始方法的執行,如果此方法中調用了其他方法,那么會將調用的這個方法對應的棧幀壓入棧,等到這個方法執行完之后,如果方法包含返回值,將這個返回值返回給上一個方法,然后這個被調用的棧幀出棧,隨后繼續執行上一個棧幀。

局部變量表

基本存儲單元是 slot(變量槽),用於存儲各種類型的數據,其中 long 和 double 會占用兩個 slot,其他基本數據類型以及對象引用變量占用一個 slot。

這也說明了為什么類方法不能使用 this 而實例方法可以(實例方法會直接在索引為0的位置創建一個 this 參數保存,所以在實例方法中使用 this 就是直接使用這個參數的)

同時局部變量表的槽位是可以重用的,當前一個局部變量失效后,下一個變量使用空出來的位置。

 上面這個方法是實例方法,包含this,應該有四個index 槽位,但是因為b是在括號里作用的,出了括號就失效了,所以它的位置(index=3的位置)被新設置的c所占用。

操作數棧

先進后出結構,是當前方法執行的位置,在方法執行時,會根據編譯生成的字節碼按順序將要操作的數據從局部變量表中進入入棧,棧中的數據只能從棧頂向下操作,不能跨數據。比如代碼 x=x+1,在執行時會將 x 先壓入棧,然后將 1 壓入棧,然后讀取到 + 的指令,將棧頂的兩個數相加,再將加的結果存入局部變量表 x 的位置。如果調用了其他方法並獲取了返回值,那么在調用方法執行完畢后,該方法的返回值會被壓入棧頂,然后再進行后續的操作。棧上不會有對象的創建,所以是不存在垃圾回收的,但是其可能會發生OOM。

棧頂緩存技術

目前只是一種想法,還未實現。因為使用的是棧式架構,所以指令多,又由於操作數是存儲在內存中的,所以頻繁地讀寫必然會影響執行速度,所以提出將棧頂元素全部緩存在物理CPU的寄存器中,以此降低對內存的讀寫次數,提升執行引擎的執行效率。

虛方法表

因為重寫方法都是虛方法這些方法在編譯時期都需要往上尋找直到找到所執行對象的實際類型,然后進行權限驗證。這個尋找的過程是比較耗時的,所以每個類會在方法區創建一個虛方法表來保存這些虛方法的實際入口。

方法返回值

1、正常返回:(boolean、byte、char、short、int)ireturn  lreturn、freturn、dreturn、areturn(String);return(無返回值)。

2、異常返回:如果發生異常的方法沒有捕獲異常而是拋給上一級,那么該異常就會被返回給調用該方法的方法去處理。

 

Java 堆

線程共享。是 Java 虛擬機內存最大的一塊,主要用於存儲創建的對象。根據對象的壽命、大小等因素將對象存儲區域划分分為新生代、老年代。在 1.7 開始引入了字符串常量池。因為對象的創建銷毀是非常頻繁的,所以堆是 JVM 中的核心位置之一,也是 OOM 發生的主要位置之一。

 

本地方法棧與native(本地)方法

本地方法棧(也就是最上面圖中的本地接口)是 JVM 與底層交互的接口,用於調用 native 方法。作用與 Java 虛擬棧差不多,只不過是為 native 方法服務的,是由非 Java 語言編寫的。

 

方法區

和堆一樣是線程共享,用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即使編譯器編譯后的代碼緩存等。方法區的實現在 1.8 之前是永久代,使用的是 JVM 的內存,在1.8開始實現變成元空間,使用的是本地內存。之所以這樣改變,是因為原來的方法區很容易發生 OOM,因為方法區的類信息被回收的條件非常苛刻,必須滿足以下三點:

1、該類的所有對象都被回收;2、加載該類的類加載器被回收;3、該類對應的 Class 對象沒有在任何地方被引用(無法在任何地方通過反射訪問該類的方法)。

關於第三點的 Class 對象,在一個類被加載時,會在堆中創建一個用於用於訪問這個類的類信息 Class 對象。而在成為元空間后,使用的是本地內存,所以方法區發生 OOM 的情況會極大改善。

 

運行時常量池

當 Class 文件被類加載器加載到 JVM 中時,存儲的位置就是在方法區,而在 Class 文件信息中包括着 class 文件的常量池,當 JVM 開始執行時,就會將文件常量池中的數據加載到 方法區內部的運行時常量池,變成運行時狀態,並將符號引用轉成直接引用。

符號引用和直接引用:當在調用中調用某個類的類方法、類屬性、接口方法、接口屬性時,因為在執行前,對應的類、接口都還在 Class 文件常量池中,沒有加載到內存中,所以不能確定這些類、接口加載后的具體位置,這時就需要一種方式來確認位置,通常使用類的全名+屬性名/方法名 來唯一標識要調用的方法/屬性,這種標識就是符號引用,等到對應的類加載到內存后,再將這些唯一標識改成在內存中的位置,這種就是直接引用。

 

字符串常量池

在 JDK 1.7 開始,字符串常量池就由方法區移入了堆中,字符串常量池是專門存放字符串常量的,至於為什么移入堆中,這是因為字符串的創建和對象一樣頻繁,銷毀也就變得尤其頻繁,而方法區的 GC 是伴隨着 full gc 的, 因為 full gc 會造成 STW,在 full gc 期間其他程序都會停止,所以都會避免 full gc,而字符串常量池放在方法區中就減少了 字符串被回收的頻率,提高了 OOM 的概率。

 

類加載

過程

在 Class 數據文件被類加載器加載到 JVM 中到編譯執行,中間經歷 加載、鏈接、初始化、使用、卸載,其中鏈接又分為 驗證、准備、解析。需要注意的是:這些操作階段不一定要等上個階段完成后才能進行下一個階段,解析操作往往在初始化之后再執行。一部分驗證和加載同時執行,一部分驗證等到解析才會執行。下面就一個個來說明每一步的操作。

加載

通過類加載器將 Class 數據文件加載到方法區,並且在堆中創建一個 Class 對象用於訪問方法區的類數據。

驗證:

驗證主要用於檢驗傳來的二進制數據格式是否滿足加載要求。雖然在 java 文件的編譯階段編譯器已經進行了一次檢查,但是 JVM 是與前面編譯器編譯的過程隔開的。

驗證主要包括格式驗證、語義驗證、字節碼驗證、符號引用驗證。

1、格式驗證:與加載過程同時進行的。用於檢驗字節碼魔數是否正確、主版本和副版本是否在支持范圍內、數據每一項是否有正確的長度等。

2、語義驗證:校驗不同類之間的關系是否正確,例如是否繼承了抽象類但沒有實現方法,是否繼承了 final 類。

3、字節碼驗證:最復雜的一個驗證。從方法層面驗證各個操作是否正確,比如是否會跳轉到不存在的指令,函數調用是否傳遞正確類型的值,變量賦值是否給了正確的類型

4、符號引用驗證:發生在解析操作。將符號驗證轉化為直接引用時,驗證符號引用是否能正確使用。

准備

為類屬性分配內存並設置零值(這里不包括使用 static final 修飾的屬性且賦值的值是一個字符串常量或一個基本數據類型常量或其他不觸發方法的情況(也就是過程不會涉及構造器或者其他方法),因為字符串或者基本數據是常量,在編譯時期就會分配地址,准備階段直接就會顯式初始化,而如果賦的值包括方法調用就需要在 <client> 方法里執行)。如果屬性值是常量,那么常量值就會在方法區中分配內存,而如果是對象,那么對象則會在堆中創建;並且實例屬性參數也會跟隨對象的創建在堆中,只有靜態屬性和對應的常量值在方法區中分配內存。而設置的零值是當前類型的默認值,比如 private int a = 2;那么設的零值就是 0, a = 2 是在后面的<client>方法中執行的。

解析

將符號引用轉成直接直接引用。符號引用主要包括類或接口、靜態屬性、類方法和接口方法這四種(都是類數據,在類加載后就能獲取的)。

初始化

執行靜態代碼塊方法以及靜態屬性的賦值。會將類中所有的關於類屬性的賦值語句以及靜態代碼塊中的語句收集起來集中進 <clinet> 方法中,然后執行。執行的順序就是按賦值以及靜態代碼塊的排列順序執行。

虛擬機在在執行 <client>方法時會加鎖,使得此方法只會被一個線程加載,所以我們需要考慮類在加載時會不會發生異常死循環導致此類無法被加載

使用

使用不必多說,就是調用類屬性、方法。

卸載

上面說過一個類卸載所需要的條件:1、該類的所有對象都被回收;2、加載該類的類加載器被回收;3、該類對應的 Class 對象沒有在任何地方被引用(無法在任何地方通過反射訪問該類的方法)。那么具體原因是什么?

我們知道,對象被回收的條件是這個對象沒有被引用,類也是如此,在類被加載到內存后,它會在堆中創建一個 Class 對象,並且和加載它的加載器互相關聯,也就是圖中的 MyClassLoader,而這個對象也和類對應的實例對象所關聯,這種關聯是無法切斷的,而如果對應的三種變量都沒有再引用,那么就相當於這個類信息沒有被引用,那么也就可以被回收了。

 

類被加載的場景

Java 對類的使用方式分為主動使用和被動使用。主動使用會觸發類的初始化,被動使用不會(但是還是會觸發初始化之前的操作)。

主動使用的場景

1、創建某個類的對象

2、調用某個類的類屬性、類方法

3、獲取某個類的反射對象

4、初始化子類,如果父類沒有初始化,會先觸發父類的初始化(不適用接口)

5、如果一個接口定義了 default 方法,那么直接實現或者間接實現該接口的類的初始化,該接口要在其之前被初始化。

6、虛擬機啟動,調用主方法的類會被初始化

7、初次調用 MethodHanlder 實例時,初始化該 MethodHanlder 指向的方法所在的類。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄所在的類)

 

被動使用的場景

1、訪問的類屬性不是當前類的屬性,比如從父類繼承而來的或者實現接口得到的,比如

public class InitTest{
    public static void main(String[] args) {
        int a = son.a;
    }
}
class parent{
    public static int a =0;
    static {
        System.out.println("12");
    }
}
class son extends parent{
    public static int b =0;
    static {
        System.out.println("1ss2");
    }
}

這里只會觸發 parent 的初始化,而不會觸發 son 類的初始化,而如果 son 重寫了屬性 a 或者調用的是 son 的另一個屬性 b ,那么就會觸發 son 類的初始化,並且因為 son 繼承了 parent 類,所以在 son 初始化前還會先初始化 parent。

2、通過數組定義類引用,不會觸發此類的初始化(如果數組類型是基本數據類型,那么不需要加載;如果是引用數據類型,那么就進行類的加載,但不會進行初始化操作)

3、調用 static final 修飾的且是常量或者是字符串或是其他沒有方法觸發的情況,也不會觸發初始化操作。

4、調用 ClassLoader 的 loadClass() 方法加載一個類,只會觸發加載操作,而不會觸發初始化操作。

 

類加載器的拓展

類的唯一性

每個類加載器都有其自己的命名空間,命名空間由該加載器及其所有的父加載器所加載的類組成。在同一個命名空間中,不會出現類的完整名字(包名+類名)相同的兩個類。但在不同的命名空間中,有可能出現完整名字相同的兩個類。

所以,在比較兩個類是否是同一個類的前提是這兩個類由同一個類加載器加載,如果這兩個類是由兩個類加載器加載的,那么這兩個類必然不是同一個類。一個類只能被一個類加載器加載一次,但是可以被多個類加載器加載。

 

類加載器的主要方法

1、getParent()

返回該類加載器的父類加載器。

2、loadClass(name)

加載 name 類,如果找不到該類,就拋出異常。內部的實現是父類委托機制。

3、findClass(name)

 查找二進制的 name 類,返回該類的實例,這個類是 loadClass 內部調用的一個方法,JDK 維護了一個推薦的重寫方法,鼓勵我們去重寫這個方法來實現對功能的拓展。JDK 1.2 之前還未引入父類委托機制,所以要拓展就需要去重寫 loadClass 方法,1.2 引入父類委托機制后通過重寫 findClass 方法來拓展,並且也沒有破壞父類委托機制。

4、defineClass(String name, byte[] b,int off, int len)

將字節數組 b  轉換為 Class 的實例,off 和 len 參數表示實際 Class 信息在 byte 數組中的位置和長度。其中 b 是ClassLoader 從外部獲取的。這是受保護的方法,只有在自定義的 ClassLoader 子類中使用。一般在 findClass 方法中被調用,在 findClass 方法中先類的字節碼數組,然后調用 defineClass 獲取類實例返回。

 

ClassLoader 一些實現類的繼承關系

SecureClassLoader 擴展了 ClassLoader,增加一些方法,但是一般我們使用的是其子類 URLClassLoader,URLClassLoader 實現了 ClassLoader 很多抽象方法,如 findClass()、findResource() 。我們在編寫自定義類加載器時,如果沒有特別復雜的實現,可以直接繼承 URLClassLoader ,這樣可以避免自己編寫 findClass 以及獲取字節流的方式,使自定義類加載更加簡潔。而拓展類加載器與系統類加載器也是繼承 URLClassLoader 。

 

Class.forName 與 ClassLoader.loadClass 的區別

ClassLoader.loadClass 是一個實例方法,該方法將 Class 文件加載到內存中后,只會執行類加載過程的加載、驗證、准備、 解析。初始化等到類的第一次使用時才會執行。Class.forName 是靜態方法,該方法在將 Class 文件加載到內存的同時,還會執行類的初始化。

 

破壞雙親委派機制的三次場景

1、由於雙親委派機制是在 JDK1.2 之后才引入的,而在 Java 的第一個版本就有類加載器的概念以及抽象類 ClassLoader ,所以此時是沒有雙親委派機制的,用戶自定義類加載器就是直接重寫 loadClass 方法,這也就是破壞了雙親委托機制。

2、第二次是為了彌補雙親委托機制的缺陷,因為雙親委托機制使得父類加載器無法使用子類加載器的類資源,這樣對於父類需要調用子類加載器加載的類資源時就無法實現。為了解決這個問題,引入了線程上下文類加載器(默認為系統類加載器),當需要調用系統類加載器就可以使用這個屬性進行加載。

3、IBM 公司設計的代碼熱部署,使得傳統簡單的樹狀繼承關系,改成了更為復雜的網狀結構,讓每個模塊都有自己自定義的類加載器。

 

自定義類加載器

好處

1、隔離加載類,創建多個模塊空間,確保相互間加載的類不會沖突。

2、修改類加載的方式。某些非必要導入的類可以自定義類加載器在某個事件按需導入。

3、擴展加載器,加載不同位置位置的資源。

4、防止源碼外泄。在編譯時加密。

 

注意

1、因為同一個類被兩個類加載器加載會生成不同的類對象,所以如果兩個繼承關系的類被兩個類加載器加載,那么強制轉換類型會報錯。所以使用自定義類加載器需要結合場景,不能一味使用。

2、實現時推薦重寫 findClass 方法,不破壞雙親委托機制。

 

沙箱安全機制

Java 沙箱是將 Java 代碼限定在 JVM 特定的運行范圍中,並且嚴格限制代碼對本地系統資源的訪問。防止對本地系統造成破壞。

演變

1、JDK 1.0 時期

將執行的 Java 代碼分為本地和遠程兩種,本地代碼默認視為可信賴的,而遠程代碼則看作不受信賴的。對於信賴的代碼,可以訪問一切本地資源。而不受信賴的代碼,則會受到沙箱的限制,不能訪問本地資源。

2、JDK 1.1 時期

由於1.0 中對遠程代碼限制太過激進,導致一些需要訪問本地資源的遠程代碼無法訪問,極大影響了程序的可用性,所以在 1.1 中進行了優化,在前者基礎上,增加了 安全策略。允許用戶指定代碼對本地資源的訪問權限。

 

 3、JDK 1.2 時期

1.1 中無法解決的是本地代碼權限問題,因為本地都是可以訪問本地資源的,所以在 1.2 中又引入了 代碼簽名。無論是本地代碼還是遠程代碼,都會按照用戶的安全策略設定,由類加載器加載到虛擬機中權限不同的運行空間,來實現差異化的代碼執行權限控制。

 4、JDK 1.6時期

也是當前最新的安全策略,相比於前代引入了域的概念。主要升級是將資源的訪問進一步划分。虛擬機會把所有代碼加載到系統域或應用域中。系統域是與關鍵資源交互,而各個應用域部分則通過系統域的部分代理來對各種需要的資源進行訪問。

 

 

JDK9 的新特性

1、擴展類加載器改名為平台類加載器(platform classloader)。可以通過 ClassLoader 的新方法 getPlatformClassLoader() 來獲取。

2、原來的 rt.jar(啟動類加載器加載的核心源碼)和 tool.jar(Java程序啟動所需的 class 目錄下的類) 被拆分成數十個 JMOD 文件,Java 類庫也被改成可擴展的模式,所以拓展目錄也就無需存在了。

3、平台類加載器和應用程序類加載器不再繼承 URLClassLoader。現在 三大加載器全部繼承於 jdk.internal.loader.BuiltinClassLoader

4、類加載器擁有了 name 屬性,可以通過 getName() 獲取,平台類加載器的 name 是 platform。應用類加載器的名稱是 app。類加載器的名稱在調試與類加載器相關的問題時會非常有用。

5、啟動類加載器現在是 jvm 內部和 java 類庫共同協作的實現的(c++和java,過去只是c++),但是為了與之前的代碼兼容,在獲取啟動類加載器的場景中仍然為 null。

6、委派機制變化。在加載器受到加載請求后,會先判斷該類是否屬於某個系統模塊,如果屬於直接將這個請求發給這個模塊的類加載器。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM