jvm類載入

錦魚不忘舊時晨發表於2020-09-29

一、類載入過程

1、載入

載入指的是將類的class檔案讀入到記憶體,併為之建立一個java.lang.Class物件。

類的載入由類載入器完成,類載入器通常由JVM提供,JVM提供的這些類載入器通常被稱為系統類載入器。除此之外,開發者可以通過繼承ClassLoader基類來建立自己的類載入器。

通過使用不同的類載入器,可以從不同來源載入類的二進位制資料,通常由如下幾種來源。

  • 本地檔案系統載入class檔案,絕大部分程式的類載入方式。
  • JAR包載入class檔案,比如JDBC程式設計時用到的資料庫驅動類就放在JAR檔案中,JVM從JAR檔案中直接載入該class檔案。
  • 通過網路載入class檔案。
  • 把一個Java原始檔動態編譯,並執行載入。

類載入器通常無須等到“首次使用”該類時才載入該類,Java虛擬機器規範允許系統預先載入某些類

2、連結

類被載入之後,系統會為之生成一個對應的Class物件,接著將會進入連線階段,連線階段負責把類的二進位制資料合併到JRE中,類連線又可分為3個階段。

(1)驗證

這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。主要有四種驗證:

  • 檔案格式驗證: 主要驗證位元組流是否符合Class檔案格式規範,並且能被當前的虛擬機器載入處理。例如:主、次版本號是否在當前虛擬機器處理的範圍內。常量池中是否有不被支援的常量型別。
  • 後設資料驗證:對位元組碼描述的資訊進行語義的分析,分析是否符合java的語言語法的規範。
  • 位元組碼驗證:主要針對後設資料驗證後對方法體進行驗證,保證類方法在執行時不會有危害出現。
  • 符號引用驗證:主要是在虛擬機器將符號引用轉化為直接引用的時候進行校驗,這個轉化動作是發生在解析階段。符號引用可以看做是對類自身以外(常量池的各種符號引用)的資訊進行匹配性的校驗。

驗證階段對於虛擬機器的類載入機制來說,是一個非常重要但不一定是必要的階段。如果所執行的全部程式碼都已經被反覆使用和驗證過,在實施階段就可以考慮使用-Xverify:none引數來關閉大部分的類驗證措施,從而縮短虛擬機器類載入的時間

(2)準備

負責為類的靜態變數分配記憶體,並設定預設初始值

注意:這個時候進行記憶體分配的僅包括靜態變數(static修飾),而不包括例項變數,例項變數將會在物件例項化時隨物件一起被分配在Java堆中

(3)解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

  • 符號引用:符號引用以一組符號來描述所引用的目標,符號引用可以是任何形式的字面量,符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經在記憶體中。
  • 直接引用:是指向目標的指標、偏移量或者能夠直接定位的控制程式碼。直接引用是與虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同的虛擬機器例項上翻譯出來的直接引用一般都不相同,如果有了直接引用,虛擬機器可能會對第一次解析的結果進行快取

對於同一個符號引用可能會出現多次解析,虛擬機器可能會對第一次解析的結果進行快取。

解析動作分為四類:包括類或介面的解析、欄位解析、類方法解析、介面方法解析。

3、初始化

初始化是為類的靜態變數賦予正確的初始值。

二、類載入時機

JVM雖然沒有強制性約束在什麼時候開始類載入過程,但是對於類的初始化,虛擬機器規範嚴格規定了有且是由四種情況必須立即對類進行初始化,遇到newgetStaticputStaticinvokeStatic這四條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這四條指令最常見的javadiamante場景是:

1、建立類的例項,也就是new一個物件
2、呼叫類的靜態方法
3、訪問某個類或介面的靜態變數,或者對該靜態變數賦值。
4、反射(Class.forName(“com.xxx.xxx”))。
5、JVM啟動時標明的啟動類,即檔名和類名相同的那個類。

注意:對於final型別的靜態變數,如果該變數的值在編譯器就可以確定下來,那麼這個變數相當於“巨集變數”,Java編譯器會在編譯時直接把這個變數出現的地方替換成它的值,因此即使程式使用該靜態變數,也不會導致該類的初始化

巨集變數:滿足下面三個條件的即是巨集變數。

  • 必須是final修飾的變數;
  • 必須在開始時就指定初始值;
  • 該初始值必須在編譯器就可以確定;

三、類載入器

類載入負責載入所有的類,其為所有被載入記憶體的類生成一個java.lang.Class例項物件。一旦一個類被載入到JVM中,同一個類就不會被再次載入了。正如一個物件有一個唯一的標識一樣,一個載入JVM的類也有一個唯一的標識。

  • 在Java中,一個類用其全限定類名(包名和類名)作為標識,
  • 在JVM中, 一個類用其全限定類名和其類載入器作為其唯一標識

JVM預定義有三種類載入器,當一個JVM啟動的時候,Java開始使用如下三種類載入器。

1、根類載入器(啟動類)

用來載入Java的核心類,是用原生程式碼來實現的,並不繼承自java.lang.ClassLoader(負責載入$JAVA_HOME中jre/lib/rt.jar裡面所有的class。)

2、擴充套件類載入器

負責載入JRE的擴充套件目錄,lib/ext或者由java.ext.dirs系統屬性指定的目錄中的JAR包的類。

呼叫ClassLoader(ClassLoader parent)建構函式將父類載入器設定為null,因為如果不強制設定,預設會通過呼叫getSystemClassLoader()方法獲取並設定成系統類載入器。

3、系統類載入器

它根據在Java應用的類路徑(CLASSPATH)來載入Java類,一般來說,Java應用的類都是由它來完成載入的。可以通過ClassLoader.getSystemClassLoader()來獲取它的。

四、類載入機制

1、全盤負責

就是當一個類載入器負責載入某個Class時,該Class所依賴引用其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入。

2、雙親委派

就是先讓父類載入器試圖載入該class,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類。通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回,只有父載入器無法完成此載入任務時,才自己去載入。

使 用 雙 親 委 派 加 載 機 制 的 優 點 \color{green}{使用雙親委派載入機制的優點} 使

  • 1、Java類隨著它的類載入器一起具備了一種優先順序的層次關係,通過這種層次關係可以避免類的重複載入,當父親已經載入了該類時,就沒必要子ClassLoader再載入一次。

  • 2、防止核心API庫被隨意篡改,假設通過網路傳遞一個名為java.lang.Integer的類,通過雙親委派模式傳遞到啟動類載入器,而啟動類載入器在核心Java API發現這個名字的類,發現該類已被載入,就不會重新載入網路傳遞過來的java.lang.Integer,而直接返回已載入過的Integer.class。

如 何 破 壞 雙 親 委 派 機 制 ? \color{green}{如何破壞雙親委派機制?}

沿用雙親委派機制自定義類載入器很簡單,只需繼承ClassLoader類並重寫findClass方法即可。

定義一個繼承ClassLoader的類,除了重寫findClass方法外還要重寫loadClass方法,這裡loadClass方法預設是雙親委派機制,要想打破,必須重寫loadClass方法,即這裡先嚐試交由System類載入器載入,載入失敗才會由自己載入。

public class TestClassLoaderN extends ClassLoader {

  private String name;

  public TestClassLoaderN(ClassLoader parent, String name) {
    super(parent);
    this.name = name;
  }

  @Override
  public String toString() {
    return this.name;
  }

  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    Class<?> clazz = null;
    ClassLoader system = getSystemClassLoader();
    try {
      clazz = system.loadClass(name);
    } catch (Exception e) {
      // ignore
    }
    if (clazz != null)
      return clazz;
    clazz = findClass(name);
    return clazz;
  }

  @Override
  public Class<?> findClass(String name) {

    InputStream is = null;
    byte[] data = null;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try {
      is = new FileInputStream(new File("d:/Test.class"));
      int c = 0;
      while (-1 != (c = is.read())) {
        baos.write(c);
      }
      data = baos.toByteArray();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        is.close();
        baos.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    return this.defineClass(name, data, 0, data.length);
  }

  public static void main(String[] args) {
    TestClassLoaderN loader = new TestClassLoaderN(
        TestClassLoaderN.class.getClassLoader(), "TestLoaderN");
    Class clazz;
    try {
      clazz = loader.loadClass("test.classloader.Test");
      Object object = clazz.newInstance();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

3、快取機制

快取機制將會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區中搜尋該Class,只有當快取區中不存在該Class物件時,系統才會讀取該類對應的二進位制資料,並將其轉換Class物件,存入快取區中。

相關文章