java虛擬機器(4)--類載入機制

來源:互聯網
上載者:User

標籤:position   1.2   分享   相等   tor   extc   工作過程   指定路徑   導致   

     類載入機制

類是在運行期間第一次使用時動態載入的,而不是編譯時間期一次性載入。因為如果在編譯時間期一次性載入,那麼會佔用很多的記憶體。

1.1 類的生命週期

 

 

包括以下 7 個階段:

  • 載入(Loading
  • 驗證(Verification
  • 準備(Preparation
  • 解析(Resolution
  • 初始化(Initialization
  • 使用(Using)
  • 卸載(Unloading)
1.2 類載入過程

包含載入,驗證,準備,解析,初始化這5個階段

1.2.1 載入

載入是類載入的一個階段,注意不要混淆。

載入過程完成以下三件事:

  • 通過一個類的全限定名來擷取定義此類的二進位位元組流。
  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的運行時儲存結構。
  • 在記憶體中產生一個代表這個類的 Class 對象,作為方法區這個類的各種資料的訪問入口。

其中二進位位元組流可以從以下方式中擷取:

  • 從 ZIP 包讀取,成為 JAR、EAR、WAR 格式的基礎。
  • 從網路中擷取,最典型的應用是 Applet。
  • 運行時計算產生,例如動態代理技術,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理類的二進位位元組流。
  • 由其他檔案產生,例如由 JSP 檔案產生對應的 Class 類。
1.2.2 驗證

確保 Class 檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

1.2.3 準備

類變數是被 static 修飾的變數,準備階段為類變數分配記憶體並設定初始值,使用的是方法區的記憶體。

執行個體變數不會在這階段分配記憶體,它將會在對象執行個體化時隨著對象一起分配在堆中。

注意,執行個體化不是類載入的一個過程,類載入發生在所有執行個體化操作之前,並且類載入只進行一次,執行個體化可以進行多次。

初始值一般為 0 值,例如下面的類變數 value 被初始化為 0 而不是 123。

public static int value = 123;

如果類變數是常量,那麼會按照運算式來進行初始化,而不是賦值為 0。

public static final int value = 123;

1.2.4 解析

將常量池的符號引用替換為直接引用的過程。

其中解析過程在某些情況下可以在初始化階段之後再開始,這是為了支援 Java 的動態綁定。

1.2.5 初始化

初始化階段才真正開始執行類中定義的 Java 程式碼。初始化階段即虛擬機器執行類構造器 <clinit>() 方法的過程。

在準備階段,類變數已經賦過一次系統要求的初始值,而在初始化階段,根據程式員通過程式制定的主觀計劃[陳文文1] 去初始化類變數和其它資源。

<clinit>() 方法具有以下特點

  • 是由編譯器自動收集類中所有類變數的賦值動作和靜態語句塊中的語句合并產生的,編譯器收集的順序由語句在源檔案中出現的順序決定。特別注意的是,靜態語句塊只能訪問到定義在它之前的類變數,定義在它之後的類變數只能賦值,不能訪問。例如以下代碼:[陳文文2] 

public class Test {

    static {

        i = 0;                // 給變數賦值可以正常編譯通過

        System.out.print(i);  // 這句編譯器會提示“非法向前引用”

    }

    static int i = 1;

}

  • 與類的建構函式(或者說執行個體構造器 <init>())不同,不需要顯式的調用父類的構造器。虛擬機器會自動保證在子類的 <clinit>() 方法運行之前,父類的 <clinit>() 方法已經執行結束。[陳文文3] 因此虛擬機器中第一個執行 <clinit>() 方法的類肯定為 java.lang.Object。
  • 由於父類的 <clinit>() 方法先執行,也就意味著父類中定義的靜態語句塊的執行要優先於子類。例如以下代碼:

static class Parent {

    public static int A = 1;

    static {

        A = 2;

    }

}

 

static class Sub extends Parent {

    public static int B = A;

}

 

public static void main(String[] args) {

     System.out.println(Sub.B);  // 2

}

  • <clinit>() 方法對於類或介面不是必須的,如果一個類中不包含靜態語句塊,也沒有對類變數的賦值操作,編譯器可以不為該類產生 <clinit>() 方法。[陳文文4] 
  • 介面中不可以使用靜態語句塊,但仍然有類變數初始化的賦值操作,因此介面與類一樣都會產生 <clinit>() 方法。但介面與類不同的是,執行介面的 <clinit>() 方法不需要先執行父介面的 <clinit>() 方法。只有當父介面中定義的變數使用時,父介面才會初始化。另外,介面的實作類別在初始化時也一樣不會執行介面的 <clinit>() 方法。[陳文文5] 
  • 虛擬機器會保證一個類的 <clinit>() 方法在多線程環境下被正確的加鎖和同步,如果多個線程同時初始化一個類,只會有一個線程執行這個類的 <clinit>() 方法,其它線程都會阻塞等待,直到活動線程執行 <clinit>() 方法完畢。如果在一個類的 <clinit>() 方法中有耗時的操作,就可能造成多個線程阻塞,在實際過程中此種阻塞很隱蔽[陳文文6] 。
1.3 類初始化時機1.3.1 主動引用

虛擬機器規範中並沒有強制限制式何時進行載入,但是規範嚴格規定了有且只有下列五種情況必須對類進行初始化(載入、驗證、準備都會隨之發生):

  • 遇到 new、getstatic、putstatic、invokestatic 這四條位元組碼指令時,如果類沒有進行過初始化,則必須先觸發其初始化。最常見的產生這 4 條指令的情境是:使用 new 關鍵字執行個體化對象的時候;讀取或設定一個類的靜態欄位(被 final 修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候;以及調用一個類的靜態方法的時候。
  • 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行初始化,則需要先觸發其初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  • 當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機器會先初始化這個主類;
  • 當使用 JDK 1.7 的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 執行個體最後的解析結果為 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化;
1.3.2 被動引用[陳文文7] 

以上 5 種情境中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱為被動引用。被動引用的常見例子包括:

  • 通過子類引用父類的靜態欄位,不會導致子類初始化。

System.out.println(SubClass.value);  // value 欄位在 SuperClass 中定義

  • 通過數組定義來引用類,不會觸發此類的初始化。該過程會對數組類進行初始化,數組類是一個由虛擬機器自動產生的、直接繼承自 Object 的子類,其中包含了數組的屬性和方法。

SuperClass[] sca = new SuperClass[10];

  • 常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化。

System.out.println(ConstClass.HELLOWORLD);

1.4 類與類載入器

兩個類相等需要類本身相等,並且使用同一個類載入器進行載入。這是因為每一個類載入器都擁有一個獨立的類名稱空間。

這裡的相等,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果為 true,也包括使用 instanceof 關鍵字做對象所屬關係判定結果為 true。

1.5 類載入器分類

從 JAVA 虛擬機器的角度來講,只存在以下兩種不同的類載入器:

  • 啟動類載入器(Bootstrap ClassLoader),這個類載入器用 C++ 實現,是虛擬機器自身的一部分;
  • 所有其他類的載入器,這些類由 Java 實現,獨立於虛擬機器外部,並且全都繼承自抽象類別 java.lang.ClassLoader。

從 Java 開發人員的角度看,類載入器可以劃分得更細緻一些:

  • 啟動類載入器(Bootstrap ClassLoader)此類載入器負責將存放在 <JRE_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,並且是虛擬機器識別的(僅按照檔案名稱識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被載入)類庫載入到虛擬機器記憶體中。啟動類載入器無法被 Java 程式直接引用,使用者在編寫自訂類載入器時,如果需要把載入請求委派給啟動類載入器,直接使用 null 代替即可。
  • 擴充類載入器(Extension ClassLoader)這個類載入器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變數所指定路徑中的所有類庫載入到記憶體中,開發人員可以直接使用擴充類載入器。
  • 應用程式類載入器(Application ClassLoader)這個類載入器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。由於這個類載入器是 ClassLoader 中的 getSystemClassLoader() 方法的傳回值,因此一般稱為系統類別載入器。它負責載入使用者類路徑(ClassPath)上所指定的類庫,開發人員可以直接使用這個類載入器,如果應用程式中沒有自訂過自己的類載入器,一般情況下這個就是程式中預設的類載入器。
1.6 雙親委派模型

應用程式都是由三種類載入器相互配合進行載入的,如果有必要,還可以加入自己定義的類載入器。

展示的類載入器之間的層次關係,稱為類載入器的雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啟動類載入器外,其餘的類載入器都應有自己的父類載入器。這裡類載入器之間的父子關係一般通過組合(Composition)關係來實現,而不是通過繼承(Inheritance)的關係實現。

 

 

1.6.1 工作過程

一個類載入器首先將類載入請求傳送到父類載入器,只有當父類載入器無法完成類載入請求時自己才嘗試載入。

1.6.2 好處

使得 Java 類隨著它的類載入器一起具有一種帶有優先順序的層次關係,從而使得基礎類得到統一。

例如 java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 的類並放到 ClassPath 中,程式可以編譯通過。由於雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先順序更高,這是因為 rt.jar 中的 Object 使用的是啟動類載入器,而 ClassPath 中的 Object 使用的是應用程式類載入器。rt.jar 中的 Object 優先順序更高,那麼程式中所有的 Object 都是這個 Object。

1.6.3 實現

以下是抽象類別 java.lang.ClassLoader 的程式碼片段,其中的 loadClass() 方法運行過程如下:先檢查類是否已經載入過,如果沒有則讓父類載入器去載入。當父類載入器載入失敗時拋出 ClassNotFoundException,此時嘗試自己去載入。

public abstract class ClassLoader {    // The parent class loader for delegation    private final ClassLoader parent;     public Class<?> loadClass(String name) throws ClassNotFoundException {        return loadClass(name, false);    }     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) {                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.                    c = findClass(name);                }            }            if (resolve) {                resolveClass(c);            }            return c;        }    }     protected Class<?> findClass(String name) throws ClassNotFoundException {        throw new ClassNotFoundException(name);    }}

 

1.7 自訂類載入器實現

FileSystemClassLoader 是自訂類載入器,繼承自 java.lang.ClassLoader,用於負載檔案系統上的類。它首先根據類的全名在檔案系統上尋找類的位元組代碼檔案(.class 檔案),然後讀取該檔案內容,最後通過 defineClass() 方法來把這些位元組代碼轉換成 java.lang.Class 類的執行個體。

java.lang.ClassLoader 的 loadClass() 實現了雙親委派模型的邏輯,因此自訂類載入器一般不去重寫它,但是需要重寫 findClass() 方法。

public class FileSystemClassLoader extends ClassLoader {    private String rootDir;    public FileSystemClassLoader(String rootDir) {        this.rootDir = rootDir;    }     protected Class<?> findClass(String name) throws ClassNotFoundException {        byte[] classData = getClassData(name);        if (classData == null) {            throw new ClassNotFoundException();        } else {            return defineClass(name, classData, 0, classData.length);        }    }     private byte[] getClassData(String className) {        String path = classNameToPath(className);        try {            InputStream ins = new FileInputStream(path);            ByteArrayOutputStream baos = new ByteArrayOutputStream();            int bufferSize = 4096;            byte[] buffer = new byte[bufferSize];            int bytesNumRead;            while ((bytesNumRead = ins.read(buffer)) != -1) {                baos.write(buffer, 0, bytesNumRead);            }            return baos.toByteArray();        } catch (IOException e) {            e.printStackTrace();        }        return null;    }     private String classNameToPath(String className) {        return rootDir + File.separatorChar                + className.replace(‘.‘, File.separatorChar) + ".class";    }}

 

 

 

 [陳文文1]程式碼

 [陳文文2]所以類變數的賦值動作+靜態語句塊中的語句。

注意:靜態語句塊只能訪問定義它前面的類變數,後面的可以賦值,不能訪問。

 [陳文文3]虛擬機器保證,先調用父類構造,後調用子類。父類是java.lang.Object

 [陳文文4]當類(介面)中不包含靜態語句塊,也沒有對類變數的賦值操作,編譯器不調用構造。

 [陳文文5]介面中沒有靜態代碼塊,但是有類變數初始化的賦值,一樣會產生<clinit>()方法。但介面(實作類別)中,不會執行父類的<clinit>()方法。只有使用到父類的變數時才執行。

 [陳文文6]虛擬機器自動封裝類的<clinit>()方法在多線程下正確加鎖和同步。

 

  1.  [陳文文7]子類訪問父類的靜態欄位,不會初始化子類
  2. 數組定義來引用類,不會出發此類。對數組類進行初始化。繼承Object的子類。數組屬性和方法。
  3. 常量在編譯階段存放在調用類中的常量池中,沒有直接引用到所在類。

java虛擬機器(4)--類載入機制

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.