JVM:早期(編譯期)優化的深入理解

今天小編就爲大家分享一篇關於JVM:早期(編譯期)優化的深入理解,小編覺得內容挺不錯的,現在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧

早期(編譯期)優化

JVM的編譯器可以分爲三個編譯器:

  1. 前端編譯器:把*.java轉變爲*.class的過程。如Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ)
  2. JIT編譯器:把字節碼轉變爲機器碼的過程,如HotSpot VM的C1、C2編譯器
  3. AOT編譯器:靜態提前編譯器,直接將*.java文件編譯本地機器代碼的過程

本章的後續文字裏,“編譯期”和“編譯器”都僅限於第一類編譯過程

1、Javac編譯器

Javac編譯器本身就是一個由Java語言編寫的程序

1)、Javac的源碼與調試

Javac的源碼存放在JDK_SRC_HOME/langtools/src/share/slasses/com/sun/tools/javac中

編譯過程大致可以分爲3個過程:

  • 解析與填充符號表過程
  • 插入式註解處理器的註解處理過程
  • 分析與字節碼生成過程

這3個步驟之間的關係與交互順序如下:

2)、解析與符號填充表

解析步驟由parseFiles()方法完成,解析步驟包括了詞法分析和語法分析兩個過程

A.詞法分析與語法分析

詞法分析:將源代碼的字符流轉變爲標記(Token)集合,單個字符是程序編寫過程的最小元素,而標記則是編譯過程的最小元素,關鍵字、變量名、字面量、運算符都可以成爲標記,在Javac的源碼中,詞法分析過程由com.sun.tools.javac.parser.Scanner類來實現。

語法分析是根據Token序列構造抽象語法樹的過程,抽象語法樹是一種用來描述程序代碼語法結構的樹形表述方式。語法樹的每一個節點都代表着程序代碼中的一個語法結構,例如包、類型、修飾符、接口、返回值甚至代碼註釋都可以是一個語法結構。語法分析過程由com.sun.tools.javac.parser.Parser類實現,這個階段產出的抽象語法樹由com.sun.tools.javac.tree.JCTree類表示,經過這個步驟之後,編譯器就基本不會再對源碼文件進行操作了,後續的操作都是建立在抽象語法樹之上的

B.填充符號表

完成抽象語法樹之後,下一步就是填充符號表的過程,即enterTrees()方法。符號表是由一組符號地址和符號信息構成的表格,類似於哈希表中K-V值對的形式。符號表中所登記的信息在編譯的不同階段都要用到。當對符號名進行地址分配時,符號表是地址分配的依據。填充過程由com.sun.tools.javac.comp.Enter類實現

3)、註解處理器

JDK1.5之後,Java提供了對註解的支持,這些註解與普通的Java代碼一樣,在運行期間發揮作用。 有了編譯器註解處理的標準API後,我們的代碼纔有可能干涉編譯器的行爲,由於語法樹中的任意元素,甚至包括代碼註釋都可以在插件之中訪問到,所以使用插入式註解處理器在功能上有很大的發揮空間

4)、語義分析與字節碼生成

語義分析的主要任務是對結構上正確的源程序進行上下文有關性質的審查,如進行類型審查

A.標註檢查

Javac的編譯過程中,語義分析過程分爲標註檢查以及數據及控制流分析兩個步驟,分別是attribute()flow()方法

標準檢查步驟檢查的內容包括諸如變量使用前是否已被聲明、變量與賦值之間的數據類型是否能夠匹配等。在標準檢查步驟中,還有一個重要的動作稱爲常量摺疊

int a = 1 + 2;

語法樹上仍然能看到字面量“1”、“2”以及操作符“+”,但是在經過常量摺疊以後,它們將會被摺疊爲字面量“3”。由於編譯期間進行了常量摺疊,所以在代碼裏面定義“a=1+2”比起直接定義“a=3”,並不會增加程序運行期哪怕僅僅一個CPU指令的運算量

標註檢查步驟在Javac源碼中的實現類是com.xun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check類

B.數據及控制流分析

數據及控制流分析可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問題

局部變量與字段(實例變量、類變量)是有區別的,它在常量池中沒有CONSTANT_Fielddref_info的符號引用,自然就沒有訪問標誌的信息,因此,將局部變量聲明爲final,對運行期是沒有影響的,變量的不變性僅僅由編譯器在編譯期間保障。在Javac的源碼中,數據及控制流分析的入口是flow()方法,具體操作由com.sun.tools.javac.comp.Flow類來完成

C.解語法糖

語法糖是指在計算機語言中添加某種語法,這種語法對語言的功能並沒有影響,但是更方便程序員使用
Java是一種“低糖語言”,常用的語法糖主要是之前提到的泛型、變長參數、自動裝箱/拆箱等。虛擬機運行時不支持這些語法,它們在編譯期還原回簡單的基礎語法結構,這個過程稱爲解語法糖。解語法糖的過程是由desuger()方法觸發的

D.字節碼生成

字節碼生成是Javac編譯過程的最後一個階段,由com.sun.tools.javac.jvm,Gen類來完成,字節碼生成階段不僅僅是把前面各個步驟所生成的信息(語法樹、符號表)轉化爲字節碼寫入磁盤中,編譯器還進行了少量代碼添加和轉換工作

實例構造器<init>()方法和類構造器<client>()方法就是在這個階段添加到語法樹之中的,這兩個構造器的產生過程實際上是一個代碼收斂的過程,編譯器會把語句塊(對於實例構造器而言是{}塊,對於類構造器而言是static{}塊)、變量初始化(實例變量和類變量)、調用父類的實例構造器等操作收斂到<init>()<client>()方法之中,並且保證一定是按先執行父類的實例構造器,然後初始化變量,最後執行語句塊的順序進行,上面所述的動態由Gen.normalizeDefs()方法來實現

完成對語法樹的遍歷與調整之後,就會把填充了所有所需信息的符號表交給com.sun.tools.javac.jvm.ClassWriter類,由這個類的wrtieClass()方法輸出字節碼,生成最終的Class文件

2、Java語法糖的味道

1)、泛型與類型擦除

泛型是JDK1.5的一項新增特性,它的本質是參數化類型的應用,也就是說所操作的數據類型被指定爲一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱爲泛型類、泛型接口和泛型方法

Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯後的字節碼文件中,就已經替換爲原來的原生類型了,並且在相應的地方插入了強制轉型代碼,因此,對於運行期的Java語言來說,ArrayList<int>ArrayList<String>就是同一個類,所以泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型稱爲僞泛型

 public static void main(String[] args) {
 Map<String, String> map = new HashMap<String, String>();
 map.put("hello", "你好");
 map.put("how are you?", "吃了沒?");
 System.out.println(map.get("hello"));
 System.out.println(map.get("how are you?"));
 }

把這段Java代碼編譯成Class文件,然後再用字節碼反編譯工具進行反編譯後,代碼如下:

 public static void main(String[] paramArrayOfString)
 {
  HashMap localHashMap = new HashMap();
  localHashMap.put("hello", "你好");
  localHashMap.put("how are you?", "吃了沒?");
  System.out.println((String)localHashMap.get("hello"));
  System.out.println((String)localHashMap.get("how are you?"));
 }

當泛型遇到重載:

 public static String method(List<String> list) {
 System.out.println("invoke method(List<String> list)");
 }
 public static int method(List<Integer> list) {
 System.out.println("invoke method(List<Integer> list)");
 }

這段代碼是不能被編譯的,因此參數List<String>List<Integer>編譯之後都被擦除了,變成了一樣的原生類型List<E>,擦除動作導致這兩種方法的特徵簽名變得一模一樣

2)、自動裝箱、拆箱與遍歷循環

自動裝箱、拆箱在編譯之後被轉化成了對應的包裝和還原方法,遍歷循環則把代碼還原成了迭代器的實現,這也是爲何遍歷循環需要被遍歷的類實現Iterable接口的原因,變長參數在調用的時候變成了一個數組類型的參數

 public static void main(String[] args) {
 Integer a = 1;
 Integer b = 2;
 Integer c = 3;
 Integer d = 3;
 Integer e = 321;
 Integer f = 321;
 Long g = 3L;
 System.out.println(c == d);// true
 System.out.println(e == f);// false
 System.out.println(c == (a + b));// true
 System.out.println(c.equals(a + b));// true
 System.out.println(g == (a + b));// true
 System.out.println(g.equals(a + b));// false
 }

包裝類的“==”運算在不遇到算術運算的情況下不會自動拆箱,以及它們equals()方法不處理數據轉型的關係

3)、條件編譯

Java語言使用條件爲常量的if語句,此代碼中的if語句不同於其他Java代碼,它在編譯階段就會被運行,生成的字節碼之中只包含條件正確的部分

 public static void main(String[] args) {
 if (true) {
  System.out.println("block 1");
 } else {
  System.out.println("block 2");
 }
 }

Java語言中條件編譯的實現,也是Java語言的一顆語法糖,根據布爾常量值的真假,編譯器將會把分支中不成立的代碼塊消除掉,這是在解語法糖階段實現的

Java語言中還有不少的其他語言糖,如內部類、枚舉類、斷言語句、對枚舉和字符串的switch支持、try語句中定義和關閉資源等等

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對神馬文庫的支持。如果你想了解更多相關內容請查看下面相關鏈接

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