天天看點

類的加載過程和類加載器

一般分為三個比較大的階段,分别是加載階段,連接配接階段和初始化階段,五個主要的階段。

  在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是确定的,而解析階段則不一定,它在某些情況下可以在初始化階段之後開始。另外注意這裡的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執行的過程中調用或激活另一個階段。

如下圖所示:

類的加載過程和類加載器

  (1)通過一個類的全限定名(包名+類名)來擷取其定義的二進制位元組流

  (2)将這個位元組流所代表的的靜态存儲結構轉化為方法區的運作時資料結構

  (3)在堆中生成一個代表這個類的java.lang.class對象,作為方法區中這些資料的通路入口。

驗證的主要作用就是確定被加載的類的正确性。也是連結階段的第一步。主要是完成四個階段的驗證:

  (1)檔案格式的驗證: 驗證.class檔案位元組流是否符合class檔案的格式的規範,并且能夠被目前版本的虛拟機處理。這裡面主要對魔數、主版本号、常量池等等的校驗

  (2)中繼資料驗證: 主要是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合java語言規範的要求

  (3)位元組碼驗證:這是整個驗證過程最複雜的階段,主要是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。在中繼資料驗證階段對資料類型做出驗證後,這個階段主要對類的方法做出分析,保證類的方法在運作時不會做出危害虛拟機安全的事。

  (4)符号引用驗證:在解析階段,會将虛拟機中的符号引用轉化為直接引用,該階段則負責對各種符号引用進行比對性校驗,保證外部依賴真實存在,并且符合外部依賴類、字段、方法的通路性

  準備階段主要為靜态變量配置設定記憶體并設定初始值(預設值)

  注意:

    1.執行個體變量不會進行記憶體的配置設定,執行個體變量主要随着對象的執行個體化一塊配置設定到java堆中

    2.為靜态變量設定的是預設值,如static int a=5; 這個時候a的值是0

    3.靜态常量由于是用final static進行修飾的,final修飾的靜态變量不會導緻類的初始化,final static int b=5,在這個時候b的值就是5

  解析階段主要是虛拟機将常量池中的符号引用轉化為直接引用的過程

  符号引用:以一組符号來描述所引用的目标,可以是任何形式的字面量,隻要是能無歧義的定位到目标就好,符号引用與虛拟機實作的記憶體布局無關,引用的目标并不一定已經加載到記憶體中。

  直接引用:直接引用是可以指向目标的指針、相對偏移量或者是一個能直接或間接定位到目标的句柄。和虛拟機實作的記憶體有關,不同的虛拟機直接引用一般不同。如果有了直接引用,那引用的目标必定已經在記憶體中存在。

  初始化階段是執行類構造器<clinit>()方法的過程。類構造器<clinit>()方法是由編譯器自動收集類中的所有類變量的指派動作和靜态代碼塊(static塊)中的語句合并産生的。

 也就是說會按照一定的順序來繼續,靜态變量的指派,靜态代碼塊的調用,靜态變量的指派等等,jvm會保證一個類的<clinit>()方法在多線程環境中被正确加鎖和同步。

  當初始化一個類的時候,如果發現其父類還沒有進行過初始化、則需要先初始化其父類。

運作結果

類的加載過程和類加載器

所有靜态的代碼塊和靜态變量會先于非靜态變量進行加載

1、父類的靜态變量

2、父類的靜态代碼塊

3、子類的靜态變量

4、子類的靜态代碼塊

5、父類的非靜态變量

6、父類的非靜态代碼塊

7、父類的構造方法

8、子類的非靜态變量

9、子類的非靜态代碼塊

10、子類的構造方法

接下來調整代碼的順序

類的加載過程和類加載器

 将靜态變量和非靜态變量移到後面然後執行

類的加載過程和類加載器

 java中的類加載器主要分為下圖的四種

類的加載過程和類加載器

引導類加載器(bootstrap class loader)

(1)它用來加載 java 的核心庫(java_home/jre/lib/rt.jar,sun.boot.class.path路徑下的内容),是用原生代碼(c語言)來實作的,并不繼承自 java.lang.classloader。

(2)加載擴充類和應用程式類加載器。并指定他們的父類加載器。

擴充類加載器(extensions class loader)

(1)用來加載 java 的擴充庫(java_home/jre/ext/*.jar,或java.ext.dirs路徑下的内容) 。java 虛拟機的實作會提供一個擴充庫目錄。該類加載器在此目錄裡面查找并加載 java類。

(2)由sun.misc.launcher$extclassloader實作。

應用程式類加載器(application class loader)

(1)它根據 java 應用的類路徑(classpath,java.class.path 路徑下的内容)來加載 java 類。一般來說,java 應用的類都是由它來完成加載的。

(2)由sun.misc.launcher$appclassloader實作

自定義類加載器

(1)開發人員可以通過繼承 java.lang.classloader類的方式實作自己的類加載器,以滿足一些特殊的需求。

(2)遵守雙親委派模型:繼承classloader,重寫findclass()方法。

(3)破壞雙親委派模型:繼承classloader,重寫loadclass()方法。

雙親委派的執行流程:

  如果一個類加載器收到了加載某個類的請求,則該類加載器并不會去加載該類,而是把這個請求委派給父類加載器,每一個層次的類加載器都是如此,是以所有的類加載請求最終都會傳送到頂端的啟動類加載器;隻有當父類加載器在其搜尋範圍内無法找到所需的類,并将該結果回報給子類加載器,子類加載器會嘗試去自己加載。

雙親委派模型的好處

(1)主要是為了安全性,避免使用者自己編寫的類動态替換 java的一些核心類,比如 string。

(2)同時也避免了類的重複加載,因為 jvm中區分不同類,不僅僅是根據類名,相同的 class檔案被不同的 classloader加載就是不同的兩個類。

沙箱安全機制:部落格位址 javascript:void(0)

為什麼要破壞雙親委派模型?

首先你要知道:雙親委派模型不是一種強制性限制,它是一種java設計者推薦使用類加載器的方式。

但是有的時候不得不違反這個限制,例如spi,他不是和api一樣,它面向拓展的,spi全稱service provider interface,是java提供的一套用來被第三方實作或者擴充的api,它可以用來啟用架構擴充和替換元件。例如 資料庫驅動加載接口實作類的加載、spring、日志接口實作類加載,

其中最常使用的就是jdbc

  位于java.sql包下的drivermanager類,它需要資料庫的驅動器,這個驅動器一般是資料庫廠商進行,按照雙親委派模型來說的。drivermanager類是會被引導類加載器進行加載的,而其實作是由服務商提供的,由系統類加載器加載,這個時候就需要啟動類加載器來委托子類來加載driver實作,進而破壞了雙親委派。

在調用drivermanager的時候,會先初始化類,調用其中的靜态塊:

靜态代碼塊中調用了loadinitialdrivers()方法,改方法中的其他的東西我們不關注,隻關注标紅的對象和方法serviceloader.load

serviceloader.load()方法

load方法調用擷取了目前線程中的上下文類加載器,在launcher初始化的時候,會擷取appclassloader,然後将其設定為上下文類加載器,是以上下文類加載器預設情況下就是系統加載器。

 可以使用 -verbose:class參數來檢視類加載器所加載的類

類的加載過程和類加載器

  全盤負責是指當一個classloader加載一個類時,除非顯示地使用另一個classloader,則該類所依賴與引用的類也由這個classloader加載。