JVM學習筆記一:類加載機制介紹

在代碼編譯後,就會生成JVM(Java虛擬機)可以識別的二進制字節流文件(*.class)。而JVM把Class文件中的類描述數據從文件加載到內存,並對數據進行校驗、轉換解析、初始化,使這些數據最終成爲能夠被JVM直接使用的Java類型,這個說來簡單但實際複雜的過程叫作JVM的類加載機制。java

一、類加載器

先來查看一波代碼數組

package com.black.example.helloworld;

public class JvmTest {

    public static void main(String[] args) {
        ClassLoader classLoader = JvmTest.class.getClassLoader();
        while (classLoader!=null){
            System.out.println(classLoader);
            classLoader = classLoader.getParent();
        }
    }
}

運行結果:安全

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@7530d0a

從上面的結果能夠看出,並無獲取到ExtClassLoader的父Loader,緣由是Bootstrap Loader(引導類加載器)是用C語言實現的,找不到一個肯定的返回父Loader的方式,因而就返回null。數據結構

 二、類加載器的雙親委派/雙親委任模型

當一個類加載器收到一個類加載的請求,它首先會將該請求委派給父類加載器去加載,每個層次的類加載器都是如此,所以全部的類加載請求最終都應該被傳入到頂層的啓動類加載器(Bootstrap ClassLoader)中,只有當父類加載器反饋沒法完成這個列的加載請求時(它的搜索範圍內不存在這個類),子類加載器才嘗試加載。其層次結構示意圖以下:app

加載順序:自頂向下
檢查順序:自底向上學習

雙親委任的好處是:

  • 避免重複加載,父類加載過了,子類就不須要再加載了。
  • 是一種安全機制,解決了各個類加載器的基礎類的統一問題。程序安全是jdk的事,文件安全是系統的事

示例以下:測試

package java.util;
//能編譯,可是不能運行
public class List {

    public static void main(String[] args) {
        System.out.println("我是List");
    }
}

運行結果:this

錯誤: 在類 java.util.List 中找不到 main 方法, 請將 main 方法定義爲:
   public static void main(String[] args)
不然 JavaFX 應用程序類必須擴展javafx.application.Application

雙親委派的底層實現,源代碼以下:

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) {
                    //父類加載器拋出異常,沒法完成類加載請求
                }

                if (c == null) {
                    //父類加載器沒法完成類加載請求時,調用自身的findClass方法來完成類加載
                    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;
        }
    }

三、類加載的三種方式

  • 經過命令行啓動應用時由JVM初始化加載含有main()方法的主類。
  • 經過Class.forName()方法動態加載,會默認執行初始化塊(static{}),可是Class.forName(name,initialize,loader)中的initialze可指定是否要執行初始化塊。
  • 經過ClassLoader.loadClass()方法動態加載,不會執行初始化塊。

四、自定義類加載器的兩種方式 

一、遵照雙親委派模型:繼承ClassLoader,重寫findClass()方法。 
二、破壞雙親委派模型:繼承ClassLoader,重寫loadClass()方法。 spa

一般咱們推薦採用第一種方法自定義類加載器,最大程度上的遵照雙親委派模型。 .net

五、JAVA類的聲明週期

5.1 加載

加載過程主要完成三件事情:

  1. 經過類的全限定名來獲取定義此類的二進制字節流
  2. 將這個類字節流表明的靜態存儲結構轉爲方法區的運行時數據結構
  3. 在堆中生成一個表明此類的java.lang.Class對象,做爲訪問方法區這些數據結構的入口。

這個過程主要就是類加載器完成。(對於HotSpot虛擬而言,Class對象較爲特殊,其被放置在方法區而不是堆中)

5.2 連接之驗證

此階段主要確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機的自身安全。主要包括如下四個階段:

  1. 文件格式驗證:基於字節流驗證,驗證字節流符合當前的Class文件格式的規範,能被當前虛擬機處理。驗證經過後,字節流纔會進入內存的方法區進行存儲。好比版本號不一致的問題

        

  1. 元數據驗證:基於方法區的存儲結構驗證,對字節碼進行語義驗證,確保不存在不符合java語言規範的元數據信息。
  2. 字節碼驗證:基於方法區的存儲結構驗證,經過對數據流和控制流的分析,保證被檢驗類的方法在運行時不會作出危害虛擬機的動做。
  3. 符號引用驗證:基於方法區的存儲結構驗證,發生在解析階段,確保可以將符號引用成功的解析爲直接引用,其目的是確保解析動做正常執行。換句話說就是對類自身之外的信息進行匹配性校驗。

5.2 連接之準備

準備要執行的指定類,準備階段爲變量分配內存並設置靜態變量初始化

爲類的靜態變量(static filed)在方法區分配內存,並賦默認初值(0值或null值)。如static int a = 100;

靜態變量a就會在準備階段被賦默認值0。

對於通常的成員變量是在類實例化時候,隨對象一塊兒分配在堆內存中。

另外,靜態常量(static final filed)會在準備階段賦程序設定的初值,如static final int a = 666;  靜態常量a就會在準備階段被直接賦值爲666,對於靜態變量,這個操做是在初始化階段進行的。

5.2 解析:將類的二進制數據中的符號引用換爲直接引用。

5.3 初始化

類初始化是類加載的最後一步,除了加載階段,用戶能夠經過自定義的類加載器參與,其餘階段都徹底由虛擬機主導和控制。到了初始化階段才真正執行Java代碼。
類的初始化的主要工做是爲靜態變量賦程序設定的初值。
如static int a = 100;在準備階段,a被賦默認值0,在初始化階段就會被賦值爲100。

Java虛擬機規範中嚴格規定了有且只有五種狀況必須對類進行初始化:
一、使用new字節碼指令建立類的實例,或者使用getstatic、putstatic讀取或設置一個靜態字段的值(放入常量池中的常量除外),或者調用一個靜態方法的時候,對應類必須進行過初始化。
二、經過java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則要首先進行初始化。
三、當初始化一個類的時候,若是發現其父類沒有進行過初始化,則首先觸發父類初始化。
四、當虛擬機啓動時,用戶須要指定一個主類(包含main()方法的類),虛擬機會首先初始化這個類。
五、使用jdk1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、       RE_invokeStatic的方法句柄,而且這個方法句柄對應的類沒有進行初始化,則須要先觸發其初始化。

注意,虛擬機規範使用了「有且只有」這個詞描述,這五種狀況被稱爲「主動引用」,除了這五種狀況,全部其餘的類引用方式都不會觸發類初始化,被稱爲「被動引用」。

 

被動引用的例子一:

經過子類引用父類的靜態字段,對於父類屬於「主動引用」的第一種狀況,對於子類,沒有符合「主動引用」的狀況,故子類不會進行初始化。代碼以下:

//父類
public class SuperClass {
	//靜態變量value
	public static int value = 666;
	//靜態塊,父類初始化時會調用
	static{
		System.out.println("父類初始化!");
	}
}
//子類
public class SubClass extends SuperClass{
	//靜態塊,子類初始化時會調用
	static{
		System.out.println("子類初始化!");
	}
}
//主類、測試類
public class NotInit {
	public static void main(String[] args){
		System.out.println(SubClass.value);
	}
}


//####輸出結果:
//####父類初始化!
//####666

被動引用的例子之二:

經過數組來引用類,不會觸發類的初始化,由於是數組new,而類沒有被new,因此沒有觸發任何「主動引用」條款,屬於「被動引用」。代碼以下:

//主類、測試類
class NotInit {
	public static void main(String[] args){
		SuperClass[] test = new SuperClass[10];
	}
}


輸出結果:沒有任何輸出

被動引用的例子之三:

剛剛講解時也提到,靜態常量在編譯階段就會被存入調用類的常量池中,不會引用到定義常量的類,這是一個特例,須要特別記憶,不會觸發類的初始化!

package com.black.example.helloworld;

//常量類
public class ConstClass {
    static{
		System.out.println("常量類初始化!");
	}
	
	public static final String HELLOWORLD = "hello world!";
}
 
//主類、測試類
 class NotInit1 {
	public static void main(String[] args) {
		System.out.println(ConstClass.HELLOWORLD);
	}
}


輸出結果:hello world!

 

 

下一篇:JVM學習筆記二:內存結構規範 

相關文章
相關標籤/搜索