天天看點

jvm類加載機制_JVM系列之二[類加載機制]

官網描述:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html

虛拟機的類加載機制:虛拟機将class檔案中的二進制資料讀入到記憶體,按先後順序對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型。

類在虛拟機中的生命周期如下圖:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、解除安裝(Unloading)7個階段。

jvm類加載機制_JVM系列之二[類加載機制]

類在虛拟機中的生命周期

類加載過程必須按照順序按部就班開始:加載、驗證、準備、初始化和解除安裝,但是解析階段可以在初始化後開始,這是為了支援java的動态綁定。

類加載過程之【加載】

類加載階段的操作:

  1. 擷取類的二進制位元組流
  2. 将位元組流中的靜态存儲結構轉為方法區的的運作時資料結構
  3. 在記憶體中(HotSpot虛拟機中,Class對象存放在方法區中)生成對應的java.lang.Class對象,作為方法區這個類的各種資料的通路入口

數組類的加載:

數組類本身不通過類加載器建立,而由Java虛拟機直接建立。

  • 如果數組中的元素類型是引用類型,則遞歸類加載過程來加載這個元素,數組C将在加載這個元素的類加載器的類名稱空間上被辨別
  • 如果數組中的元素不是引用類型(如int[]),數組C标記為與引導類加載器關聯
  • 數組類的可見性與其中的元素的可見性一緻,如果其中元素不是可見類型,數組類的可見性預設為public

二進制的位元組流來源:

  • 從本地系統中直接加載
  • 通過網絡下載下傳class檔案
  • 從jar,zip等歸檔的檔案中加載class檔案
  • 從資料庫中提取class檔案
  • 動态編譯java源檔案為class檔案

類加載器ClassLoader:

ClassLoader隻負責class檔案的加載。

類加載器并不是在等到某個類需要的時候才去加載它,jvm規範是允許類加載器在預料到某個類将要被使用時就預先将其加載,如果預加載時class檔案缺失或存在錯誤,類加載器必須在首次主動使用時報錯LinkageError,如果此類一直沒被程式主動使用,類加載器不會報錯。

jvm類加載機制_JVM系列之二[類加載機制]

類加載器

啟動類加載器(Bootstrap ClassLoader)

加載JAVA_HOMElib目錄中的,及通過-Xbootclasspath 參數指定路徑中的,且被虛拟機認可(按檔案名識别,如rt.jar)的類。

擴充類加載器(Extension ClassLoader)

加載 JAVA_HOMElibext目錄中的,或通過-Djava.ext.dirs系統變量指定路徑中的類庫。

應用程式類加載器(Application ClassLoader)

負責加載使用者路徑(classpath),及-Djava.class.path系統變量指定路徑中的類庫

自定義的類加載器:

通過java.lang.ClassLoader的子類自定義加載class檔案,如tomcat。

如何實作自定義類加載器:

繼承ClassLoader,重寫findClass()方法,在findClass()方法中加載目标類,在loadClass(…)方法中,父加載器加載失敗後會調用findClass()方法來進行加載。

public class ClassLoaderCustomer extends ClassLoader {  @Override  protected Class> findClass(String name) throws ClassNotFoundException {    //加載clazz檔案   Class clazz = this.findLoadedClass(className);   if (null == clazz) {    try {        String classFile = getClassFile(className);        FileInputStream fis = new FileInputStream(classFile);        FileChannel fileC = fis.getChannel();        ByteArrayOutputStream baos = new ByteArrayOutputStream();        WritableByteChannel outC = Channels.newChannel(baos);        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);         省略部分代碼        fis.close();        byte[] bytes = baos.toByteArray();        clazz = defineClass(className, bytes, 0, bytes.length);    } catch (FileNotFoundException e) {        e.printStackTrace();    } catch (IOException e) {        e.printStackTrace();    }}   return clazz;  }}
           

以上四種類加載器,除了啟動類加載器是由C++實作的外,剩餘的都會有父類加載器(使用組合實作)。

jvm類加載機制_JVM系列之二[類加載機制]

雙親委派機制

雙親委派模型工作過程:如果一個類加載器收到了類加載的請求,它不會自己嘗試加載這個類,而是先把請求交給父類加載器去完成,每個層次的類加載器都是如此,那麼所有的加載請求最終都會到達頂層的啟動類加載器中,隻有當父類加載器無法完成加載任務時,子類加載器才會嘗試自己加載。

雙親委派模型的優勢:

Java類随着它的類加載器一起具備的帶有優先級的層次關系。如rt.ar中的java.lang.String類,最終都是由啟動類加載器加載,在程式中無論使用何種類加載器環境都是使用的一個類。如果沒有雙親委派,系統中會出現多個String類,程式将變得混亂。

破壞雙親委派模型:

  • Tomcat的WebappClassLoader 就會先加載自己的Class,找不到再委托parent
  • OSGi的ClassLoader形成網狀結構,根據需要自由加載Class
  • JNDI服務中,為了對自己進行集中管理和查找,需要調用由獨立廠商實作并部署在程式的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)代碼。

如果啟動類加載器加載不了這些代碼,使用線程上下文類加載器(Thread Context ClassLoader),通過Thread類的方法setContextClassLoader(ClassLoader)進行設定,如果建立線程時未設定則從父線程中的ClassLoader,如果程式全局範圍沒有設定過,則設定未系統類加載器。

JNDI服務使用線程上下文類加載器加載所需的SPI代碼,即父類加載器請求子類加載器來完成類加載的動作。

Java中的SPI加載如JDBC,如下代碼可解釋如何通過設定Thread.setContextClassLoader(ClassLoader)破壞雙親委派。

//使用JDBC建立資料庫連接配接Class.forName(driver);//加載驅動Connection  conn = DriverManager.getConnection(url,user,password);//建立連接配接
           

調用類DriverManager的靜态方法getConnection (…)之前會先初始化靜态代碼塊,方法loadInitialDrivers()中調用ServiceLoader.load(Driver.class)。

類Driver在jre/lib/rt.jar中,由啟動類加載器加載,但是Driver的實作類在不同的廠商提供的jar包中,需要由子類加載器來加載Driver的實作類。調用調用ServiceLoader.load(Driver.class)會加載檔案META-INF/services/java.sql.Driver中的類到jvm,完成Driver的實作類的自動加載。

public class DriverManager { static {    loadInitialDrivers();    println("JDBC DriverManager initialized");} private static void loadInitialDrivers() {    //代碼省略    AccessController.doPrivileged(new PrivilegedAction() {        public Void run() {            ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);            //其他代碼省略        }    });    //其他代碼省略}}
           

類加載過程之【連結】

1. 驗證(Verification)

目的:確定加載類的正确性,并且不會危害虛拟機自身的安全

驗證階段包含四個檢驗:

  • 檔案格式驗證:確定位元組流符合class檔案格式的規範,并且能被目前版本的虛拟機處理
  • 中繼資料驗證:確定位元組碼的描述資訊符合Java語言規範的要求
  • 位元組碼驗證:通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。保證位元組碼流可以被java虛拟機安全地執行。
  • 符号引用驗證:對類自身以外的資訊進行比對性校驗,確定解析動作能正常執行,否則報錯如java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。此驗證階段發生在解析階段(符号引用轉化為直接引用時進行驗證)。

2. 準備(Preparation)

為類變量配置設定記憶體(方法區)并設定預設的初始值(基本資料類型的預設值參考官網:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.3)。

類變量的預設的初始值設定的兩種情況:

  • 例如:public static int value = 10;

    static修飾的變量value在準備階段過後的值為0,此時未執行任何java方法,将10指派給value是指令putstatic 被程式編譯後,存放在類構造器()方法中,就是說在初始化階段将value指派為10。

  • 例如:public static final int value = 10;

    如果類字段的字段屬性表中存在ConstantValue屬性,那麼在準備階段變量的預設值設定為ConstantValue屬性所指的值。

final修飾的value屬性,在編譯時會生成ConstantValue屬性,準備階段會将ConstantValue的值指派給value。

3. 解析(Resolution)

将常量池内的符号引用替換為直接引用,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符這7類符号引用。

符号引用:用一組符号來描述所引用的目标,符号引用以字面量形式明确定義在Java虛拟機規範的class檔案格式中。在類檔案結構中已被提及,存于常量池中。

直接引用:指向目标的指針、相對偏移量後一個能間接定位到目标的檔案句柄。

類加載過程之【初始化】

Java虛拟機執行類的初始化語句,為類的靜态變量賦予初始值。

在準備階段,系統已為類變量賦予初始值,而在初始化階段将根據程式代碼為類變量指派。

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

類的靜态變量初始化有兩種方式:

  • 在靜态變量的聲明出進行初始化
  • 在靜态代碼塊中程序初始化

Java虛拟機會根據初始化語句在類檔案中的先後順序依次執行。

類的初始化步驟:

  1. 假如這個類還沒有被加載和連接配接,就先進行加載和連接配接;
  2. 假如這個類存在直接的父類且沒有被初始化,則先初始化直接的父類;
  3. 假如類中存在初始化語句,則按順序依次執行;

Java程式中對類的使用有兩種:

主動使用和被動使用。所有的類或接口在被程式 “首次主動使用”時才對它們進行初始化。

主動使用場景如下:

  • 執行個體化對象、讀取或設定類或接口的靜态變量(被final修飾、已在編譯器把結果放入常量池的靜态字段除外)、調用類的靜态方法的時候,如果類沒有初始化過,需先進行初始化;
  • 使用類的反射調用,如果類沒有初始化過,要先進行初始化
  • 當初始化一個子類的時候,發現父類還沒初始化過,要先對父類進行初始化
  • 虛拟機啟動時,先初始化被辨別為啟動類的的類(有main()方法的類)
  • 在使用jdk1.7的動态語言支援時,如果一個java.lang.invoke.MethodHandle執行個體最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且方法句柄對應的類沒有初始化過,需要先進行初始化

除了上述5中情況外的使用java類都是對類的被動使用,不會進行類的初始化。

java虛拟機生命周期結束的幾種情況:

  1. 執行了方法System.exit()
  2. 程式正常結束
  3. 程式運作過程中遇到異常或錯誤而導緻的
  4. 作業系統出現問題而導緻java虛拟機終止

繼續閱讀