JAVA代碼執行機制

1.java源碼編譯機制

  

   jvm規範中定義了class文件的格式,但並未定義Java源碼如何編譯爲class文件,在Sun JDK中就是javac編譯器,可分爲下面三個步驟:

   1.分析和輸入到符號表(Parse and Enter)

      Parse  過程所做的是詞法和語法分析

      Enter  過程是將符號輸入到符號表   

   2.註解處理(Annotation Processing)

      該步驟主要用於處理用戶自定義的annotation,可能帶來的好處是基於annotation來生成附加的代碼,如:

   public class User{ private @Getter String username; }

      編譯時引入Lombok對User.java進行編譯後,再通過javap查看class文件可看到自動生成了public String getUsername()方法

  3.語義分析和生成class文件(Analyse and Generate)

  

  class 文件中並不僅僅存放了字節碼,還存放了很多輔助jvm來執行class文件的附加信息,一個class文件包含了以下信息:

  •   結構信息:包括class文件格式版本號及各部分的數量與大小的信息
  •   元數據:可以認爲元數據就是java源碼中“聲明”與“常量”信息
  •   方法信息:可以說就是java源碼中“語句”和“表達式”對應的信息

  class文件是個完整的自描述文件,字節碼在其中只佔了很小的部分,源碼編譯爲class文件後,即可放入jvm中執行,執行時jvm首先要做的是裝載class文件,這個就是類加載機制

 

2.類加載機制

 

   jvm將類加載過程劃分爲三個步驟:

 

  1.裝載(Load) 裝載過程負責找到二進制字節碼並加載到jvm中,jvm通過類的全限定義名(com.bluedavy.HelloWord)及類加載器(ClassLoaderA 實例)完成類的加載,同樣,也採用以上兩個素來標識一個被加載了的類:類的全限定名+ClassLoader 實例ID,類名的命名方式如下:

    對於接口式非數組型的類,其名稱即爲類名,此種類型的類由所在的ClassLoader負責加載:數組型類中的元素類型由所在的ClassLoader負責加載,但數組類則由JVM直接創建。

 

  2.鏈接(Link) 鏈接過程負責對二進制字節碼的格式進行校驗、初始化裝載類中的靜態變量及解析類中調用的接口、類。

 

  3.初始化(Initialize)初始化過程即即執行類中的靜態初始化代碼、構造器代碼及靜態屬性的初始化,在下面的四種情況下初始化過程會被觸發執行:

  •   調用了new
  •   反射調用了類中的方法
  •   子類調用了初始化
  •   JVM啓動過程中指定的初始化

  JVM的類加載通過ClassLoader及其子類來完成,分爲Bootstrap ClassLoader、Extensin ClassLoader、System ClassLoader(在Sun JDK中對應的類名爲AppClassLoader)及 User-Defined ClassLoader,其關係如下圖:

 

1.Bootstrap ClassLoader  Sun JDK 採用C++實現了此類,此類並非ClassLoader的子類,在代碼中沒有辦法拿到這個對象,Sun  JDK 啓動時會初始化此ClassLoader,並由ClassLoader完成$JAVA_HOME 中jre/lib/rt.jar裏所有class文件的加載;

2.Extension ClassLoader JVM用此ClassLoader來加載擴展功能的一些jar包,例如JDK中dns工具jar包等,對應的類名爲ExtClassLoader;

3.System ClassLoader JVM 用此ClassLoader來加載啓動參數中指定的Classpath中的jar包及目錄,在Sun JDK中ClassLoader對應的類名爲AppClassLoader;

4.User-Defined ClassLoader 這是開發人員繼承ClassLoader抽象類自行實現的ClassLoader,可以用加載非Classpath中的jar及目錄、還可以在加載之前對class文件做一些動作,例如解密等;

JVM會保證同一個ClassLoader實例對象中只能加載一次同樣名稱的Class

 

Cloader抽象類提供了幾個關鍵的方法

  • loadClass  此方法負責加載指定名字的類,ClassLoader的實現方法爲先從已經加載的類中尋找,如沒有,則繼續從 parent ClassLoader中尋找;如果仍然沒有找到,則從System ClassLoader中尋找,最後再調用findClass方法來尋找;如果最終沒有找到就拋出ClassNotFoundException;
  • findLoaderClass 方法負責從當前ClassLoader實例對象的緩存中尋找已加載的類,調用的爲native的方法;
  • findClass 此方法直接拋出ClassNotFoundException,因此要通過覆蓋loadClass或此方法來以自定義的方式加載相應的類;
  • findSystemClass 此負責從System ClassLoader 中尋找,如未找到,則繼續從Bootstrap ClassLoader 中尋找,如果仍然未找到,則返回null;
  • defineClass 此方法負責將二進制的字節碼轉換成Class對象,如果二進制的字節碼的格式不符合JVM Class文件的格式,則拋出ClassFormatError;如果生成的類名和二進制字節碼中的不同,則拋出NoClassDefFoundError;如果加載的class是受保護的、採用不同簽名的或者類名是以java.開頭的,則拋出SecurityException;如果加載的class在此ClassLoader中已加載,則拋出LinkageError;
  • resolveClass 此方法負責完成Class對象的鏈接,如果鏈接過,則會直接返回;

3.類的執行機制

   在源碼編譯階段源碼編譯爲JVM字節碼,JVM字節碼是一種中間代碼的方式,要由JVM在運行期對其進行解釋並執行,這種方式稱爲字節碼解釋執行方式;

   1.字節碼解釋執行

      JVM採用了invokestatic,invokevirtual,invokeinterface和invokespecial四個指令來執行不同的方法調用。

      invokestatic對應的是調用static方法,invokevirtual對應的是調用對象實例的方法,invokeinterface對應的是調用接口的方法,invokespecial對應的是調用private方法和編譯源碼後生成的<init>方法;

     

      Sun JDK基於棧的體系結構來執行字節碼,基於棧方式的好處爲代碼緊湊,體積小,線程在創建後,都會產生程序計數器(pc)(或稱爲PC registers)和棧(Stack);PC存放了下一條要執行的指令在方法內的偏移量;棧中存放了棧幀(StackFrame),每個方法每次調用都會產生棧幀。棧幀主要分爲局部變量區和操作數棧兩部分,局部變量區用於存放方法中的局部變量和參數,操作數棧中用於存放方法執行過程中產生的中間結果,棧幀中還會有一些雜用空間,例如指向方法已解析的常量池的引用、其他一些JVM內部實現需要的數據等;

 

   2.編譯執行

     解釋執行的效率較低,Sun JDK在執行過程中對執行頻率高的代碼進行編譯,對執行不頻繁的代碼則繼續採用解釋的方式;在編譯上Sun JDK提供了兩種模式:client compiler(-client)和server compiler(-server)。

 

     client compiler又稱爲C1,較爲輕量級,只做少量性能開銷比高的優化,它佔用內存較少,適合於桌面交互式應用,其他方面的優化主要有:

  • 方法內聯 這種方法把調用到的方法的指令直接植入當前方法中,如下代碼:

 

Java代碼 複製代碼 收藏代碼
  1. public void bar(){   
  2.   ....   
  3.   bar2();   
  4.   ....   
  5. }   
  6.   
  7. private void bar2(){   
  8.   //bar2;   
  9. }  
public void bar(){
  ....
  bar2();
  ....
}

private void bar2(){
  //bar2;
}

  上面代碼編譯後就變成類似下面的結構:

 

Java代碼 複製代碼 收藏代碼
  1. public void bar(){   
  2.  ....   
  3.  //bar2   
  4.  ....   
  5. }  
public void bar(){
 ....
 //bar2
 ....
}

 

  • 去虛擬化   去虛擬化是指在裝載class文件後,進行類層次的分析,如發現類中的方法只提供一個實現類,那麼對於調用了此方法的代碼,也可進行方法內聯;
  • 冗餘削除   冗餘削除是指在編譯時,根據運行時狀況進行代碼摺疊或削除,例如一段這樣的代碼:
Java代碼 複製代碼 收藏代碼
  1. public void execute(){   
  2.     if(isDebug){   
  3.          log.debug("enter this method: execute");   
  4.      }   
  5.     //do something   
  6. }  
public void execute(){
    if(isDebug){
         log.debug("enter this method: execute");
     }
    //do something
}

     如isDebug的值是false,在執行C1編譯後,這段代碼就變成類似下面的結構:

Java代碼 複製代碼 收藏代碼
  1. public void execute(){   
  2.     //do something   
  3. }  
public void execute(){
    //do something
}

 

  Server compiler又稱C2,較爲重量級,它採用大量的傳統編譯優化技巧來進行優化,佔用內存相對會多一些,適合於服務器端的應用;逃逸分析是C2進行很多優化的基礎,逃逸分析是指根據運行狀況來判斷方法中的變量是否會被外部讀取,如不會則認爲此變量是逃逸的,基於逃逸分析C2在編譯時會標量替換,棧上分配和同步消除等優化;

  •  標量替換  就是用標量替換聚合量,例如下面這段代碼:
Java代碼 複製代碼 收藏代碼
  1. Point  point=new Point(1,2);   
  2. ystem.out.println("point.x="+point.x+";point.y"+point.y);  
 Point  point=new Point(1,2);
System.out.println("point.x="+point.x+";point.y"+point.y);

      當point對象在後面的執行過程中未用到時,經過編譯後,代碼會變成類似下面的結構:

Java代碼 複製代碼 收藏代碼
  1. int  x=1;   
  2. int  y=2;   
  3. System.out.println("point.x="+x+";point.y"+y);  
int  x=1;
int  y=2;
System.out.println("point.x="+x+";point.y"+y);
  •  棧上分配  在上面的例子中,如果p沒有逃逸,那麼C2會選擇在棧上直接創建Point對象實例,而不是在堆上; 
  •  同步削除  指如果發現同步的對象未逃逸,那也沒有同步的必要了,在C2編譯時會直接去掉同步,例如:
Java代碼 複製代碼 收藏代碼
  1. Point  point=new Point(1,2);   
  2.     synchronized(point) {   
  3.   //do something;   
  4. }  
Point  point=new Point(1,2);
    synchronized(point) {
  //do something;
}

      經過分析如果發現point未逃逸,在編譯後,代碼就會變成下面的結構:

Java代碼 複製代碼 收藏代碼
  1. Point  point = new Point(1,2);   
  2. //do something  
Point  point = new Point(1,2);
//do something

 

Sun JDK之所以未選擇在啓動時即編譯成機器碼,有下面幾個原因:

  1. 靜態編譯並不能根據程序的運行狀況來優化執行的代碼
  2. 解釋執行比編譯執行更節省內存
  3. 啓動解釋執行的啓動速度比編譯再啓動更快
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章