JVM 類加載/類加載器

程序的class文件是程序編譯的產物,
虛擬機把描述類的數據從Class文件加載到內存中,並進行數據校驗,轉換解析和初始化,最終形成可以直接被虛擬機使用的Java類型------這叫虛擬機的類加載機制。
一些語言在編譯的時候需要進行連接工作,但是Java中,類型的加載,連接和初始化都是在程序運行期間做的。

類的生命週期
類從被加載到虛擬機開始到卸載出內存爲止,整個生命週期:
加載,(驗證,準備,解析),初始化,使用,卸載。其中驗證,準備,解析叫做連接。
這個七個階段除了解析之外其他的階段都是按照這個順序執行的而解析在某些情況下是可以在初始化之後開始。


PART ONE:類的加載

1.加載★

弄清楚概念:加載是類加載過程的一個階段

幹什麼:

  1. 通過一個類的全限定名來獲取定義此類的而進制字節流
  2. 將這個字節流代表的靜態存儲結構轉化爲方法去的運行時數據結構,將class文件中的數據加載到方法區的運行時常量池
  3. 在內存中生成一個代表這個類的java.lang.Class 對象。作爲方法區這個類各種數據訪問的入口

關於加載的時機,JVM對此並沒有強制性的限制,同時對於如何獲取二進制字節流也沒有明確的規定
細節:

  1. 對於一個非數組類的加載階段(準確的是獲取二進制字節流的階段),可以使用系統提供的引導類加載器完成,也可以使用自定義的類加載器完成。
  2. 數組類本身不是通過類加載器創建的,是有JVM 直接創建的。但是數組類仍然和類加載器聯繫緊密
    1. 數組類的組建類型是引用類型,還是要使用類加載器去加載組件類型
    2. 如果數組的組件類型是一般數據類型(int[]),JVM 會把該數組標記爲與引導類加載器相關聯
    3. 數組類和組件類的可見性是一致的

2.驗證

爲了確保Class文件的字節流包含的信息符合當前虛擬機的要求(Class文件不一定由Java源碼編譯而來,所以要進行檢查)

  1. JVM規範校驗。JVM 會對字節流進行文件格式校驗,判斷其是否符合 JVM 規範,是否能被當前版本的虛擬機處理。例如:文件是否是以 0x cafe bene開頭,主次版本號是否在當前虛擬機處理範圍之內等。
  2. 代碼邏輯校驗。JVM 會對代碼組成的數據流和控制流進行校驗,確保 JVM 運行該字節碼文件後不會出現致命錯誤。例如一個方法要求傳入 int 類型的參數,但是使用它的時候卻傳入了一個 String 類型的參數。一個方法要求返回 String 類型的結果,但是最後卻沒有返回結果。代碼中引用了一個名爲 Apple 的類,但是你實際上卻沒有定義 Apple 類。

當代碼數據被加載到內存中後,虛擬機就會對代碼數據進行校驗,看看這份代碼是不是真的按照JVM規範去寫的。

3.準備★

正式爲類變量分配內存並設置類變量初始值

類變量:staitic修飾(沒有被final 修飾)
實例變量:在對象實例化的時候隨對象一起分配到Java堆中

public static int a = 123; 
public static final int b =1245;
/**
*準備階段之後a=0;b=1245
*/

4.解析

在解析階段,JVM針對類或者接口,字段,方法,類方法,接口方法,方法類型,方法句柄和調用下寧府7類引用進行解析。將常量池中的符號引用替換成其在內存中的直接內存

1.符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字>面量,符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經在內存中。
2.直接引用(Direct Reference):直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般都不相同,如果有了直接引用,那引用的目標必定已經在內存中存在。

字段,類方法,接口方法都是要先解析所屬的類,如果類中有相符合的內容則直接返回直接引用

5.初始化★★

幹什麼:
類加載的幾個階段,除了加載階段可以有自定義的類加載器參與,其他階段都由JVM主導控制。到了初始化階段,才真正的執行另類中定義的Java程序代碼, 會根據語句執行順序對類進行初始化
在準備階段,JVM已經爲變量(static)賦過一次系統希望的初始值,而在初始化階段,則是根據程序員制定的程序去初始化變量和其他資源

public static int a=123;
public int b ;
/*在準備階段a=0;
*初始化之後 a=123;
*初始化之後 b = 0;
*/

換一個角度:類的初始化過程就是執行類構造器< clinit >() 方法的過程
類構造器< clinit >() 方法 :是由編譯器自動收集類中所有類變量的所有賦值動作和靜態語句塊合併產生

靜態語句只能訪問定義在靜態語句塊之前的靜態變量,聲明在靜態語句塊之後的靜態變量,靜態語句塊只能賦值,不能訪問

分清楚< clinit >() 是類的構造器與類的構造函數(實例構造器< init >())是不一樣的,當前類在執行< clinit >(),默認父類已經執行完了
< clinit >不是必須的,沒有靜態代碼塊和對類變量的賦值語句也行
時機:
虛擬機對什麼情況下進行類的初始化是有嚴格規定的

  1. 使用new ,getstatic,putstatic,invokrstatic 這幾個字節碼指令的時候,如果該類沒有進行初始化,那麼就要先進行初始化。當new 一個該類對象,設置或者獲取該類的靜態字段(除了final 修飾的字段),以及調用一個類的靜態方法的時候
  2. 使用java.lang.reflect 的方法對類進行反射操作的時候
  3. 當初始化一個類時,如果他的父類沒有初始化那麼要先初始化他的父類
  4. 當虛擬機啓動的時候,用戶指定的一個要執行的類(包含main() 方法的類)虛擬機會先初始化這個類。

有且只有出現這幾種情況的時候會初始化類,其他任何方式都不行
例如:

  1. 通過子類訪問父類的靜態字段,不會初始化子類,只會初始化父類。
  2. 通過數組定義來引用該類,不會觸發該類的初始化。
people[] peoples = new people[10];
/**
*這樣並不能觸發people這個類的初始化,而會觸發[people  這個類的初始化,這是有虛擬機自己生成的類繼承於Object,
*這個類*代表了一個元素類型爲people 的數組,數組使用的clone /length 方法都定義在裏面。這是Java中對於數組訪問更
*加安全的訪問模式的實現---封裝
*/
  1. 常量(final)在編譯階段就會存入調用類的常量池中,所以本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量類的初始化
  2. 關於接口:
    接口和類大致是一樣的,但是對於第三條,一個類在初始化時要求他的父類都已完成初始化,但是對於接口並不這樣要求;當接口初始化時,他的接口可以不用初始化,當使用接口的時候再初始化也行。

PART TWO:類加載器

在加載階段“通過一個類的全限定名來獲取描述此類的二進制字節流”。實現這個過程的模塊代碼叫類加載器
對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。
比較兩個類是否“相等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相等。
這裏所指的“相等”,包括代表類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果

JVM中的類加載器體制
JVM中只有兩種不同的類加載器:

  1. 啓動類加載器:是有C++編寫,是虛擬機的一部分
  2. 其他類加載器:由Java語言實現,獨立於虛擬機外部都繼承Java.lang.ClassLoader

體制結構:

  1. 啓動類加載器(BootStrap ClassLoader): 啓動ClassLoader< JAVA_HOME>\lib 目錄,-Xbootclasspath,rt.jar 類庫到內存中
  2. 擴展類加載器(Extension ClassLoader):擴展ClassLoader < JAVA_HOME>\lib\ext 目錄
  3. 應用類加載器(Application ClassLoader):應用ClassLoader
  4. 用戶類加載器(User ClassLoader):自定義加載器ClassLoader
    在這裏插入圖片描述

雙親委派模式
除了頂層的BootStrap Class Loader 其他類加載器都有自己的父類加載器,這裏的父子關係不是通過繼承實現的而是通過組合關係來複用父加載器中的代碼。
雙親委派的工作模式:
當一個類加載器收到了加載請求他首先不會自己去嘗試加載,而是 會把這個加載請求委派給父類加載器去完成,所以最後所有的請求都會傳到啓動類加載器中,當父類返回自己無法完成加載時(他的搜索範圍內沒有找到所需類),子類纔會自己去嘗試完成加載。

雙親委派模式的好處
Java類隨類加載器有一個很好的層級關係,例如:加載java.lang.Object 因爲加載請求都會傳遞給啓動加載器,因此就保證類始終只有一個java.lang.Object ,不會因爲不同的類加載器加載這個類就會出現多個java.lang.Object ,這樣程序就亂套了。

/*
*雙親委派模式的實現代碼:java.lang.ClassLoader  的loadClass 方法*/
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先檢查該類是否已經加載
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 如果父類加載器中沒有加載這個類
                    // 再調用本類的加載器加載
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章