JVM類加載與雙親委派機制被打破


前言

前文已經講了虛擬機將java文件編譯成class文件后的格式:JVM虛擬機Class類文件研究分析

java文件經過編譯,形成class文件,那么虛擬機如何將這些Class文件讀取到內存中呢?

加載的時機

JVM 會在程序第一次主動引用類的時候加載該類,被動引用時並不會引發類加載的操作。也就是說,JVM 並不是在一開始就把一個程序就所有的類都加載到內存中,而是到不得不用的時候才把它加載進來,而且只加載一次。

一個類的生命周期如圖所示:

上圖中的加載、驗證、准備、初始化、卸載這幾個步驟是相對固定的,但是初始化這一步不一定,他在某些情況下可以是再初始化之后執行。

加載

加載是類加載的第一階段,虛擬機此時主要做以下三件事情:

1.通過類的全限定名來獲取定義這個類的二進制字節流。

2.將字節流的靜態存儲結構轉化為運行時的數據結構;

3.在內存中生成該類的 java.lang.Class 對象,作為方法區這個類各種數據訪問入口。

主動引用一定會加載,但是被動引用則不一定

主動引用

  1. 遇到 new、getstatic、putstatic、invokestatic 字節碼指令,例如:

    使用 new 實例化對象;

    讀取或設置一個類的 static 字段(被 final 修飾的除外);

    調用類的靜態方法。

  2. 對類進行反射調用;

  3. 初始化一個類時,其父類還沒初始化(需先初始化父類);

    這點類與接口具有不同的表現,接口初始化時,不要求其父接口完成初始化,只有真正使用父接口時才初始化,如引用父接口中定義的常量。

  4. 虛擬機啟動,先初始化包含 main() 函數的主類;

  5. JDK 1.7 動態語言支持:一個 java.lang.invoke.MethodHandle 的解析結果為 REF_getStatic、REF_putStatic、REF_invokeStatic。

被動引用

  1. 通過子類引用父類靜態字段,不會導致子類初始化;

  2. Array[] arr = new Array[10]; 不會觸發 Array 類初始化;

  3. static final VAR 在編譯階段會存入調用類的常量池,通過 ClassName.VAR 引用不會觸發 ClassName 初始化。

也就是說,只有發生主動引用所列出的 5 種情況,一個類才會被加載到內存中,也就是說類的加載是 lazy-load 的,不到必要時刻是不會提前加載的,畢竟如果將程序運行中永遠用不到的類加載進內存,會占用方法區中的內存,浪費系統資源。

驗證

目的: 確保 .class 文件中的字節流信息符合虛擬機的要求。

4 個驗證過程:

文件格式驗證:是否符合 Class 文件格式規范,驗證文件開頭 4 個字節是不是 “魔數” 0xCAFEBABE

元數據驗證:保證字節碼描述信息符號 Java 規范(語義分析)

字節碼驗證:程序語義、邏輯是否正確(通過數據流、控制流分析)

符號引用驗證:對類自身以外的信息(常量池中的符號引用)進行匹配性校驗

這個操作雖然重要,但不是必要的,可以通過 -Xverify:none 關掉。

准備

  • 描述: 為 static 變量(類變量,非實例變量)在方法區分配內存。

  • static 變量准備后的初始值:

    當static變量未被final修飾時:

    public static int value = 123;
    

    准備后為 0,value 的賦值指令 putstatic 會被放在 () 方法中,()方法會在初始化時執行,也就是說,value 變量只有在初始化后才等於 123。

    當static變量被final修飾時:

    public static final int value = 123;
    

    准備后為 123,因為被 static final 賦值之后 value 就不能再修改了,所以在這里進行了賦值之后,之后不可能再出現賦值操作,所以可以直接在准備階段就把 value 的值初始化好。

解析

描述:將常量池中的 “符號引用” 替換為 “直接引用”,也就是說將引用指向內存。

符號引用,比如com.courage.People引用了com.courage.Man,這時候Man並不在內存中

但是直接飲用則是引用Man所在的內存地址。

在此之前,常量池中的引用是不一定存在的,解析過之后,可以保證常量池中的引用在內存中一定存在。

什么是 “符號引用” 和 “直接引用” ?

  • 符號引用:以一組符號描述所引用的對象(如對象的全類名),引用的目標不一定存在於內存中。
  • 直接引用:直接指向被引用目標在內存中的位置的指針等,也就是說,引用的目標一定存在於內存中。

初始化

描述: 執行類構造器<clinit>()方法的過程。

<clinit>()方法包含的內容:

所有 static 的賦值操作;

static 塊中的語句;

<clinit>()方法中的語句順序:

基本按照語句在源文件中出現的順序排列;

靜態語句塊只能訪問定義在它前面的變量,定義在它后面的變量,可以賦值,但不能訪問。

與實例構造器<init>()不同的地方在於:

不需要顯示調用父類的<clinit>()方法;

虛擬機保證在子類的<clinit>()方法執行前,父類的<clinit>()方法一定執行完畢。

也就是說,父類的 static 塊和 static 字段的賦值操作是要先於子類的。

接口與類的不同

執行子接口的<clinit>()方法前不需要先執行父接口的<clinit>()方法(除非用到了父接口中定義的 public static final 變量);

執行過程中加鎖

同一時刻只能有一個線程在執行<clinit>()方法,因為虛擬機要保證在同一個類加載器下,一個類只被加載一次。

非必要性:

一個類如果沒有任何 static 的內容就不需要執行 <clinit>()方法。

注:初始化時,才真正開始執行類中定義的 Java 代碼。

虛擬機規范中並沒有規定何時加載類,但是以下6種場景,場景必須初始化

類的顯式加載和隱式加載

顯示加載

  1. 調用 ClassLoader#loadClass(className)Class.forName(className)

  2. 兩種顯示加載 .class 文件的區別:

    Class.forName(className) 加載 class 的同時會初始化靜態域,ClassLoader#loadClass(className) 不會初始化靜態域;

    Class.forName 借助當前調用者的 class 的 ClassLoader 完成 class 的加載。

隱式加載

  1. new 類對象;

  2. 使用類的靜態域;

  3. 創建子類對象;

  4. 使用子類的靜態域;

  5. 其他的隱式加載,在 JVM 啟動時

    BootStrapLoader 會加載一些 JVM 自身運行所需的 Class;

    ExtClassLoader 會加載指定目錄下一些特殊的 Class;

    AppClassLoader 會加載 classpath 路徑下的 Class,以及 main 函數所在的類的 Class 文件。

雙親委派機制

通過一個類的全限定名來獲取描述該類的二進制字節流這個動作在Java虛擬機外部實現,這樣做的好處是應用程序自己決定如何去獲取所需的類。實現這個動作的代碼被稱為“類加載器”(Class Loader)。

在比較兩個類是不是同一個類,只有在同一個類加載器下比較才有意義,對於同一個類用不同的加載器加載內存,兩個類是不相等的。

站在Java虛擬機的角度來看,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現 ,是虛擬機自身的一部分;另外一種就是其他所有的類加載器,這些類加載器都由Java語言實現,獨立存在於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader。

為了保證加載應該被加載的類,遵循雙親委派機制,目的是保證安全性,例如自己定義的String類不至於替換掉虛擬機默認的String類,雙親委派機制如圖:

需要說明的是,此處的Cache以及倉庫是我為了后面說明方便而做的定義,每一個啟動器都有自己對應的Class文件存放位置,將這個位置稱之為倉庫,已經加載進內存的Class存放在內存中,這塊內存稱之為Cache,對於我們自定義的String類,肯定是放在用戶空間的倉庫上,如果要加載這個類,會依次往上查找,各級的內存,首先查找用戶自定義的ClassLoader,如果已經加載過就直接返回,如果沒有加載過就往上一類加載器緩存中查找,如果直到Bootstrap都沒有找到的話就會開始查找倉庫,查找倉庫的順序與查找緩存相反,先查找Bootstrap的倉庫,再查找Extension,找到就加載然后返回Class,也就意味着,自定義的String根本沒法被查找到,因為在Bootstrap倉庫中已經查找到String並且加載返回了。

雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載器。不過這里類加載器之間的父子關系一般不是以繼承(Inheritance)的關系來實現的,而是通常使用組合(Composition)關系來復用父加載器的代碼。

使用雙親委派模型來組織類加載器之間的關系,一個顯而易見的好處就是Java中的類隨着它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的啟動類加載器進行加載,因此Object類在程序的各種類加載器環境中都能夠保證是同一個類。

雙親委派機制被破壞

在Java的世界中大部分的類加載器都遵循這個模型,但也有例外的情況,直到Java模塊化出現為止,雙親委派模型主要出現過3次較大規模“被破壞”的情況。

第一次被破壞

雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前——即JDK 1.2面世以前的“遠古”時代。由於雙親委派模型在JDK 1.2之后才被引入,但是類加載器的概念和抽象類java.lang.ClassLoader則在Java的第一個版本中就已經存在,面對已經存在的用戶自定義類加載器的代碼,Java設計者們引入雙親委派模型時不得不做出一些妥協,為了兼容這些已有代碼,無法再以技術手段避免loadClass()被子類覆蓋的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一個新的protected方法findClass(),並引導用戶編寫的類加載邏輯時盡可能去重寫這個方法,而不是在loadClass()中編寫代碼。上節我們已經分析過loadClass()方法,雙親委派的具體邏輯就實現在這里面,按照loadClass()方法的邏輯,如果父類加載失敗,會自動調用自己的findClass()方法來完成加載,這樣既不影響用戶按照自己的意願去加載類,又可以保證新寫出來的類加載器是符合雙親委派規則的。

第二次被破壞

雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷導致的,雙親委派很好地解決了各個類加載器協作時基礎類型的一致性問題(越基礎的類由越上層的加載器進行加載),基礎類型之所以被稱為“基礎”,是因為它們總是作為被用戶代碼繼承、調用的API存在,但程序設計往往沒有絕對不變的完美規則,如果有基礎類型又要調用回用戶的代碼,那該怎么辦呢?

這並非是不可能出現的事情,一個典型的例子便是JNDI服務,JNDI現在已經是Java的標准服務,它的代碼由啟動類加載器來完成加載(在JDK 1.3時加入到rt.jar的),肯定屬於Java中很基礎的類型了。但JNDI存在的目的就是對資源進行查找和集中管理,它需要調用由其他廠商實現並部署在應用程序的ClassPath下的JNDI服務提供者接口(Service Provider Interface,SPI)的代碼,現在問題來了,啟動類加載器是絕不可能認識、加載這些代碼的,那該怎么辦?

為了解決這個困境,Java的設計團隊只好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContext-ClassLoader()方法進行設置,如果創建線程時還未設置,它將會從父線程中繼承一個,如果在應用程序的全局范圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。

有了線程上下文類加載器,程序就可以做一些“舞弊”的事情了。JNDI服務使用這個線程上下文類加載器去加載所需的SPI服務代碼,這是一種父類加載器去請求子類加載器完成類加載的行為,這種行為實際上是打通了雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的一般性原則,但也是無可奈何的事情。Java中涉及SPI的加載基本上都采用這種方式來完成,例如JNDI、JDBC、JCE、JAXB和JBI等。不過,當SPI的服務提供者多於一個的時候,代碼就只能根據具體提供者的類型來硬編碼判斷,為了消除這種極不優雅的實現方式,在JDK 6時,JDK提供了java.util.ServiceLoader類,以META-INF/services中的配置信息,輔以責任鏈模式,這才算是給SPI的加載提供了一種相對合理的解決方案。

第三次被破壞

雙親委派模型的第三次“被破壞”是由於用戶對程序動態性的追求而導致的,這里所說的“動態性”指的是一些非常“熱”門的名詞:代碼熱替換(Hot Swap)、模塊熱部署(Hot Deployment)等。說白了就是希望Java應用程序能像我們的電腦外設那樣,接上鼠標、U盤,不用重啟機器就能立即使用,鼠標有問題或要升級就換個鼠標,不用關機也不用重啟。對於個人電腦來說,重啟一次其實沒有什么大不了的,但對於一些生產系統來說,關機重啟一次可能就要被列為生產事故,這種情況下熱部署就對軟件開發者,尤其是大型系統或企業級軟件開發者具有很大的吸引力。


免責聲明!

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



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