深刻理解Java虛擬機讀書筆記-第10章 前端編譯與優化

第10章 前端編譯與優化

10.1 概述

先明確幾個概念 即時編譯器(JIT編譯器,Just In Time Compiler),運行期把字節碼變成本地代碼的過程。 提早編譯器(AOT編譯器,Ahead Of Time Compiler),直接把程序編譯成與目標及其指令集相關的二進制代碼的過程。html

這裏討論的「前端編譯器」,是指把*.java文件轉換成*.class文件的過程,主要指的是javac編譯器。前端

10.2 Javac編譯器

10.2.1 介紹

Javac編譯器是由Java語言編寫。分析Javac代碼的整體結構來看,編譯過程大體分爲1個準備過程和3個處理過程。以下:java

  • 1)準備過程:初始化插入式註解處理器
  • 2)解析與填充符號表
    • 詞法、語法分析。將源代碼的字符流轉變爲標記集合,構造出抽象語法樹。
    • 填充符號表。產生符號地址和符號信息。
  • 3)插入式註解處理器的註解處理
  • 4)分析與字節碼生成
    • 標註檢查。對語法的靜態信息進行檢查。
    • 數據流及控制流分析。對程序動態運行過程進行檢查
    • 解語法糖。將語法糖代碼還原爲原有形式
    • 字節碼生成。將前面各個步驟生成的信息轉化爲字節碼。

若是註解處理產生新的符號,又會再次進行解析填充過程。 截屏2020-08-28上午10.10.16.png Javac編譯動做的入口com.sun.tools.javac.main.JavaCompiler類。代碼邏輯主要所在方法compile(),compile2() 截屏2020-08-28上午11.24.33.png編程

10.2.2 解析和填充符號表

1. 詞法、語法分析

對應parserFiles()方法 詞法分析:源碼字符流轉變爲標記(Token)集合的過程。標記是編譯時的最小元素。關鍵字、變量名、字面量、運算符都是能夠做爲標記。 如「int a = b + 2」, 包含了6個標記,int,a , =, b, +, 2 。詞法分析由com.sun.tools.javac.parser.Scanner實現。 語法分析:根據標記序列構造抽象語法樹的過程。抽象語法樹(Abstract Syntax Tree,AST),描述代碼語法結構的樹形表示形式,樹的每一個節點都表明一個語法結構,例如包,類型,運算符,接口,返回值等等。com.sun.tools.javac.parser.Parser實現。抽象語法樹是以com.sun.tools.javac.tree.JCTree類表示。 後續的操做創建在抽象語法樹之上。數組

2.填充符號表

對應enterTree()方法。markdown

10.2.3 註解處理器

JDK6,JSR-269提案,「插入式註解處理器」API。提早至編譯期對特定註解進行處理,能夠理解成編譯器插件,容許讀取、修改、添加抽象語法樹中的任意元素。若是產生改動,編譯器將回到解析及填充符號表過程從新處理,直到不產生改動。每一次循環過程稱爲一個輪次(Round).
使用註解處理器能夠作不少事情,譬如Lombok,能夠經過註解自動生成getter/setter方法、空檢查、產生equals()和hashCode()方法。
複製代碼

10.2.4 語義分析與字節碼生成

抽象語法樹可以表示一個正確的源程序,但沒法保證語義符合邏輯。語義分析的主要任務是進行類型檢查、控制流檢查、數據流檢查等等。 例如oop

int a = 1;
boolean b = false;
char c = 2;

//後續可能出現的運算,都是能生成抽象語法樹的,但只有第一條,能經過語義分析
int  d= a + c;
int  d= b + c;
char d= a + c;
複製代碼

在IDE中看到的紅線標註的錯誤提示,絕大部分來源於語義分析階段的結果。優化

1. 標註檢查

attribute()方法,檢查變量使用前是否已被聲明,變量與賦值的數據類型是否匹配等等。 3個變量的定義屬於標註檢查。標註檢查順便會進行極少許的一些優化,好比常量摺疊(Constant Folding).ui

int a = 1 + 2; 實際會被摺疊成字面量「3複製代碼

2. 數據及控制流分析

flow()方法,上下文邏輯進一步驗證,好比方法每條路徑是否有返回值,數值操做類型是否合理等等。spa

3. 解語法糖

語法糖(Syntactic Sugar),編程術語 Peter J.Landin。減小代碼量,增長程序可讀性。好比Java語言中的泛型(其餘語言的泛型不必定是語法糖實現,好比C#泛型直接有CLR支持),變長數組,自動裝箱拆箱等等。 解語法糖,編譯期將糖語法轉換成原始的基礎語法。

4. 字節碼生成

  • 將前面生成的信息(語法樹,符號表)轉化爲字節碼,
  • 少許代碼添加,(),()等等
  • 少許代碼優化轉換,字符串拼接操做替換爲StringBuffer或StringBuilder等等。

10.3 Java語法糖

10.3.1 泛型

1.Java泛型

JDK5,Java的泛型實現稱爲「類型擦除式泛型」(Type Erasure Generic),相對的C#選擇的是「具現化泛型」(Reified Generics),C#泛型不管在源碼中,仍是編譯後的中間語言表示(此時泛型都是一個佔位符),List<int> 與List<String>是兩個不一樣的類型。而Java泛型,只是在源碼中存在,編譯後都變成了統一的類型,稱之爲類型擦除,在使用處會增長一個強制類型轉換的指令。
複製代碼
Map<String, String> stringMap = new HashMap<String, String>();
stringMap.put("hello", "你好");
System.out.println(stringMap.get("hello"));

Map objeMap = new HashMap();
objeMap.put("hello2", "你好2");
System.out.println((String)objeMap.get("hello2"));
複製代碼

截取部分字節碼

0: new           #2                  // class java/util/HashMap
4: invokespecial #3                  // Method java/util/HashMap."<init>":()V
13: invokeinterface #6,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
25: invokeinterface #8,  2            // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
30: checkcast     #9                  // class java/lang/String
33: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 
        
36: new           #2                  // class java/util/HashMap
40: invokespecial #3                  // Method java/util/HashMap."<init>":()V
49: invokeinterface #6,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
61: invokeinterface #8,  2            // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
66: checkcast     #9                  // class java/lang/String
69: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

72: return
複製代碼

能夠看到兩部分代碼在編譯後是同樣的。 第0行,new HashMap<String, String>() 實際構造的是java/util/HashMap。 第30行,stringMap.get("hello") ,checkcast指令,作了一個類型轉換

2. 歷史背景

2004年,Java5.0。爲了保證代碼的「二進制向後兼容」,引入泛型後,原先的代碼必須可以編譯和運行。例如Java數組支持協變,集合類也能夠存入不一樣類型元素。 代碼以下

Object[] array = new String[10]; 
array[0] = 10; // 編譯期不會有問題,運行時會報錯 

ArrayList things = new ArrayList(); 
things.add(Integer.valueOf(10)); //編譯、運行時都不會報錯
things.add("hello world");
複製代碼

若是要保證Java5.0引入泛型後,上述代碼依然能夠運行,有兩個選擇:

  • 原先須要泛型化的類型保持不變,再新增一套泛型化的類型版本。泛型具現化,好比C#新增了一組System.Collections.Generic的新容器,原先的System.Collections保持不變。
  • 把須要泛型化的類型原地泛型化,Java5.0採用的原地泛型化方式爲類型擦除。

爲什麼C#與Java的選擇不一樣,主要是C#當時才2年遺留老代碼少,Java快10年了老代碼多。類型擦除是偷懶留下的技術債。

3.類型擦除

類型擦除除了前面提到的編譯後都變成了統一的裸類型以及使用時的類型檢查和轉換以外還有其餘缺陷。 1)不支持原始類型(Primitive Type)數據泛型,ArrayList須要使用其對應引用類型ArrayList,致使了讀寫的裝箱拆箱。 2)運行期沒法獲取泛型類型信息,例如

public  <E> void doSomething(E item){
        E[] array=new E[10];  //不合法,沒法使用泛型建立數組
        if(item instanceof  E){}//不合法,沒法對泛型進行實例判斷
}
複製代碼

當咱們去寫一個List到數組的轉換方法時,須要額外傳遞一個數組的組件類型

public  static <T> T[] convert(List<T> list,Class<T> componentType){
        T[] array= (T[]) Array.newInstance(componentType,list.size());
        for (int i = 0; i < list.size(); i++) {
            array[i]=list.get(i);
        }
        return array;
}
複製代碼

3)類型轉換問題。

//沒法編譯經過
//雖然String是Object的子類,但ArrayList<String>並非ArrayList<Object>的子類。
ArrayList<Object> list=new ArrayList<String>();
複製代碼

爲了支持協變和逆變,泛型引入了 extends ,super

//協變 
ArrayList<? extends Object> list = new ArrayList<String>();

//逆變
ArrayList<? super String> list2 = new ArrayList<Object>();
複製代碼

4 值類型與將來泛型

2014年,Oracle,Valhalla語言改進項目內容之一,新泛型實現方案

10.3.2 其餘

自動裝箱,自動拆箱,遍歷循環,變長參數,條件編譯,內部類,枚舉類,數值字面量,switch,try等等。

10.3.3 *擴展閱讀

Java協變介紹 Lambda與invokedynamic

10.4 實戰 Lombok註解處理器

相關文章
相關標籤/搜索