JVM 類加載機制

概述

Java文件最終會被編譯成Class文件,Class文件最終須要加載到JVM中才能運行和使用,虛擬機把描述類的Class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被直接使用的Java類型,這就是虛擬機的類加載機制java

在java的語言裏,類的加載,連接,初始化,都是在運行期間進行的,這樣雖然會增長一些性能開銷,可是會讓Java程序更加的靈活,Java天生能夠動態擴展語言的特性就是依賴運行期動態加載和動態連接來實現的安全

類的加載時機

類被加載到虛擬機內存開始,到卸載出內存爲止,他的整個生命週期包括,加載,驗證,準備,解析,初始化,使用,和卸載七個階段,其中驗證,準備,解析3個部分統稱爲鏈接網絡

其中加載,驗證,準備,初始化,卸載這5個階段的開始順序是肯定的,類加載過程必須按照這種順序循序漸進的開始,而解析則不肯定,他某些狀況下能夠在初始化以後開始,注意這裏咱們說的是開始,而不是進行或完成,強調這一點是由於這些階段是交叉混合的完成,一般在一個階段進行過程當中激活另外一個階段數據結構

類的加載過程

下面咱們全面瞭解一下類加載的全過程性能

加載

加載類加載的一個階段,不要混淆這倆個概念spa

加載階段主要完成了下面三個事情代理

  • 經過一個類的全限定名獲取定義此類的二進制字節流
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區運行時數據結構
  • 在內存中生成一個java.lang.class對象,做爲方法區這個類的各個數據的訪問入口

這三個定義不算具體,第一個經過一個類的全限定名獲取定義此類的二進制字節流,沒有指定從哪裏獲取,怎樣獲取,這樣就給了開發人員很大的靈活性,好比code

  • 咱們能夠從ZIP中獲取,好比jar
  • 咱們能夠從網絡中獲取
  • 運行時計算生成,這種場景運用最多的是動態代理技術
  • ...

加載階段完成後,虛擬機外部的二進制字節流,就按照虛擬機所需的格式存入到了方法區之中,而後實例化一個java.lang.class對象,雖然他是對象,可是他存在方法區裏面cdn

驗證

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流包含的信息是否符合當前虛擬機的要求,而且不會危害虛擬機的安全對象

準備

準備階段是正式爲類變量(static變量)分配內存並設置初始值的階段,這些變量所使用的內存,將在方法區分配,這裏咱們重點說一下初始值,好比下面這個變量

public static int value=123;
複製代碼

這個階段爲他賦值爲0,而不是123,由於這時還沒有執行任何java方法,把變量賦值爲123,是在初始化階段纔會執行,下面列出,java基本數據類型的零值

解析

解析階段是虛擬機把常量池內的符號替換爲直接引用的過程

初始化

初始化是類加載的最後一步,前面的類加載過程當中,除了加載階段用戶能夠控制之外,其他動做都由虛擬機主導,到了初始化階段,纔是真正執行類中定義的java程序代碼

關於類加載的初始化階段,在虛擬機規範嚴格規定了有且只有5種場景必須對類進行初始化:

  • 使用new關鍵字實例化對象時,讀取或者設置一個靜態字段(不包括編譯期常量),以及調用靜態方法的時候,必須觸發類加載的初始化過程(類加載的最終階段)
  • 使用反射對類進行調用的時候,若是類還沒初始化,那麼就要初始化
  • 當初始化一個類的時候,若是其父類還沒初始化,那麼則先觸發父類的初始化
  • 當java虛擬機啓動時,用戶須要制定一個要執行的主類(包含main方法的類),虛擬機會先初始化這個主類
  • 當使用JDK 1.7 的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後解析結果爲REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄對應類沒有初始化時,必須觸發其初始化(這點看不懂就算了,這是1.7的新增的動態語言支持,其關鍵特徵是它的類型檢查的主體過程是在運行期而不是編譯期進行的,這是一個比較大點的話題,這裏暫且打住)

類與類加載器

虛擬機把類加載階段的經過一個類的全限定名獲取定義此類的二進制字節流,這個動做放到java虛擬機外部去實現,以便讓用戶來決定如何去獲取須要的類。實現這個動做的代碼塊叫作類加載器

類加載器雖然最用於類的加載階段,可是他在java程序起到的做用不限類的加載階段,好比,如何判斷倆個類相等,只有倆個類是被同一個加載器加載的前提下才有意義,不然即便倆個類源於同一個Class文件,被同一個虛擬機加載,只要加載他們的類加載器不一樣,倆個類一定不相等

這裏說的相等,包括類的Class對象equals方法,也包括Instance of 關鍵字判斷

雙親委派模型

從java虛擬機的角度來說,只存在倆種不一樣的類加載器

  • 一種是啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++實現,是虛擬機自身的一本分,
  • 另外一種是全部其餘的類加載器,這些類加載器由java語言實現,獨立於虛擬機外部,而且所有繼承自java.lang.ClassLoader

從java開發人員來看,類加載器還能夠劃分更爲細緻一些,絕大部分都會用到如下3種

Bootstrap ClassLoader(啓動列加載器)

這個上面已經介紹過了,這個類加載器負責將放在<JAVA_HOME>\lib目錄中的或者被-Xbootclasspath參數所指定路徑中的,而且被虛擬機識別的類庫加載到虛擬機內存中

Java虛擬機啓動就是經過啓動類加載器建立一個初始類完成的,因爲這個加載器是C++實現的,因此該加載器不能被java代碼訪問

Extension ClassLoader(擴展類加載器)

他負責加載<JAVA_HOME>\lib\ext目錄中的文件,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器

Application ClassLoader(應用程序類加載器)

因爲這個類類加載器是,ClassLoad中的getSystemClassLoader方法的返回值,因此又稱爲系統類加載器,他負責加載用戶路徑(classpath)所指定的類庫,開發者能夠直接使用,若是開發者沒有自定義ClassLoader,這個就是程序的默認類加載器

下圖中展現的類加載器的層次關係,被稱爲雙親委派模型,雙親委派模型要求除了頂層的啓動類加載器外,其餘應當有本身的父加載器,這裏的加載器如父子關係通常,不是以繼承實現,而是以組合實現

雙親委派模型的工做過程

若是一個加載器收到了類加載的請求,他首先不會本身嘗試加載這個類,而是把這個請求委派給父類加載器完成,每個層次的加載器都是如此,所以全部的加載請求,都應該傳入到頂層的啓動加載器中,只有父類加載器反饋,沒法完成這個加載請求,自加載器纔會嘗試本身去加載

雙親委託模型的好處

  • 避免重複加載,若是已經加載,就不須要再次加載
  • 安全,若是你定義String類來代替系統的String類,這樣會致使風險,可是在雙親委託模型中,String類在java虛擬機啓動時就被加載了,你自定義的String類是不會被加載的

雙親委託模型的實現源碼

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{
            // 首先檢查類是否被加載過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    //若是父類拋出ClassNotFoundException異常
                    //則說明父類不能加載該類
                }

                if (c == null) {
                    //若是父類沒法加載,則調用自身的findClass進行加
                    c = findClass(name);
                }
            }
            return c;
    }

複製代碼

上方邏輯很清楚,首先檢查類是否被加載過,若是沒有被加載過,就調用父類的加載器加載,若是父類加載器爲空就調用啓動加載器加載,若是父類加載失敗,就調用本身的findClass加載

參考:《深刻理解Java虛擬機》

相關文章
相關標籤/搜索