1. 程式人生 > >Java原始碼編譯機制、類載入機制、類執行機制

Java原始碼編譯機制、類載入機制、類執行機制

-----------------------------------------------------------------------------------------------------------------

Java程式碼編譯和執行的整個過程包含了以下三個重要的機制:

Java原始碼編譯機制

類載入機制

類執行機制

這裡對其一一簡單的介紹。

-----------------------------------------------------------------------------------------------------------------

1 Java程式碼編譯

  程式碼編譯由JAVA原始碼編譯器來完成。主要是將原始碼編譯成位元組碼檔案(class檔案);位元組碼檔案格式主要分為兩部分:常量池方法位元組碼

  Java程式碼編譯是由Java原始碼編譯器來完成,流程圖如下所示:

  

Java原始碼編譯機制由以下三個過程組成:

  1. 分析和輸入到符號表
  2. 註解處理
  3. 語義分析和生成class檔案

(javac–verbose  輸出有關編譯器正在執行的操作的訊息)

 

最後生成的class檔案由以下部分組成:

結構資訊:包括class檔案格式、版本號、各部分的數量與大小的資訊

元資料:對應於Java原始碼中宣告與常量的資訊。包含類/繼承的超類/實現的介面的宣告資訊、域與方法宣告資訊和常量池

方法資訊:對應Java原始碼中語句和表示式對應的資訊。包含位元組碼、異常處理器表、求值棧與區域性變數區大小、求值棧的型別記錄、除錯符號資訊。

2 類載入機制

2.1 類的生命週期

      類的生命週期由被載入到虛擬機器記憶體中開始,到卸載出記憶體結束,共有七個階段,其中到初始化之前的都是屬於類載入的部分:

                           載入---驗證---準備---解析----初始化---使用---解除安裝

       系統可能在第一次使用某個類時載入該類,也可能採用預載入機制來載入某個類,當執行某個java程式時,會啟動一個java虛擬機器程序,兩次執行的java程式處於兩個不同的JVM程序中,兩個jvm之間並不會共享資料。

1、載入階段

這個流程中的載入是類載入機制中的一個階段,段需要完成的事情有:

1)通過一個類的全限定名來獲取定義此類的二進位制位元組流。

2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。

3)在java堆中生成一個代表這個類的Class物件,作為訪問方法區中這些資料的入口。

由於第一點沒有指明從哪裡獲取以及怎樣獲取類的二進位制位元組流,所以這一塊區域留給我開發者很大的發揮空間。

2、準備階段

這個階段正式為類變數(被static修飾的變數)分配記憶體並設定類變數初始值,這個記憶體分配是發生在方法區中。

1、注意這裡並沒有對例項變數進行記憶體分配,例項變數將會在物件例項化時隨著物件一起分配在JAVA堆中。

2、這裡設定的初始值,通常是指資料型別的零值。

private static int a = 3;

 這個類變數a在準備階段後的值是0,將3賦值給變數a是發生在初始化階段。

3、初始化階段

初始化是類載入機制的最後一步,這個時候才正真開始執行類中定義的JAVA程式程式碼。在前面準備階段,類變數已經賦過一次系統要求的初始值,在初始化階段最重要的事情就是對類變數進行初始化,關注的重點是父子類之間各類資源初始化的順序。

java類中對類變數指定初始值有兩種方式:

1)宣告類變數時指定初始值;

2)使用靜態初始化塊為類變數指定初始值

初始化的時機

1)建立類例項的時候,分別有:1、使用new關鍵字建立例項;2、通過反射建立例項;3、通過反序列化方式建立例項。

new Test();
Class.forName(“com.mengdd.Test”);

2)呼叫某個類的類方法(靜態方法)

Test.doSomething();

3)訪問某個類或介面的類變數,或為該類變數賦值。  

int b=Test.a;
Test.a=b;

4)初始化某個類的子類。當初始化子類的時候,該子類的所有父類都會被初始化。

5)直接使用java.exe命令來執行某個主類。

除了上面幾種方式會自動初始化一個類,其他訪問類的方式都稱不會觸發類的初始化,稱為被動引用。

被動引用的情況

1、子類引用父類的靜態變數,不會導致子類初始化。

publicclass SupClass
{
    public static int a = 123;
   
    static
    {
        System.out.println("supclassinit");
    }
}
 
publicclass SubClass extends SupClass
{
    static
    {
        System.out.println("subclassinit");
    }
}
 
publicclass Test
{
    public static void main(String[] args)
    {
        System.out.println(SubClass.a);
    }
}

執行結果:

supclass init

123

2、引用常量時,不會觸發該類的初始化

用final修飾某個類變數時,它的值在編譯時就已經確定好放入常量池了,所以在訪問該類變數時,等於直接從常量池中獲取,並沒有初始化該類。

初始化機制:

1、如果該類還沒有載入和連線,則程式先載入該類並連線。

2、如果該類的直接父類沒有載入,則先初始化其直接父類。

3、如果類中有初始化語句,則系統依次執行這些初始化語句。

     在第二個步驟中,如果直接父類又有直接父類,則系統會再次重複這三個步驟來初始化這個父類,依次類推,JVM最先初始化的總是java.lang.Object類。當程式主動使用任何一個類時,系統會保證該類以及所有的父類都會被初始化。

2.2 類載入機制

類載入器結構關係

JVM的類載入是通過ClassLoader及其子類來完成的,類的層次關係和載入順序可以由下圖來描述:

 

1)Bootstrap ClassLoader /啟動類載入器 

是ClassLoader子類 ,自身也沒有子類,並且不遵守classLoader載入機制;是JVM核心中的載入器,由C++實現;負責載入$JAVA_HOME中jre/lib/rt.jar裡所有的class。

2)Extension ClassLoader/擴充套件類載入器 

是用JAVA編寫,且它的父載入器是Bootstrap,但是因為BootStrap是用C++寫的,所以有時候也說ExtClassLoader沒有父載入器,自身也是頂層父類,但是血統不純,不全是JVM實現的。

負責載入java平臺中擴充套件功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包

通過程式來看下系統變數java.ext.dirs所指定的路徑:

public class Test

{

   public static void main(String[] args)

   {

       System.out.println(System.getProperty("java.ext.dirs"));

   }

}

執行結果:

C:\Program Files(x86)\Java\jdk1.6.0_43\jre\lib\ext;C:\Windows\Sun\Java\lib\ext

3)App ClassLoader/ 系統類載入器

也稱為應用程式類載入器,負責載入應用程式classpath目錄下的所有jar和class檔案。它的父載入器為Ext ClassLoader。

4)Custom ClassLoader/使用者自定義類載入器

(java.lang.ClassLoader的子類) 

屬於應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader 

這幾種類載入器的層次關係如下圖所示:

                          

類載入機制

類載入機制的特點:

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

父類委託,先讓父類載入器試圖載入該類,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類

快取機制,快取機制將會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區尋找該Class,只有快取區不存在,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入快取區。這就是為什麼修改了Class後,必須重啟JVM,程式的修改才會生效

由上述分析可知,類的載入機制採用的是一種父類委託機制,也叫作雙親委派機制或者父優先等級載入機制

     如果一個類載入器收到了一個類載入請求,它不會自己去嘗試載入這個類,而是首先會自下而上的檢查該類是否已被載入,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已載入就視為已載入此類,並將結果逐層向下反饋;如果沒有載入,則繼續向上層檢查,所有的類載入請求都應該傳遞到最頂層的啟動類載入器中,只有到父類載入器反饋自己無法完成這個載入請求(在它的搜尋範圍沒有找到這個類)時,子類載入器才會嘗試自己去載入,這種委派機制的好處就是保證了一個類不被重複載入。

              所以說,類載入檢查順序是自下而上,而載入的順序是自頂向下,也就是由上層來逐層嘗試載入類。

這種類載入機制的實現比較簡單,原始碼如下:

protectedsynchronized Class<?> loadClass(String paramString, boolean paramBoolean)
    throws ClassNotFoundException
  {
       //檢查是否被載入過
    Class localClass =findLoadedClass(paramString);
       //如果沒有載入,則呼叫父類載入器
    if (localClass == null) {
      try {
           //父類載入器不為空
        if (this.parent != null)
          localClass = this.parent.loadClass(paramString,false);
        else {
      //父類載入器為空,則使用啟動類載入器,傳統意義上啟動類載入器沒有父類載入器
          localClass =findBootstrapClass0(paramString);
        }
      }
      catch (ClassNotFoundExceptionlocalClassNotFoundException)
      {
           //如果父類載入失敗,則使用自己的findClass方法進行載入
        localClass = findClass(paramString);
      }
    }
    if (paramBoolean) {
      resolveClass(localClass);
    }
    return localClass;
  }
 

程式碼大意就是先檢查是否已經被載入過,若沒有載入則呼叫父類載入器的loadClass方法,若父類載入器不存在,則使用啟動類載入器。如果父類載入器載入失敗,則丟擲異常之後看,再呼叫自己定義的的findClass方法進行載入。

2.3 自定義類載入器

      通常情況下,我們都是直接使用系統類載入器。但是,有的時候,我們也需要自定義類載入器。比如應用是通過網路來傳輸 Java類的位元組碼,為保證安全性,這些位元組碼經過了加密處理,這時系統類載入器就無法對其進行載入,這樣則需要自定義類載入器來實現。自定義類載入器一般都是繼承自 ClassLoader,從上面對 loadClass方法來分析來看,我們只需要重寫 findClass方法即可。

       下面我們通過一個示例來演示自定義類載入器的流程:

public class MyClassLoader extendsClassLoader { 
   private String root; 
   protected Class<?> findClass(String name) throwsClassNotFoundException {
       byte[] classData = loadClassData(name);
       if (classData == null) {
           throw new ClassNotFoundException();
       } else {
            return defineClass(name, classData, 0,classData.length);
       }
    }
 
   private byte[] loadClassData(String className) {
       String fileName = root + File.separatorChar
                + className.replace('.',File.separatorChar) + ".class";
       try {
           InputStream ins = new FileInputStream(fileName);
           ByteArrayOutputStream baos = new ByteArrayOutputStream();
           int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
           int length = 0;
           while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
           }
           return baos.toByteArray();
       } catch (IOException e) {
           e.printStackTrace();
       }
       return null;
    }
 
   public String getRoot() {
       return root;
    }
 
   public void setRoot(String root) {
       this.root = root;
    }
 
   public static void main(String[] args) {
 
       MyClassLoader classLoader = new MyClassLoader();
       classLoader.setRoot("E:\\temp");
 
       Class<?> testClass = null;
       try {
           testClass =classLoader.loadClass("com.neo.classloader.Test2");
           Object object = testClass.newInstance();
           System.out.println(object.getClass().getClassLoader());
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       }
    }
}

       自定義類載入器的核心在於對位元組碼檔案的獲取,如果是加密的位元組碼則需要在該類中對檔案進行解密。由於這裡只是演示,並未對class檔案進行加密,因此沒有解密的過程。這裡有幾點需要注意:

1)這裡傳遞的檔名需要是類的全限定性名稱,即com.paddx.test.classloading.Test格式的,因為 defineClass 方法是按這種格式進行處理的。

2)最好不要重寫loadClass方法,因為這樣容易破壞雙親委託模式。

3)這類Test 類本身可以被 AppClassLoader 類載入,因此我們不能把 com/paddx/test/classloading/Test.class 放在類路徑下。否則,由於雙親委託機制的存在,會直接導致該類由 AppClassLoader 載入,而不會通過我們自定義類載入器來載入。

3 類執行機制

Java位元組碼的執行是由JVM執行引擎來完成,流程圖如下所示:

                 

       JVM是基於棧的體系結構來執行class位元組碼的。

執行緒建立後,都會產生一個執行緒私有的程式計數器(PC暫存器)和棧(Stack)

程式計數器存放程式正常執行時下一條要執行的指令在方法內的偏移量地址;

中存放一個個棧幀,各個方法每呼叫一次就會建立一個自己私有的棧幀,棧幀分為區域性變量表、運算元棧、動態連線、方法返回地址和一些附加資訊

1) 區域性變數區是一組變數值儲存空間,用於存放方法中的引數、區域性變數;

區域性變量表的容量以變數槽(slot)為最小單位,一個slot可以存放一個32位以內的資料型別,而Java中佔32位以內的資料型別有boolean、byte、char、short、int、float、reference(也可以64位)和returnAddress八種類型

Java語句中明確規定的64位的資料型別只有long和double兩種(reference可能是32位,也可能是64位)故long和double不是原子操作,只是區域性變量表建立線上程的堆疊上,是執行緒私有的資料,無論讀寫兩個連續的slot是否是原子操作,都不會引起資料安全問題

2) 運算元棧中用於存放方法執行過程中產生的中間結果。

3) 動態連線

符號引用一部分會在類載入階段或第一次使用的時候轉換成為直接引用,這種轉換稱為靜態解析。另外一部分將在每一次的執行期間轉換為直接引用,這部分稱為動態引用

4)方法返回地址

當一個方法被執行後,有兩種方式退出這個方法。

第一種是執行引擎,遇到一個方法返回的位元組碼指令,這時可能會返回值傳遞給上層的方法呼叫者。這種退出方式為正常完成出口

另一種是遇到異常並且沒有在方法體內得到處理(throws不屬於方法體內處理),這種退出方式是不會給它的上層呼叫者產生任何返回值的。

一般來說,方法正常退出時,呼叫者的PC計數器的值就可以作為返回地址,棧幀中很可能會儲存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會儲存這部分資訊。

方法退出的實質

實際上等同於把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的區域性變量表盒運算元棧,把返回值(如果有的話)壓入呼叫者棧幀的運算元棧中,調整PC計數器的值以指向方法呼叫指令後面的一條指令等

  再續相關部分……