java類加載機制

1.總覽

以下圖所示(圖片來源於網路),JVM類加載機制分爲五個部分:加載,驗證,準備,解析,初始化,下面咱們就分別來看一下這五個過程。html

2.加載

若是一個類或者接口 C 不是數組類,那麼 C 使用 類加載器 加載二進制表示,因爲數組類不具備外部二進制表示形式,它們由 Java 虛擬機建立,而不是由類加載器建立。java

加載是類加載過程當中的一個階段,這個階段會在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的入口。注意這裏不必定非得要從一個Class文件獲取,這裏既能夠從ZIP包中讀取(好比從jar包和war包中讀取),也能夠在運行時計算生成(動態代理),也能夠由其它文件生成(好比將JSP文件轉換成對應的Class類)。數據庫

所作的3件事:數組

  • 經過一個類的全限定名來獲取定義此類的二進制字節流(並無指明要從一個Class文件中獲取,能夠從其餘渠道,譬如:網絡、動態生成、數據庫等);
  • 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
  • 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口;

2.1安全

3.驗證

驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
驗證階段大體會完成4個階段的檢驗動做:網絡

  • 文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
  • 元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。
  • 字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
  • 符號引用驗證:確保解析動做能正確執行。

驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間數據結構

 

4.準備

準備階段是正式爲類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這裏所說的初始值概念,好比一個類變量定義爲:函數

public static int v = 8080;

實際上變量v在準備階段事後的初始值爲0而不是8080,將v賦值爲8080的putstatic指令是程序被編譯後,存放於類構造器<client>方法之中,這裏咱們後面會解釋。
可是注意若是聲明爲:佈局

public static final int v = 8080;

在編譯階段會爲v生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將v賦值爲8080。this

5.解析

解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程。符號引用就是class文件中的:

  • CONSTANT_Class_info
  • CONSTANT_Field_info
  • CONSTANT_Method_info

等類型的常量。

下面咱們解釋一下符號引用和直接引用的概念:

  • 符號引用與虛擬機實現的佈局無關,引用的目標並不必定要已經加載到內存中。各類虛擬機實現的內存佈局能夠各不相同,可是它們能接受的符號引用必須是一致的,由於符號引用的字面量形式明肯定義在Java虛擬機規範的Class文件格式中。
  • 直接引用能夠是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。若是有了直接引用,那引用的目標一定已經在內存中存在。

6.初始化

初始化階段是類加載最後一個階段,前面的類加載階段以後,除了在加載階段能夠自定義類加載器之外,其它操做都由JVM主導。到了初始階段,纔開始真正執行類中定義的Java程序代碼。

初始化階段是執行類構造器<clinit>方法的過程。<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。

p.s: 若是一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器能夠不爲這個類生成<clinit>()方法。

<clinit>()方法與實例構造器<init>()方法不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢.

注意如下幾種狀況不會執行類初始化:

  • 經過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
  • 定義對象數組,不會觸發該類的初始化。
  • 常量在編譯期間會存入調用類的常量池中,本質上並無直接引用定義常量的類,不會觸發定義常量所在的類。
  • 經過類名獲取Class對象,不會觸發類的初始化。
  • 經過Class.forName加載指定類時,若是指定參數initialize爲false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
  • 經過ClassLoader默認的loadClass方法,也不會觸發初始化動做。

6.1 類加載器

虛擬機設計團隊把加載動做放到JVM外部實現,以便讓應用程序決定如何獲取所需的類,JVM提供了3種類加載器:

  • 啓動類加載器(Bootstrap ClassLoader):負責加載 JAVA_HOME\lib 目錄中的,或經過-Xbootclasspath參數指定路徑中的,且被虛擬機承認(按文件名識別,如rt.jar)的類。
  • 擴展類加載器(Extension ClassLoader):負責加載 JAVA_HOME\lib\ext 目錄中的,或經過java.ext.dirs系統變量指定路徑中的類庫。
  • 應用程序類加載器(Application ClassLoader):負責加載用戶路徑(classpath)上的類庫。

JVM經過 雙親委派模型  和 沙箱機制 進行類的加載,固然咱們也能夠經過繼承java.lang.ClassLoader實現自定義的類加載器。

當一個類加載器收到類加載任務,會先交給其父類加載器去完成,所以最終加載任務都會傳遞到頂層的啓動類加載器,只有當父類加載器沒法完成加載任務時,纔會嘗試執行加載任務。

採用雙親委派的一個好處是好比加載位於rt.jar包中的類java.lang.Object,不論是哪一個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不一樣的類加載器最終獲得的都是一樣一個Object對象。

在有些情境中可能會出現要咱們本身來實現一個類加載器的需求。咱們直接看一下jdk8中的ClassLoader的源碼實現:

  

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

  

  • 首先經過Class c = findLoadedClass(name);判斷一個類是否已經被加載過。
  • 若是沒有被加載過執行if (c == null)中的程序,遵循雙親委派的模型,首先會經過遞歸從父加載器開始找,直到父類加載器是Bootstrap ClassLoader爲止。
  • 最後根據resolve的值,判斷這個class是否須要解析。

而上面的findClass()的實現以下,直接拋出一個異常,而且方法是protected,很明顯這是留給咱們開發者本身去實現的。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

  

7.練習

執行下面的代碼後,輸出什麼呢?

package com.ycit;

/**
 * @author chenxiaolei
 * @date 2019/3/26
 */
public class StaticTest {

    public static void main(String[] args)
    {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static
    {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    StaticTest()
    {
        System.out.println("3");
        System.out.println("a="+a+",b="+b);
    }

    public static void staticFunction(){
        System.out.println("4");
    }

    int a=110;
    static int b =112;

}

  

正確答案:

2
3
a=110,b=0
1
4

  

解析:

按照上面對 類加載過程的解讀,

準備階段,爲類變量賦值,這裏是賦的是默認值,即 st = null,b=0;

初始化階段,是執行類構造器<client>方法的過程。。<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的

  • st = new StaticTest()
  • static 代碼塊
  • b = 112

先 new StaticTest(),對象的初始化是先初始化成員變量再執行構造方法,即,a = 100;> 而後 打印 2,> 而後構造函數中的 打印 3, a=100,b=0;>接着執行static 代碼塊,打印1;> 而後 b 賦值 12;最後執行函數,打印4;

 

通常按照下面的套用規則順序,可是上面的 靜態變量 是對象初始化,結果就不同了。

  • 父類的靜態變量賦值
  • 自身的靜態變量賦值
  • 父類成員變量賦值和父類塊賦值
  • 父類構造函數賦值
  • 自身成員變量賦值和自身塊賦值
  • 自身構造函數賦值
相關文章
相關標籤/搜索