JVM09-類加載過程

這一篇我們來學習一下JVM中的類加載過程。說到類的加載過程,我們需要先了解一下JVM中類的生命週期。在JVM中類的生命週期有七個階段。分別是:

  1. 加載(Loading):加載是通過類加載器從不同的地方加載進二進制字節流,類加載器可以參考類加載器與雙親委派模型
  2. 驗證(Verification):驗證階段是爲了確保Class文件的字節流中包含的信息是否符合《JVM虛擬機規範》的全部約束要求。
  3. 準備(Preparation):準備階段是爲靜態資源分配內存,並賦初始值
  4. 解析(Resolution) :解析階段是Java虛擬機將常量池內的符號引用替換成直接引用的過程
  5. 初始化(Initialization):根據靜態變量的賦值語法和靜態代碼塊語法,生成一個初始化方法並執行。
  6. 使用(Using):
  7. 卸載(Unloading): 如果該類所有的實例都已經被回收,也就是java堆中不存在該類的任何實例。不過由JVM自帶的類加載器加載的類,在JVM的生命週期中,始終不會被卸載。
    其中,驗證、準備、解析三個部分統稱爲連接(Linking)。而加載,驗證,準備,解析,初始化這五個階段組成了類加載過程。

加載

加載階段是整個類加載過程中的一個階段,加載階段,Java虛擬機需要完成以下三件事情。

  1. 通過全限定名來獲取定義此類的二進制字節流。
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。
    《Java虛擬機規範》中沒有指明要從哪裏獲取,如何獲取。所以我們獲取的方式有很多種,比如:
    A. 從ZIP壓縮包中讀取,這很常見,各種JAR,WAR格式的壓縮包均是這種形式
    B. 從網絡中獲取,這種場景最典型的應用就是Web Applet。
    C. 運行時計算生成,這種場景使用最多的就是動態代理技術,就是用ProxyGenerator.generateProxyClass()來爲特定接口生成形式爲"$Proxy"的代理類的二進制字節流。
    D. 由其他文件生成,典型的場景是JSP應用,由JSP文件生成對應的Class文件。

驗證

加載完成之後,就是驗證,驗證是連接階段的第一步,這一階段的目的是確保Class文件的字節流中包含信息符合《Java虛擬機規範》的全部約束要求,保證這些信息被當作代碼運行後不會危害虛擬機自身的安全。大致上會完成下面四個階段的驗證動作。

文件格式驗證

驗證字節流是否符合Class文件格式的規範;例如:是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍 之內、常量池中的常量是否有不被支持的類型。

元數據驗證

對字節碼描述的信息進行語義分析(注意:對比Javac編譯階段的語義分析),以保證其描述的
信息符合Java語言規範的要求;例如:這個類是否有父類,除了Java.lang.Object之外。

字節碼驗證

第三階段是整個驗證過程中最複雜的一個階段,主要目的是通過數據流分析和控制流分析,
確定程序語義是合法的、符合邏輯的。在第二階段對元數據信息中的數據類型校驗完畢以後,
這階段就要對類的方法體(Class文件中的Code屬性)進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行爲。

符號引用驗證

最後一個階段的校驗行爲發生在虛擬機將符號引用轉化爲直接引用的時候,
這個轉化動作將在連接的第三階段-解析階段中發生。符號引用驗證可以看作是對類自身以外的
各類信息進行匹配性校驗,通俗來說,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、
字段等資源。

準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中
進行分配。這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會
在對象實例化時隨着對象一起分配在堆中。其次,這裏所說的初始值""通常情況"下是數據類型的零值,假設一個類變量定義爲:

public static int value=123

那變量value在準備階段過後的初始值爲0而不是123。因爲這時候尚未開始執行任何Java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器()方法之中,所以把value賦值爲123的動作將在初始化階段纔會執行。至於“特殊情況”是指:public static final int value=123,即當類字段的字段屬性是ConstantValue時,會在準備階段初始化爲指定的值,
所以標註爲final之後,value的值在準備階段初始化爲123而非0。

解析

解析階段是Java虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、
方法句柄和調用點限定符等7類符號引用進行,分別對應常量池的CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna_info和
CONSTANT_InvokeDynamic_info 8種常量類型。
其中符號引用(Symbolic Reference): 符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定是已經加載到虛擬機內存當中的內容。
直接引用: 直接引用是可以直接指向目標的指針、相對偏移量或者一個能間接定位到目標的句柄。直接引用是和虛擬機實現的內存佈局直接相關的。同一個符合引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在虛擬機的內存中存在。

初始化

類初始化階段是類加載過程的最後一步,到了初始化階段,才真正開始執行類中
定義的Java程序代碼。進行準備階段時,變量已經賦過一次系統要求的初始零值,而在初始化階段,則會根據程序員通過程序編碼制定的主觀計劃去初始化類變量和其他資源。我們也可以從另外一種更直接的形式來表達;初始化階段就是執行類構造器()方法的過程。() 並不是程序員在Java代碼中直接編寫的方法,它是Javac編譯器的自動生成物,但我們非常有必要了解這個方法具體是如何產生的。以及()方法執行過程中各種可能會影響程序運行行爲的細節。
<clinit>() 方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊(static{}塊)
中的語句合併產生的。編譯器收集的順序是由語句在源文件中出現的順序決定的。靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊可以賦值,但是不能訪問。如下代碼所示:

public class Test
     {
         static
         {
             i=0;
             System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應用)
         }
         static int i=1;
     }

接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生成()方法。但接口與類不同的是,執行接口的()方法不需要先
執行父接口的()方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行
接口的()方法。

虛擬機會保證一個類的()方法在多線程環境中被正確的解鎖、同步、如果多個線程同時去初始化
一個類,那麼只會有一個線程去執行這個類的()方法,其他線程都需要阻塞等待,直到活動線程執行
()方法完畢。如果在一個類的()方法中有耗時很長的操作,就可能造成多個線程阻塞,在實際應用中這種
阻塞往往是隱藏的。

  package jvm.classload;
   
  public class DealLoopTest
  {
      static class DeadLoopClass
      {
          static
          {
              if(true)
              {
                  System.out.println(Thread.currentThread()+"init DeadLoopClass");
                  while(true)
                  {
                  }
              }
          }
      }
   
      public static void main(String[] args)
      {
          Runnable script = new Runnable(){
              public void run()
              {
                  System.out.println(Thread.currentThread()+" start");
                  DeadLoopClass dlc = new DeadLoopClass();
                  System.out.println(Thread.currentThread()+" run over");
              }
          };
   
          Thread thread1 = new Thread(script);
          Thread thread2 = new Thread(script);
          thread1.start();
          thread2.start();
      }
  }

運行結果:(即一條線程在死循環以模擬長時間操作,另一條線程在阻塞等待)

  Thread[Thread-0,5,main] start
  Thread[Thread-1,5,main] start
  Thread[Thread-0,5,main]init DeadLoopClass

需要注意的是,其他線程雖然被阻塞,但如果執行()方法的那條線程退出()方法後,
其他線程喚醒之後不會再次進入()方法。同一個類加載器下,一個類型之後初始化一次。
將上面代碼中的靜態塊替換如下:

  static
          {
              System.out.println(Thread.currentThread() + "init DeadLoopClass");
              try
              {
                  TimeUnit.SECONDS.sleep(10);
              }
              catch (InterruptedException e)
              {
                  e.printStackTrace();
              }
          }

運行結果:

  Thread[Thread-0,5,main] start
  Thread[Thread-1,5,main] start
  Thread[Thread-1,5,main]init DeadLoopClass (之後sleep 10s)
  Thread[Thread-1,5,main] run over
  Thread[Thread-0,5,main] run over

虛擬機規格嚴格規定了有且只有5種情況(jdk1.7)必須對類進行"“初始化”(而加載、驗證、準備自然需要在此之前
開始):

1、遇到new,getstatic,putstatic,invokestatic這四條字節碼指令時,如果類米有進行初始化,則需要先出發其初始化。
生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final
修飾、已在編輯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
2、使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
3、當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先出發其父類的初始化。
4、當虛擬機啓動時,用戶需要指定一個要執行的主類(包括main()方法的那個類),虛擬機會先初始化這個主類。
5、當使用jdk1.7動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,
REF_invokestatic的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則先要先觸發其初始化。

開篇已經舉了一個範例:通過子類引用賦了的靜態字段,不會導致子類初始化。
這裏再舉兩個例子。
1、通過數組定義來引用類,不會觸發此類的初始化:(SuperClass類已經在本文開篇定義)

   public class NotInitialization
   {
       public static void main(String[] args)
       {
           SuperClass[] sca = new SuperClass[10];
       }
   }

運行結果:(無)
2、常量在編譯階段會存入調用類的常量池中,本質並沒有直接引用定義常量的類,因此不會觸發定義常量
的類初始化:

  public class ConstClass
  {
      static
      {
          System.out.println("ConstClass init!");
      }
      public static  final String HELLOWORLD = "hello world";
  }
  public class NotInitialization
  {
      public static void main(String[] args)
      {
          System.out.println(ConstClass.HELLOWORLD);
      }
  }
  

運行結果:hello world

總結

本文主要是摘抄了《深入理解Java虛擬機》的相關章節,詳細介紹了Java類加載過程。

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