類加載機制:類加載過程

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。它們開始的順序以下圖所示:
 

 

其中類加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發生的順序是肯定的,而解析階段則不必定,它在某些狀況下能夠在初始化階段以後開始,這是爲了支持  Java 語言的運行時綁定(也成爲動態綁定或晚期綁定)。另外注意這裏的幾個階段是按順序開始,而不是按順序進行或完成,由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段。(例如:加載階段的同時會交叉使用到驗證階段的步驟)
下面詳細講述類加載過程當中每一個階段所作的工做:
 
 
三大階段:加載,鏈接,初始化
 
加載
加載時類加載過程的第一個階段,在加載階段,虛擬機須要完成如下三件事情:
  • 1.經過一個類的全限定名來獲取其定義的二進制字節流。
  • 2.將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
  • 3.在 Java 堆中生成一個表明這個類的 java.lang.Class 對象,做爲對方法區中這些數據的訪問入口。
相對於類加載的其餘階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動做)是可控性最強的階段,由於開發人員既可使  用系統提供的類加載器來完成加載,也能夠自定義本身的類加載器來完成加載。
加載階段完成後,虛擬機外部的  二進制字節流就按照虛擬機所需的格式存儲在方法區之中,並且在 Java 堆中也建立一個 java.lang.Class 類的對象,這樣即可以經過該對象訪問方法區中的這些數據。
說到加載,不得不提到類加載器,下面就具體講述下類加載器。
站在  Java 虛擬機的角度來說,只存在兩種不一樣的類加載器:
  • 啓動類加載器:它使用 C++ 實現(這裏僅限於 Hotspot,也就是 JDK1.5 以後默認的虛擬機,有不少其餘的虛擬機是用 Java 語言實現的),是虛擬機自身的一部分。
  • 全部其餘的類加載器:這些類加載器都由 Java 語言實現,獨立於虛擬機以外,而且所有繼承自抽象類 java.lang.ClassLoader,這些類加載器須要由啓動類加載器加載到內存中以後才能去加載其餘的類。
站在  Java 開發人員的角度來看,類加載器能夠大體劃分爲如下三類:
  • 啓動類加載器:Bootstrap ClassLoader,它負責加載存放在JDK\jre\lib(JDK 表明 JDK 的安裝目錄,下同)下的jar,(如 rt.jar,全部的java.*開頭的類均被 Bootstrap ClassLoader 加載)。啓動類加載器是沒法被 Java 程序直接引用的。
  • 擴展類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載JDK\jre\lib\ext目錄中,或者由 java.ext.dirs 系統變量指定的路徑中的全部類庫(如javax.*開頭的類),開發者能夠直接使用擴展類加載器。
  • 應用程序類加載器:Application ClassLoader,該類加載器由 sun.misc.Launcher$AppClassLoader 來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者能夠直接使用該類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
這幾種類加載器的層次關係以下圖所示:

 

這種層次關係稱爲類加載器的雙親委派模型。咱們把每一層上面的類加載器叫作當前層類加載器的父加載器,固然,它們之間的父子關係並非經過繼承關係來實現的,而是使用組合關係來複用父加載器中的代碼。該模型在  JDK1.2 期間被引入並普遍應用於以後幾乎全部的 Java 程序中,但它並非一個強制性的約束模型,而是 Java 設計者們推薦給開發者的一種類的加載器實現方式。
雙親委派模型的工做流程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。
使用雙親委派模型來組織類加載器之間的關係,有一個很明顯的好處,就是  Java 類隨着它的類加載器(說白了,就是它所在的目錄)一塊兒具有了一種帶有優先級的層次關係,這對於保證 Java 程序的穩定運做很重要。例如,類java.lang.Object 類存放在JDK\jre\lib下的 rt.jar 之中,所以不管是哪一個類加載器要加載此類,最終都會委派給啓動類加載器進行加載,這邊保證了 Object 類在程序中的各類類加載器中都是同一個類。
驗證
驗證的目的是爲了確保  Class 文件中的字節流包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。不一樣的虛擬機對類驗證的實現可能會有所不一樣,但大體都會完成如下四個階段的驗證:文件格式的驗證、元數據的驗證、字節碼驗證和符號引用驗證。
  • 文件格式的驗證:驗證字節流是否符合 Class 文件格式的規範,而且能被當前版本的虛擬機處理,該驗證的主要目的是保證輸入的字節流能正確地解析並存儲於方法區以內。通過該階段的驗證後,字節流纔會進入內存的方法區中進行存儲,後面的三個驗證都是基於方法區的存儲結構進行的。
  • 元數據驗證:對類的元數據信息進行語義校驗(其實就是對類中的各數據類型進行語法校驗),保證不存在不符合 Java 語法規範的元數據信息。
  • 字節碼驗證:該階段驗證的主要工做是進行數據流和控制流分析,對類的方法體進行校驗分析,以保證被校驗的類的方法在運行時不會作出危害虛擬機安全的行爲。
  • 符號引用驗證:這是最後一個階段的驗證,它發生在虛擬機將符號引用轉化爲直接引用的時候(解析階段中發生該轉化,後面會有講解),主要是對類自身之外的信息(常量池中的各類符號引用)進行匹配性的校驗。
 
 
準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有如下幾點須要注意:
  • 這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在 Java 堆中。
  • 這裏所設置的初始值一般狀況下是數據類型默認的零值(如 0、0L、null、false 等),而不是被在 Java 代碼中被顯式地賦予的值。
假設一個類變量的定義爲:
public static int value = 3;
那麼變量  value 在準備階段事後的初始值爲 0,而不是 3,由於這時候還沒有開始執行任何 Java 方法,而把 value 賦值爲 3 的 putstatic 指令是在程序編譯後,存放於類初始化方法 <clinit>中(<clinit>方法是Java中間語言的一個類初始化方法,它是在初始化階段才被JVM調用),因此把 value 賦值爲 3 的動做將在初始化階段纔會執行。注意:並不是全部的類都會擁有一個 <clinit>方法,在如下條件中該類不會擁有 <clinit>方法:
  • 該類既沒有聲明任何類變量,也沒有靜態初始化語句;
  • 該類聲明瞭類變量,但沒有明確使用類變量初始化語句或靜態初始化語句初始化;
  • 該類僅包含靜態 final 變量的類變量初始化語句,而且類變量初始化語句是編譯時常量表達式。
(補充說明:在反編譯的間語言中,咱們還會發現有<init>方法,這個是對象的初始化方法。它是在對象實例化的時候才被JVM調用)
下表列出了  Java 中全部基本數據類型以及 reference 類型的默認零值:

 

這裏還須要注意以下幾點:
  • 對基本數據類型來講,對於類變量(static)和全局變量,若是不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來講,在使用前必須顯式地爲其賦值,不然編譯時不經過。
  • 對於同時被 static 和 final 修飾的常量,必須在聲明的時候就爲其顯式地賦值,不然編譯時不經過;而只被 final 修飾的常量則既能夠在聲明時顯式地爲其賦值,也能夠在類初始化時顯式地爲其賦值,總之,在使用前必須爲其顯式地賦值,系統不會爲其賦予默認零值。
  • 對於引用數據類型 reference 來講,如數組引用、對象引用等,若是沒有對其進行顯式地賦值而直接使用,系統都會爲其賦予默認的零值,即null。
  • 若是在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值。
假設上面的類變量  value 被定義爲:
public static final int value = 3;
編譯時  Javac 將會爲 value 生成 ConstantValue 屬性,在準備階段虛擬機就會根據 ConstantValue 的設置將 value 賦值爲 3。
解析
解析階段是虛擬機將常量池中的符號引用轉化爲直接引用的過程。解析階段可能開始於初始化階段以前,也可能在初始化階段以後開始,虛擬機會根據須要來判斷,究竟是在類被加載器加載時就對常量池中的符號引用進行解析(初始化以前),仍是等到一個符號引用將要被使用前纔去解析它(初始化以後)。咱們也能夠經過ClassLoader.loadClass(String name, boolean resolve)方法的第二個參數決定是否須要進行解析。
初始化
初始化是類加載過程的最後一步,到了此階段,才真正開始執行類中定義的  Java 程序代碼。在準備階段,類變量已經被賦過一次系統要求的初始值,而在初始化階段,則是根據程序員經過程序指定的主觀計劃去初始化類變量和其餘資源,或者能夠從另外一個角度來表達:初始化階段是執行類初始化方法<clinit>的過程。
相關文章
相關標籤/搜索