Class類加載過程與類加載器

在說類加載器和雙親委派模型之前,先來梳理下Class類文件的加載過程,JAVA虛擬機爲了保證 實現語言的無關性,是將虛擬機只與“Class 文件”字節碼這種特定形式的二進制文件格式相關聯,而不是與實現語言綁定。所以其不一定是Class文件。

類加載過程

Class類從被加載到虛擬機內存開始,到卸載出內存爲止,其生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸載(Unloading)7個階段。其中加載過程見下:

在這裏插入圖片描述

加載階段

加載階段做了什麼?過程見下圖。其中類的全限定名是Class文件(JAVA由編譯器自動生成)內的代表常量池內的16進制值所代表的特定符號引用。因爲Class文件格式有其自己的一套規範,如第1-4字節代表魔數,第5-6字節代表次版本,第7-8字節代表主版本號等等。

在這裏插入圖片描述

說白了就是,虛擬機不關心我們的這種“特定二進制流”從哪裏來的,從本地加載也好,從網上下載的也罷,都沒關係。虛擬機要做的就是將該二進制流寫在自己的內存中並生成相應的Class對象(並不是在堆中)。在這個階段,我們能夠通過我們自定義類加載器來控制二進制流的獲取方式。

驗證階段

驗證階段,正因爲加載階段虛擬機不介意二進制的來源,所以就可能存在着影響虛擬機正常運行的安全隱患。所以虛擬機對於該二進制流的校驗工作非常重要。校驗方式包括但不限於:

在這裏插入圖片描述

準備階段

準備階段在此階段將正式爲類變量分配內存並設置變量的初始化值。注意的是,類變量是指 static 的靜態變量,是分配在方法區之中的,而不像對象變量,分配在堆中。還有一點需要注意,final 常量在此階段就已經被賦值了。如下:

    public static int SIZE = 10; // 初始化值 == 0
    public static final int SIZE = 10; // 初始化值 == 10
解析階段

解析階段是將常量池內的符號引用替換爲直接引用的過程。符號引用就是上文說的Class文件格式標準所規定的特定字面量,而直接引用就是我們說的指針,內存引用等概念

初始化階段

到了初始化階段,就開始真正執行我們的字節碼程序了。也可以理解成:類初始化階段就是虛擬機內部執行類構造 < clinit >() 方法的過程。注意,這個類構造方法可不是虛擬機內部生成的,而是我們的編譯器自動生成的,是編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,具體分析見下。

注意,這裏說的是類變量賦值動作,即static 並且具有賦值操作,如果無賦值操作,那麼在準備階段進行的方法區初始化就算完成了。爲何還要加上static{} 呢?我們可以把static{} 理解成:是由多個靜態初始化動作組織成的一個特殊的“靜態子句”,與其他的靜態初始化動作一樣。這也是爲何 static {} 只會執行一遍並在對象構造方法之前執行的原因。如下代碼:

public class Tested {
    public static int T;
    // public static int V; // 無賦值,不在類構造中再次初始化
    public int c = 1; // 不會在類構造中

    static {
        T = 10;
    }
}

還有一點,編輯器收集類變量的順序,也就是虛擬機在此初始化階段的執行順序,這個順序就是變量在類中語句定義的先後順序,如上面的:語句 2 : T 在 6 : T 之前,這是兩個獨立的語句。類構造< clinit >的其他特點如下:

在這裏插入圖片描述

編譯期的< clinit >

我們將流程回溯到編譯期階段,以剛剛的Tested 類代碼爲例。通過 javap -c /Tested.class (注意:/…/Tested 絕對路徑),獲取Class文件:

public class com.tencent.lo.Tested {
  public static int T;

  public int c;

  public com.tencent.lo.Tested();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_1
       6: putfield      #2                  // Field c:I
       9: return

  static {};
    Code:
       0: bipush        10
       2: putstatic     #3                  // Field T:I
       5: return
}

在Class 文件中我們能很明顯的看到 invokespecial 對應的對象構造 “< init >” : () V ,那爲什麼沒有看到< clinit > 類構造方法呢?其實上面的 static {} 就是。我們來看下OpenJDK源碼Constants接口,此接口定義了在編譯器中所用到的常量,這是一個自動生成的類。

public interface Constants extends RuntimeConstants {
    public static final boolean tracing = true;

    Identifier idClassInit = Identifier.lookup("<clinit>");
    Identifier idInit = Identifier.lookup("<init>");
}

MemberDefinition類 中,判斷是否爲類構造器字符:

    public final boolean isInitializer() {
        return getName().equals(idClassInit); // 類構造
    }
    public final boolean isConstructor() {
        return getName().equals(idInit); // 對象構造
    }

而在MemberDefinition 的 toString() 方法中,我們能夠看到,當類構造時,會輸出特定字符,而不會像對象構造那樣輸出規範的字符串。

    public String toString() {
        Identifier name = getClassDefinition().getName();
        if (isInitializer()) { // 類構造
            return isStatic() ? "static {}" : "instance {}";
        } else if (isConstructor()) { // 對象構造
            StringBuffer buf = new StringBuffer();
            buf.append(name);
            buf.append('(');
            Type argTypes[] = getType().getArgumentTypes();
            for (int i = 0 ; i < argTypes.length ; i++) {
                if (i > 0) {
                    buf.append(',');
                    }
                buf.append(argTypes[i].toString());
                }
            buf.append(')');
            return buf.toString();
        } else if (isInnerClass()) {
            return getInnerClass().toString();
        }
        return type.typeString(getName().toString());
    }

類加載器與雙親委派

“虛擬機將類加載階段中的“通過一個全限定名來獲取描述此類的二進制字節流”這個動作放到了外部來實現,以便開發者可以自己決定如何獲取所需的類文件,而實現這個動作的代碼模塊就被稱爲類加載器。對於任意一個類來說,只有在類加載器相同的情況下比較兩者是否相同纔有意義,否則即使是同個文件,在不同加載器下,在虛擬機看來其仍然是不同的,是兩個獨立的類。我們可以將類加載器分爲三類”:

在這裏插入圖片描述
而所謂的雙親委派模型就是:“如果一個類加載器收到類加載的請求,它首先不會自己去嘗試加載這個類,而是把加載的操作委託給父類加載器去完成,每一層次加載器都是如此,因此所有的加載請求都會傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時(它的搜索範圍沒有找到所需的類,因爲上面所說的啓動類加載器和擴展類加載器,只能加載特定目錄之下的,或被-x參數所指定的類庫),子類纔會嘗試自己加載”。注意這裏說的父類只是形容層次結構,其並不是直接繼承關係,而是通過組合方式來複用父類的加載器的。

在這裏插入圖片描述
“雙親委派的好處就是,使加載器也具備了優先級的層次結構。例如,java.lang.Object存放在< JAVA_HOME>\lib 下的rt.jar包內,無論哪個類加載器要加載這個類,最終都會委派給最頂層的啓動類加載器,所以保證了Object類在各類加載器環境中都是同一個類。相反,如果沒有雙親委派模型,如果用戶編寫了一個java.lang.Object類,並放在程序的ClassPath下,那麼系統將會出現多個不同的Object類”。

爲何?因爲每個加載器各自爲政,不會委託給父構造器,如上面所說,只要加載器不同,即使類Class文件相同,其也是獨立的。

試想如果自己在項目中編寫了一個java.lang.Object 類(當然不能放入rt.jar類庫中替換掉同名Object文件,這樣做沒有意義,如果虛擬機加載校驗能通過的話,只是相當於改了源碼嘛),我們通過自定義的構造器來加載這個類可以嗎?理論上來說,雖然這兩個類都是java.lang.Object,但由於構造器不同,對於虛擬機來說這是不同的Class文件,當然可以。但是實際上呢?來段代碼見下:

    public void loadPathName(String classPath) throws ClassNotFoundException {
        new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                InputStream is = getClass().getResourceAsStream(name);
                if (is == null)
                    return super.loadClass(name);
                byte[] b;
                try {
                    b = new byte[is.available()];
                    is.read(b);
                } catch (Exception e) {
                    return super.loadClass(name);
                }
                return defineClass(name, b, 0, b.length);
            }
        }.loadClass(classPath);
    }

實際的執行邏輯是 defineClass 方法。可以發現,自定義加載器是無法加載以 java. 開頭的系統類的。

    protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
            throws ClassFormatError {
        
        protectionDomain = preDefineClass(name, protectionDomain);
        ... // 略

        return c;
    }

    private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) {
        if (!checkName(name))
            throw new NoClassDefFoundError("IllegalName: " + name);
        // 在這裏能看到系統類,自定義的加載器是不能加載的
        if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                    ("Prohibited package name: " +
                            name.substring(0, name.lastIndexOf('.')));
        }
        ... // 略
        
        return pd;
    }

如果你用AS直接查看,你會發現,defineClass 內部是沒有具體實現的,源碼見下。可這並不代表android 的 defineClass 方法實現與 java 不同,因爲都是引用的 java.lang 包下的ClassLoader 類,邏輯肯定都是一樣的。之所以看到的源碼不一樣,這是由於SDK和JAVA源碼包的區別導致的。SDK內的源碼是谷歌提供給我們方便開發查看的,並不完全等同於源碼。

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        throw new UnsupportedOperationException("can't load this type of class file");
    }

參考
1、周志明,深入理解JAVA虛擬機:機械工業出版社

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