一個 java 檔案的執行過程詳解

萌新J發表於2021-03-03

平時我們都使用 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 的位置。如果呼叫了其他方法並獲取了返回值,那麼在呼叫方法執行完畢後,該方法的返回值會被壓入棧頂,然後再進行後續的操作。

棧頂快取技術

目前只是一種想法,還未實現。因為使用的是棧式架構,所以指令多,又由於運算元是儲存在記憶體中的,所以頻繁地讀寫必然會影響執行速度,所以提出將棧頂元素全部快取在物理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、委派機制變化。在載入器受到載入請求後,會先判斷該類是否屬於某個系統模組,如果屬於直接將這個請求發給這個模組的類載入器。

 

相關文章