JVM類加載

類的生命週期

JVM類加載

  1. 加載,驗證,準備,初始化,卸載 這五個階段先後順序是確定的。但是解析階段不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支持Java語言的運行時綁定(動態綁定或晚期綁定)。


靜態綁定:在程序執行前方法已經被綁定(也就是說在編譯過程中就已經知道這個方法到底是哪個類中的方法),此時由編譯器或其它連接程序實現。 
所有私有方法、靜態方法、構造器及初始化方法都是採用靜態綁定機制。在編譯器階段就已經指明瞭調用方法在常量池中的符號引用,JVM運行的時候只需要進行一次常量池解析即可。


動態綁定:動態綁定(後期綁定)是指:在程序運行過程中,根據具體的實例對象才能具體確定是哪個方法。
通俗的是指,對象在調用方法的時候能夠自己判斷該調用誰的方法。所以動態綁定一般發生在繼承、方法重載時。 
假設,對象o是類C1的實例,其中C1是C2的子類,C2是C3的子類,那麼o也是C2,C3的實例。如果對象o調用一個方法p,JVM會依次在類C1,C2,C3中查找方法p的實現,直到找到為止。
類對象方法的調用必須在運行過程中採用動態綁定機制:
 首先,根據對象的聲明類型(對象引用的類型)找到“合適”的方法。具體步驟如下:
 ① 如果能在聲明類型中匹配到方法簽名完全一樣(參數類型一致)的方法,那麼這個方法是最合適的。
 ② 在第①條不能滿足的情況下,尋找可以“湊合”的方法。標準就是通過將 參數類型進行自動轉型之後再進行匹配。如果匹配到多個自動轉型後的 方法簽名f(A)和f(B),則用下面的標準來確定合適的方法:傳遞給f(A) 方法的參數都可以傳遞給f(B),則f(A)最合適。反之f(B)最合適 。
 ③ 如果仍然在聲明類型中找不到“合適”的方法,則編譯階段就無法通過。
 然後,根據在堆中創建對象的實際類型找到對應的方法表,從中確定具體的方法在內存中的位置。
 
  1. JVM規範沒有強制約束 加載 過程何時開始,這部分可以由虛擬機的具體實現自由把握。但是對於初始化階段,虛擬機規範嚴格規定了有且只有 5種情況 必須立即對類進行初始化操作(加載 驗證 準備自然要在此之前開始)。
  2. 遇到new、getstatic、putstatic、invokestatic四條字節碼指令時,如果類沒有進行初始化,則必須先觸發其初始化操作。
  3. 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發初始化操作。
  4. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。但是接口在初始化時,並不要求其父接口全部完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化。
  5. 虛擬機啟動時,用戶需要制定一個要執行的主類,虛擬機會先初始化這個主類
  6. 當使用JDK1.7 的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄時,並且這個句柄對應的類沒有進行初始化,虛擬機需要先觸發其初始化操作。

類加載過程

加載

加載階段虛擬機需要完成以下3件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流
  2. 將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據訪問入口

對於數組類而言,情況就有所不同,數組本身不通過類加載器創建,它是由虛擬機直接創建的。但是數組類和類加載器仍有密切聯繫,因為數組類的元素類型最終是要靠類加載器去創建的。

數組類創建規則:

  1. 如果數組的元素類型是引用類型,那就使用系統提供的或者用戶自定義的類加載器去加載。該數組將在 加載數組元素類型 的類加載器 的類命名空間上被標識。(一個類必須與類加載器一起確定唯一性)
  2. 如果數組的元素類型不是引用類型(如 int[] 數組),虛擬機會把該數組標記為與引導類加載器關聯。
  3. 數組類的可見性和數組元素類型可見性一致,如果元素類型不是引用類型,那麼可見性默認為public。

加載完成後:

虛擬機將外部二進制字節流按照虛擬機所需的格式存儲在方法區之中,方法區的數據存儲格式由虛擬機自行實現。然後在內存中實例化一個java.lang.Class對象(Class對象比較特殊,它雖然是對象,但是存放在方法區中),這個對象將作為程序訪問方法區中類型數據的外部接口。

加載階段和連接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始,但這些夾在加載階段進行的動作,仍屬於連接階段的內容。

驗證

驗證是連接階段的第一步,這一階段是為了確保Class文件的字節流中包含的信息符合當前虛擬機得到要求,不會危害虛擬機自身的安全。

驗證階段大致會完成4個階段的動作:

  1. 文件格式驗證
  • 是否以魔數0xCAFEBABE開頭
  • 主次版本號是否在當前虛擬機處理範圍之內
  • 常量池的常量中是否有不被支持的常量類型
  • 。。。。。

驗證點還有很多,這裡不一一列舉。該驗證階段主要保證輸入的字節流能正確地解析並存儲於方法區之內,格式上符合一個描述Java類型信息的要求。

這階段的驗證是基於二進制字節流進行的,只有通過了這個階段的驗證後,字節流才會進入內存的方法區進行存儲,後面的驗證階段全部是基於方法區的存儲結構進行的,不會再直接操作部字節流。

  1. 元數據驗證

此階段是對類的元數據信息進行語義分析,以保證其描述的信息符合Java語言規範的要求。

此階段部分驗證點如下:

- 這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)
- 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)
- 如果這個類不是抽象類,是否實現了其父類或接口中要求實現的所有方法
。。。。。
  1. 字節碼驗證

此階段是驗證階段最複雜的一個階,主要目的是通過數據流和控制流分析,確定程序語義是合法的,符合邏輯的。此階段對類的方法體進行校驗分析,保證校驗的類在運行時不會做出危害虛擬機安全的事件。

在JDK1.7之後,對於主版本號大於50的Class文件,使用類型檢查來完成數據流分析校驗則是唯一選擇,不允許退回類型推導的校驗方式。

  1. 符號引用驗證

此階段的驗證發生在虛擬機將符號引用轉化為直接引用的時候,這個轉化動作將在連接的第三階段----解析階段中發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。

符號引用驗證的目的是確保解析動作能正常執行,如果無法通過符號引用驗證,將拋出
java.lang.IncompatibleClassChangeError異常的子類。

對於虛擬機來說,驗證階段是一個非常重要,但不是一定必要的階段。如果運行的代碼經過反覆使用和驗證,那麼在實施階段可以考慮使用 -Xverify:none 參數來關閉大部分的驗證措施,縮短類加載時間。

準備

準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些變量所使用的的內存在方法區中被分配。

此階段進行內存分配的僅包括類變量(static修飾),而不包括實例變量,實例變量將會在對象實例化時分配在堆中。此外,這裡的初始值是數據類型的零值。但是被 final修飾的類變量 會直接用程序中定義的值進行值初始化。

解析

解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。其中解析過程在某些情況下可以在初始化階段之後再開始,這是為了支持 Java 的動態綁定。

  • 符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要能夠使用時能無歧義地定位到目標即可。
  • 直接引用:直接引用是可以直接指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。

初始化

初始化階段是執行類構造器方法的過程

  1. 方法是由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊(static{})中的語句結合併產生的。編譯器收集順序是由語句在源代碼中出現的順序決定的,靜態語句塊中只能訪問到定義靜態語句塊之前的變量。定義在它之後的變量,靜態語句塊可以賦值,但不能訪問。
  2. 方法和類的構造函數(實例構造器)不同,它不需要顯示地調用父類構造器,虛擬機會保證子類的方法執行之前,父類的方法已經執行完畢。所以虛擬機第一個執行的方法肯定是java.lang.Object。
  3. 方法對於類或接口來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成方法。
  4. 接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口和類一樣都會生成方法。但是接口與類不同,執行接口的方法不需要先執行父接口的方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的方法。
  5. 虛擬機會保證一個類的方法在多線程環境中被正確地加鎖、同步,如果多個線程同時初始化一個類,那麼只會有一個線程去執行類的方法,其他線程需要阻塞等待。

類加載器

對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在虛擬機中的唯一性,每個類加載器都有一個獨立的命名空間。

比較兩個類是否相等:只有這兩個類是由同一個類加載器加載的前提下才有意義。

雙親委派模型

JVM類加載

  1. 啟動類加載器(Bootstrap ClassLoader):這個類負責將存放在\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,並且是虛擬機識別的類庫加載到虛擬機內存中。啟動類加載器無法直接被Java程序引用,如果需要把加載請求委派給引導類加載器,直接使用null代替即可。
  2. 擴展類加載器(Extension ClassLoader):這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 /lib/ext 或者被 java.ext.dir 系統變量所指定路徑中的所有類庫加載到內存中,開發者可以直接使用擴展類加載器。
  3. 應用程序加載器(Application ClassLoader):這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。由於這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱為系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

工作過程:

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

//雙親委派模型的工作過程源碼protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException{ // 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;}

(1)當前類加載器從自己已經加載的類中查詢是否此類已經加載,如果已經加載則直接返回原來已經加載的類。

(2)如果沒有找到,就去委託父類加載器去加載(如代碼c = parent.loadClass(name, false)所示)。父類加載器也會採用同樣的策略,查看自己已經加載過的類中是否包含這個類,有就返回,沒有就委託父類的父類去加載,一直到啟動類加載器。因為如果父加載器為空了,就代表使用啟動類加載器作為父加載器去加載。

(3)如果啟動類加載器加載失敗(例如在$JAVA_HOME/jre/lib裡未查找到該class),則會拋出一個異常ClassNotFoundException,然後再調用當前加載器的findClass()方法進行加載。

自定義類加載器

自定義類加載器的應用場景:

(1)加密:Java代碼可以輕易的被反編譯,如果你需要把自己的代碼進行加密以防止反編譯,可以先將編譯後的代碼用某種加密算法加密,類加密後就不能再用Java的ClassLoader去加載類了,這時就需要自定義ClassLoader在加載類的時候先解密類,然後再加載。

(2)從非標準的來源加載代碼:如果你的字節碼是放在數據庫、甚至是在雲端,就可以自定義類加載器,從指定的來源加載類。

(3)以上兩種情況在實際中的綜合運用:比如你的應用需要通過網絡來傳輸 Java 類的字節碼,為了安全性,這些字節碼經過了加密處理。這個時候你就需要自定義類加載器來從某個網絡地址上讀取加密後的字節代碼,接著進行解密和驗證,最後定義出在Java虛擬機中運行的類。

自定義類加載器:

自定義一個People.java類做例子


public class People {//該類寫在記事本里,在用javac命令行編譯成class文件,放在d盤根目錄下 private String name;
 public People() {}
 public People(String name) { 	this.name = name; }
 public String getName() { 	return name; }
 public void setName(String name) { 	this.name = name; }
 public String toString() { 	return "I am a people, my name is " + name; }
}

自定義一個類加載器,需要繼承ClassLoader類,並實現findClass方法。其中defineClass方法可以把二進制流字節組成的文件轉換為一個java.lang.Class(只要二進制字節流的內容符合Class文件規範)。

import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.nio.ByteBuffer;import java.nio.channels.Channels;import java.nio.channels.FileChannel;import java.nio.channels.WritableByteChannel;
public class MyClassLoader extends ClassLoader{ public MyClassLoader() {
 }
 public MyClassLoader(ClassLoader parent) { super(parent); }
 protected Class> findClass(String name) throws ClassNotFoundException { 	File file = new File("D:/People.class"); try{ byte[] bytes = getClassBytes(file); //defineClass方法可以把二進制流字節組成的文件轉換為一個java.lang.Class Class> c = this.defineClass(name, bytes, 0, bytes.length); return c; } catch (Exception e) { e.printStackTrace(); }
 return super.findClass(name); }
 private byte[] getClassBytes(File file) throws Exception { // 這裡要讀入.class的字節,因此要使用字節流 FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024);
 while (true){ int i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); }}
 

在主函數里使用

MyClassLoader mcl = new MyClassLoader();Class> clazz = Class.forName("People", true, mcl);Object obj = clazz.newInstance();
System.out.println(obj);System.out.println(obj.getClass().getClassLoader());//打印出我們的自定義類加載器


分享到:


相關文章: