一晚上搞懂 | JVM 類加載機制

前言

本文已經收錄到個人Github我的博客,歡迎大佬們光臨寒舍:java

個人GIthub博客git

學習導圖

學習導圖

一.爲何要學習類加載機制?

今天想跟你們嘮嗑嘮嗑Java的類加載機制,這是Java的一個很重要的創新點,曾經也是Java流行的重要緣由之一。github

Oracle當初引入這個機制是爲了知足Java Applet開發的需求,JVM咬咬牙引入了Java類加載機制,後來的基於Jvm的動態部署,插件化開發包括你們熱議的熱修復,總之不少後來的技術都源於在JVM中引入了類加載器。面試

現在,類加載機制也在各個領域大放異彩,在面試中,由類加載機制所衍生出來各種面試題也層出不窮。數據庫

因此,咱們要了解下類加載機制,爲工做中或者是面試中實際的須要打好良好的基礎。數組

二.核心知識點概括

2.1 概述

Q1:JVM類加載機制定義安全

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗轉換解析初始化,最終造成可被虛擬機直接使用的Java類型的過程網絡

Q2:特性數據結構

運行期類加載。即在Java語言裏面,類型的加載、鏈接和初始化過程都是在程序運行期完成的,從而經過犧牲一些性能開銷來換取Java程序的高度靈活性多線程

什麼是運行期,什麼是編譯期?

  • 編譯期是指編譯器將源代碼翻譯機器能識別的代碼Java被編譯爲Jvm認識的字節碼文件
  • 運行期則是指Java代碼的運行過程

JVM運行期動態加載+動態鏈接->Java的動態擴展特性

2.2 類加載的過程

類從被加載到虛擬機內存中開始、到卸載出內存爲止,整個生命週期包括七個階段:

  • 加載

  • 驗證

  • 準備

  • 解析

  • 初始化

  • 使用

  • 卸載

其中,驗證、準備、解析這3個部分統稱爲鏈接,流程以下圖:

類加載過程

注意:

  • 『加載』->『驗證』->『準備』->『初始化』->『卸載』這五個階段的順序是肯定的,而『解析』可能爲了支持Java的動態綁定會在『初始化』後纔開始
  • 上述階段一般都是互相交叉地混合式進行的,好比會在一個階段執行的過程當中調用、激活另一個階段

想要了解Java動態綁定和靜態綁定區別的話,能夠看下這篇文章:理解靜態綁定與動態綁定

2.2.1 加載

Q1:任務

  • 經過類的全限定名來獲取定義此類的二進制字節流。如從ZIP包讀取、從網絡中獲取、經過運行時計算生成、由其餘文件生成、從數據庫中讀取等等途徑......

想要詳細瞭解類的全限定名的知識,能夠看下這篇文章:全限定名、簡單名稱和描述符是什麼東西?

  • 將該二進制字節流所表明的靜態存儲結構轉化爲方法區運行時數據結構,該數據存儲數據結構由虛擬機實現自行定義
  • 在內存中生成一個表明這個類的java.lang.Class對象,它將做爲程序訪問方法區中的這些類型數據的外部接口

2.2.2 驗證

  • 鏈接階段的第一步,且工做量在JVM類加載子系統中佔了至關大的一部分
  • 目的:爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全

因而可知,它能直接決定JVM可否承受惡意代碼的攻擊,所以驗證階段很重要,但因爲它對程序運行期沒有影響,並不必定必要,能夠考慮使用-Xverify:none參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

  • 檢驗過程包括下面四個階段:

    A.文件格式驗證:

    • 內容:驗證字節流是否符合Class文件格式的規範、以及是否能被當前版本的虛擬機處理

    • 目的:保證輸入的字節流能正確地解析並存儲於方法區以內,且格式上符合描述一個Java類型信息的要求。只有保證二進制字節流經過了該驗證後,它纔會進入內存的方法區中進行存儲,因此後續3個驗證階段所有是基於方法區而不是字節流了

    • 例子:

      1. 是否以魔數0xCAFEBABE開頭

      2. 主次版本號是否在JVM接受範圍內

      3. 索引值是否有指向不存在/不符合類型的常量

        ......

    B.元數據驗證:

    • 內容:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求

    • 目的:對類的元數據信息進行語義校驗,保證不存在不符合Java語言規範的元數據信息

    • 例子:

      1. 類是否有父類(除了java.lang.Object以外,全部類都應有父類)

      2. 父類是否繼承了不容許被繼承的類(final修飾的類)

      3. 若是該類不是抽象類,是否實現了其父類或接口中要求實現的全部方法

        ......

    ​ C.字節碼驗證:

    • 是驗證過程當中最複雜的一個階段

    • 內容:對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件

    • 目的:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的

    • 例子:

      1. 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現「在操做數棧的數據類型中放置了int類型的數據,使用時卻按long類型來載入本地變量表中」

      2. 保證任何跳轉指令都不會跳轉到方法體外的字節碼指令上

        ......

    ​ D.符號引用驗證:

    • 內容:對類自身之外(如常量池中的各類符號引用)的信息進行匹配性校驗
    • 目的:確保解析動做能正常執行,若是沒法經過符號引用驗證,那麼將會拋出一個java.lang.IncompatibleClassChangeError異常的子類
    • 注意:該驗證發生在虛擬機將符號引用轉化爲直接引用的時候,即『解析』階段

2.2.3 準備

Q1:任務

  • 爲類變量(靜態變量)分配內存由於這裏的變量是由方法區分配內存的,因此僅包括類變量而不包括實例變量,後者將會在對象實例化時隨着對象一塊兒分配在Java堆中
  • 設置類變量初始值:一般狀況下零值

2.2.4 解析

以前提過,解析階段就是虛擬機將常量池內的符號引用替換爲直接引用的過程

  • 符號引用:以一組符號來描述所引用的目標
  • 能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可
  • 與虛擬機實現的內存佈局無關,由於符號引用的字面量形式明肯定義在Java虛擬機規範的Class文件格式中,因此即便各類虛擬機實現的內存佈局不一樣,可是能接受符號引用都是一致的
  • 直接引用:
  • 能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄
  • 與虛擬機實現的內存佈局相關,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不一樣
  • 發生時間:JVM會根據須要來判斷,是在類被加載器加載時就對常量池中的符號引用進行解析,仍是等到一個符號引用將要被使用前纔去解析
  • 解析動做:有七類符號及其對應在常量池的七種常量類型
  • 類或接口(CONSTANT_Class_info)
  • 字段(CONSTANT_Fieldref_info)
  • 類方法(CONSTANT_Methodref_info)
  • 接口方法(CONSTANT_InterfaceMethodref_info)
  • 方法類型(CONSTANT_MethodType_info)
  • 方法句柄(CONSTANT_MethodHandle_info)
  • 調用點限定符(CONSTANT_InvokeDynamic_info)

舉個例子,設當前代碼所處的爲類D,把一個從未解析過的符號引用N解析爲一個類或接口C的直接引用,解析過程分三步:

  • C不是數組類型:JVM將會把表明N的全限定名傳遞給D類加載器去加載這個類C。在加載過程當中,因爲元數據驗證字節碼驗證的須要,又可能觸發其餘相關類的加載動做。一旦這個加載過程出現了任何異常,解析過程就宣告失敗。
  • C是數組類型且數組元素類型爲對象:JVM也會按照上述規則加載數組元素類型
  • 若上述步驟無任何異常:此時CJVM中已成爲一個有效的類或接口,但在解析完成前還需進行符號引用驗證,來確認D是否具有對C的訪問權限。若是發現不具有訪問權限,將拋出java.lang.IllegalAccessError異常

Q1:字段(成員變量/域)和屬性有什麼區別?

  • 屬性,是指對象的屬性,對於JavaBean來講,是getXXX方法定義的
  • 字段,是成員變量
class Person{
    private String mingzi;  //mingzi是字段,通常來講字段和屬性是相同的,可是這個例子是特例
    public String getName(){  //name是屬性
        return mingzi:
    }
    public void setName(){
        mingzi= "張三";
    }
}
複製代碼

2.2.5 初始化

  • 是類加載過程的最後一步,會開始真正執行類中定義的Java代碼。而以前的類加載過程當中,除了在『加載』階段用戶應用程序可經過自定義類加載器參與以外,其他階段均由虛擬機主導和控制
  • 與『準備』階段的區分
  • 準備階段:變量賦初始零值
  • 初始化階段:根據Java程序的設定去初始化類變量和其餘資源,或者說是執行類構造器clinit的過程

clinit:由編譯器自動收集類中的全部類變量(靜態變量)的賦值動做和靜態語句塊static{}中的語句合併產生

  • 線程安全的,在多線程環境中被正確地加鎖、同步
  • 對於類或接口來講是必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成 clinit
  • 接口與類不一樣的是,執行接口的 clinit不須要先執行父接口clinit,只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的clinit

想詳細瞭解clinit以及其與init的區別的讀者,能夠看下這篇文章:深刻理解jvm--Java中init和clinit區別徹底解析

  • 在虛擬機規範中,規定了有且只有五種狀況必須當即對類進行『初始化』:
  • 遇到newgetstaticputstaticinvokestatic這4條字節碼指令時
  • 使用java.lang.reflect包的方法對類進行反射調用的時候
  • 當初始化一個類的時候,若發現其父類還未進行初始化,需先觸發其父類的初始化
  • 在虛擬機啓動時,需指定一個要執行的主類,虛擬機會先初始化它
  • 當使用JDK1.7的動態語言支持時,若一個java.lang.invoke.MethodHandle實例最後的解析結果爲REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,且這個方法句柄所對應的類未進行初始化,需先觸發其初始化。

2.3 類加載器&雙親委派模型

每一個類加載器,都擁有一個獨立的命名空間,它不只用於加載類,還和這個類自己一塊兒做爲在JVM中的惟一標識。因此比較兩個類是否相等,只要看它們是否由同一個類加載器加載,即便它們來源於同一個Class文件且被同一個JVM加載,只要加載它們的類加載器不一樣,這兩個類就一定不相等

2.3.1 類加載器

JVM的角度,可將類加載器分爲兩種:

  • 啓動類加載器
  • C++語言實現,是虛擬機自身的一部分
  • 負責加載存放在<JAVA_HOME>\lib目錄中、或被-Xbootclasspath參數所指定路徑中的、且可被虛擬機識別的類庫
  • 沒法被Java程序直接引用,若是自定義類加載器想要把加載請求委派給引導類加載器的話,可直接用null代替
  • 其餘類加載器:由Java語言實現,獨立於虛擬機外部,而且全都繼承自抽象類java.lang.ClassLoader,可被Java程序直接引用。常見幾種:
  • 擴展類加載器

    A.由sun.misc.Launcher$ExtClassLoader實現

    B.負責加載<JAVA_HOME>\lib\ext目錄中的、或者被java.ext.dirs系統變量所指定的路徑中的全部類庫

  • 應用程序類加載器

    A.是默認的類加載器,是ClassLoader#getSystemClassLoader()的返回值,故又稱爲系統類加載器

    B.由sun.misc.Launcher$App-ClassLoader實現

    C.負責加載用戶類路徑上所指定的類庫

  • 自定義類加載器:若是以上類加載起不能知足需求,可自定義

類加載器的關係

須要注意的是:雖然數組類不經過類加載器建立而是由JVM直接建立的,但仍與類加載器有密切關係,由於數組類的元素類型最終還要靠類加載器去建立

2.3.2 雙親委派模型

  • 定義:表示類加載器之間的層次關係
  • 前提:除了頂層啓動類加載器外,其他類加載器都應當有本身的父類加載器,且它們之間關係通常不會以繼承關係來實現,而是經過組合關係來複用父加載器的代碼
  • 工做過程:若一個類加載器收到了類加載的請求,它先會把這個請求委派給父類加載器,並向上傳遞,最終請求都傳送到頂層的啓動類加載器中。只有當父加載器反饋本身沒法完成這個加載請求時,子加載器纔會嘗試本身去加載
  • 注意:不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式
  • 優勢:類會隨着它的類加載器一塊兒具有帶有優先級的層次關係,可保證Java程序的穩定運做;實現簡單,全部實現代碼都集中在java.lang.ClassLoader的loadClass()

好比,某些類加載器要加載java.lang.Object類,最終都會委派給最頂端的啓動類加載器去加載,這樣Object類在程序的各類類加載器環境中都是同一個類。

相反,系統中將會出現多個不一樣的Object類,Java類型體系中最基礎的行爲也就沒法保證,應用程序也將會變得一片混亂

三.課堂小測試

恭喜你!已經看完了前面的文章,相信你對JVM類加載機制已經有必定深度的瞭解,下面,進行一下課堂小測試,驗證一下本身的學習成果吧!

Q1:類加載的全過程是怎樣的?

Q2:什麼是雙親委派模型?

Q3:String類如何被加載的

上面問題的答案,在前文都提到過,若是還不能回答出來的話,建議回顧下前文

Q4:請你談談類加載過程,以Person a = new Person();爲例進行說明

這道題是在牛客的暑假實習Tencent一面的麪筋上找的,附上標準答案:類的加載過程,Person person = new Person();爲例進行說明


若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力

本文參考連接:

相關文章
相關標籤/搜索