JVM類加載總結

JVM類加載總結

1、概述

類加載的過程,就是將類的字節碼裝載到內存方法區的過程(方法區的相關知識參看Java內存模型)。

與C語言這樣需要在運行前就進行鏈接(Link)的語言不通,Java語言中類型的加載、鏈接、初始化都是在程序運行期間完成的。

這種策略爲Java應用程序提高了極大的動態靈活性。

Java虛擬機(JVM)中用來完成類加載的具體實現,是類加載器。

類加載器讀取.class字節碼文件將其轉換成java.lang.Class類的一個實例。每個實例用來表示一個java類。通過該實例的newInstance()方法可以創建出一個該類的對象。

(我們通常會說方法區中存的是類,實際上存的也是實例,只不過是特殊實例,是Class這個類的實例)

類的全生命週期如下
類的全生命週期

2、類加載流程

類加載的流程主要分爲加載、鏈接、初始化三個階段。其中鏈接又細分爲驗證、準備、解析三個階段。

2-1、加載(Loading)

加載階段jvm主要做三件事

  1. 通過類的“全限定名”獲取量二進制字節流

    這裏會說成是“二進制字節流”,是因爲class文件的來源非常廣。除了最常見的jar、war文件,還可以從網絡獲取,可以運行時凍土工程,由其他文件生成(比如jsp),甚至直接從數據庫讀取。

  2. 將字節流變成方法區的運行時數據結構

  3. 生成代表這個類的java.lang.Class對象(這個對象也放在方法區中),作爲方法區中其對應的類型數據的訪問入口

2-2、驗證(Verification)

保證讀入的class文件流是合法的——符合當前jvm版本的要求,更重要的是不會危及jvm安全。

畢竟java編譯並不是class文件的唯一來源,而且class文件也是很容易篡改的。

2-3、準備(Preparation)

在方法區中,爲類變量分配內存並分配初始值。

注意這裏的初始值指的是“零值”,比如數值爲各種0(0、0L、0.0f),String爲null。

比如

public static int classint = 123;

那麼在這一階段 classint 的值爲0。因爲現在還沒有執行任何java方法,賦值123這個動作是在類構造器的()方法中的。

唯一的特例:

public static final int classint = 123;

使用final修飾的變量實際上就是常量(ConstantValue屬性),其賦值與java方法無關,在準備階段會直接賦值。

2-4、解析(Resolution)

將常量池中的“符號引用”替換爲“直接引用”的過程。

  • 符號引用:以一組符號來描述所引用的目標。與虛擬機當前內存狀況無關,需要引用的目標未必已經加載。
  • 直接引用:直接指向目標的指針、相對偏移量或者是間接定位用的句柄。

A:“班費交給誰?”

B:“交給班長”

A:“誰是班長,坐在哪兒?”

B:“我也不知道,反正我知道要交給班長” —— 符號引用

C:“我知道,他坐在第二排靠窗的座位” —— 直接引用

2-5、初始化(Initialization)

初始化,就是執行類構造器()方法的過程。

所謂的()方法,是編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{})中的語句合併而成的。順序爲該語句在源文件中的順序。

同時,虛擬機會保證首先執行父類的()方法,然後纔是子類的()。
所以最先執行的是Object的();
並且父類的靜態代碼塊一定先於子類被執行。

一個很重要的特性(其實是面試時常問):一個類只會被初始化一次。

3、類的主動引用(何時觸發類的初始化)

以外幾種場景(動作)被稱爲類的主動引用

  • 最常見的場景:使用new關鍵字實例化對象
  • 讀取或者爲一個類的靜態變量賦值(但是final修飾的除外)
  • 調用一個類的靜態方法
  • 使用反射對類進行引用
  • 初始化一個類時,如果其父類還沒有被初始化(複習一下上一節的知識哈),則先對父類進行類加載
  • 虛擬機啓動時,會先初始化包含main()方法的那個類(主類)

接口比較特殊:子接口初始化時,並不要求父接口完成初始化。

4、類的被動引用(何時不觸發類的初始化)

  • 通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化(只有真正聲明這個靜態變量的類纔會被初始化)
  • 通過數組定義類,不會觸發此類的初始化 A[] a = new A[10];
  • 編譯期間存入常量池的常量,不會觸發定義常量所在的類(本質上並沒有直接引用定義常量的類)。
  • 通過Class.forName加載指定類時,如果指定參數initialize爲false時,也不會觸發類初始化(這個參數是告訴虛擬機,是否要對類進行初始化)
  • ClassLoader默認的loadClass方法,也不會觸發初始化動作。
  • 引用final修飾的常量不會觸發此類的初始化(常量在編譯階段存入的常量池)

5、類加載器

最開始就提過,jvm中用來完成類加載的具體實現,就是類加載器。類加載器讀取.class字節碼文件將其轉換成java.lang.Class類的一個實例。

在jvm中,任意一個類,都是通過 “加載它的類加載器” + “類本身” 來確定其唯一性的。

也就是說,即使對於同一個類,被不同的類加載器加載過兩次,對於jvm來說也是不相等的。

java中的類加載器有以下幾種:

5-1、啓動類加載器 Bootstrap ClassLoader

使用 C++編寫的。(其他類加載器都是使用Java編寫,繼承自 java.lang.ClassLoader)

負責加載:

存放在<JAVA_HOME>\lib目錄下的,或者被 -Xbootclasspath 參數指定的路徑中的,

並且是虛擬機識別的類庫(僅按照文件名識別,比如rt.java,名字不符合的類庫即使放在lib目錄中也不被加載)。

5-2、擴展類加載器 Extension ClassLoader

負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量指定路徑中的類庫

5-3、應用程序類加載器 Application ClassLoader

這個類加載器是ClassLoader中getSystemClassLoader()方法的返回值,一般也稱爲系統類加載器。

負責加載用戶類路徑(ClassPath)上指定的所有類庫。

應用程序中如果沒有自定義過自己的類加載器,默認使用這個類加載器。

6、雙親委派模式

6-1、什麼是雙親委派模式

只看UML圖的話,ExtClassLoader和AppClassLoader是同級的,不存在繼承、依賴關係(兩者都存放在Launcher類中)

image

(Bootstrap ClassLoader是C++編寫的,不在上圖中)

但實際上,在類加載的時候,各個類加載器之間是有先後關係的。

jvm加載類時,調用類加載器的順序是這樣的(自頂向下嘗試加載類/自底向上委派):

在這裏插入圖片描述

每一個類加載器在獲得類加載請求時,自己不動手,都優先向自己的“上級”發起類加載請求,一直到最基本的啓動類加載器;

然後從最上層開始,逐級判斷這個類應不應該是自己負責加載的,如果是就加載,不是就將請求打回,由自己的下一級進行判斷。

也就是說,不管什麼類,都是一定由最上級(parents)的類加載器首先進行判斷是否加載,下級只負責將請求上傳,工作不被甩到自己頭上是絕不會主動去幹的。這種模式被稱爲“雙親委派模式”。

下面是該模式的時序圖

image

6-2、雙親委派的好處

Java類隨着他的類加載器一起具備了一種帶有優先級的層次關係。

比如Object存放在rt.jar裏面,最終都是委派給啓動類加載器加載,因此Object類在程序的各種類加載器環境中都是同一個類。

否則系統中會出現多個不同的Object類。

可以自己嘗試一下自定義一個 java.lang.Object,然後用自定義類加載器去加載(破壞掉雙親委派機制,不讓委派給其他類加載器)。這樣一個Object可以被加載,但是由於其類加載器不同,jvm仍然不會將它當做所有類的基類來對待。

6-3、解讀源碼

我們來看一下 Launcher 類的源碼,理解一下雙親委派是如何工作的。

(注意一下,下文的“父、類加載器”,不要理解成“父類、加載器”,僅僅指雙親委派時的順序,無關繼承關係)

public class Launcher {
    private static Launcher launcher = new Launcher();
    
    // 啓動類加載器要讀取被 -Xbootclasspath 參數指定路徑中的類庫,所以這裏讀取系統參數中的路徑名
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    
    private ClassLoader loader;

    public Launcher() {
        // 創建擴展類加載器對象。其中會讀取系統參數 System.getProperty("java.ext.dirs")
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError("Could not create extension class loader", e);
        }

        // 創建應用程序類加載器。參數中使用了擴展類加載器的對象,是爲了設定親子關係,將其設定爲自己的父類加載器(上一級)
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError("Could not create application class loader", e);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        ………………
    }

上面一段代碼中,兩級類加載器調用的 .getxxxClassLoader()方法,其實都去調用了Launcher的父類(URLClassLoader)的構造方法,它們需要傳的參數,是自己的父類加載器。

這裏,擴展類加載器傳的是null,應用程序類加載器傳的是擴展類加載器。

之所以會傳null,可以直接看基類 ClassLoader 的 loadClass() 方法

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 這裏會調用一個native方法findLoadedClass0,檢查類是否已經被加載
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 這個parent就是上面那段代碼中類加載器實例化時指定的父類加載器
                    if (parent != null) {
                        // 父類加載器非空時,直接調用其loadClass方法。那麼appClassLoader就會調用extClassLoader的loadClass方法,向上委派
                        c = parent.loadClass(name, false);
                    } else {
                        // 這裏就是給擴展類加載器傳準備的了,父類加載器爲空時,就是用BootstrapClassLoader去加載類
                        // (BootstrapClassLoader不是java寫的,這裏沒法創建java對象,只能是null)
                        // 這個方法是一個native方法
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    ………………
                }

                if (c == null) {
                    // 誰都無法加載的話,開始調用findClass方法
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    …………………
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

上面代碼裏有一個有意思的地方,“誰都無法加載的話,開始調用findClass方法”。而這個findClass方法裏面是啥呢?

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

直接拋異常哈哈

實際上,這是給用戶自定義類加載器留的接口,需要自定義的時候,重寫findClass()方法即可。

(如果想人爲破壞雙親委派模式的話,還需要自己重寫loadClass()方法——這也是個protected方法)

7、補充:關於非靜態方法

類加載時,不光靜態方法,實際上非靜態方法也會被加載(同樣加載到方法區),

只不過要調用到非靜態方法需要先實例化一個對象,然後用對象調用非靜態方法。

因爲如果讓類中所有的非靜態方法都隨着對象的實例化而建立一次,會大量消耗資源,所以纔會讓所有對象共享同一個非靜態方法,然後用this關鍵字指向調用非靜態方法的對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章