JVM09-類加載過程

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

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

加載

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

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

驗證

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

驗證字節(jié)流是否符合Class文件格式的規(guī)范;例如:是否以魔術(shù)0xCAFEBABE開頭、主次版本號是否在當(dāng)前虛擬機(jī)的處理范圍 之內(nèi)、常量池中的常量是否有不被支持的類型。
元數(shù)據(jù)驗證

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

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

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

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

public static int value=123


那變量value在準(zhǔn)備階段過后的初始值為0而不是123。因為這時候尚未開始執(zhí)行任何Java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器()方法之中,所以把value賦值為123的動作將在初始化階段才會執(zhí)行。至于“特殊情況”是指:public static final int value=123,即當(dāng)類字段的字段屬性是ConstantValue時,會在準(zhǔn)備階段初始化為指定的值,
所以標(biāo)注為final之后,value的值在準(zhǔn)備階段初始化為123而非0。
解析

解析階段是Java虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、
方法句柄和調(diào)用點限定符等7類符號引用進(jìn)行,分別對應(yīng)常量池的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): 符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可。符號引用與虛擬機(jī)實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定是已經(jīng)加載到虛擬機(jī)內(nèi)存當(dāng)中的內(nèi)容。
直接引用: 直接引用是可以直接指向目標(biāo)的指針、相對偏移量或者一個能間接定位到目標(biāo)的句柄。直接引用是和虛擬機(jī)實現(xiàn)的內(nèi)存布局直接相關(guān)的。同一個符合引用在不同虛擬機(jī)實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在虛擬機(jī)的內(nèi)存中存在。
初始化

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

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

 

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

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

  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();
      }
  }

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

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

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

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

    

運行結(jié)果:

  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


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

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

開篇已經(jīng)舉了一個范例:通過子類引用賦了的靜態(tài)字段,不會導(dǎo)致子類初始化。
這里再舉兩個例子。
1、通過數(shù)組定義來引用類,不會觸發(fā)此類的初始化:(SuperClass類已經(jīng)在本文開篇定義)

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



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

  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);
      }
  }
 



運行結(jié)果:hello world
總結(jié)

本文主要是摘抄了《深入理解Java虛擬機(jī)》的相關(guān)章節(jié),詳細(xì)介紹了Java類加載過程。



作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥