JVM:早期(編譯期)優(yōu)化的深入理解
早期(編譯期)優(yōu)化
JVM的編譯器可以分為三個(gè)編譯器:
- 前端編譯器:把*.java轉(zhuǎn)變?yōu)?.class的過程。如Sun的Javac、Eclipse JDT中的增量式編譯器(ECJ)
- JIT編譯器:把字節(jié)碼轉(zhuǎn)變?yōu)闄C(jī)器碼的過程,如HotSpot VM的C1、C2編譯器
- AOT編譯器:靜態(tài)提前編譯器,直接將*.java文件編譯本地機(jī)器代碼的過程
本章的后續(xù)文字里,“編譯期”和“編譯器”都僅限于第一類編譯過程
1、Javac編譯器
Javac編譯器本身就是一個(gè)由Java語言編寫的程序
1)、Javac的源碼與調(diào)試
Javac的源碼存放在JDK_SRC_HOME/langtools/src/share/slasses/com/sun/tools/javac中
編譯過程大致可以分為3個(gè)過程:
- 解析與填充符號表過程
- 插入式注解處理器的注解處理過程
- 分析與字節(jié)碼生成過程
這3個(gè)步驟之間的關(guān)系與交互順序如下:


2)、解析與符號填充表
解析步驟由parseFiles()方法完成,解析步驟包括了詞法分析和語法分析兩個(gè)過程
A.詞法分析與語法分析
詞法分析:將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記(Token)集合,單個(gè)字符是程序編寫過程的最小元素,而標(biāo)記則是編譯過程的最小元素,關(guān)鍵字、變量名、字面量、運(yùn)算符都可以成為標(biāo)記,在Javac的源碼中,詞法分析過程由com.sun.tools.javac.parser.Scanner類來實(shí)現(xiàn)。
語法分析是根據(jù)Token序列構(gòu)造抽象語法樹的過程,抽象語法樹是一種用來描述程序代碼語法結(jié)構(gòu)的樹形表述方式。語法樹的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語法結(jié)構(gòu),例如包、類型、修飾符、接口、返回值甚至代碼注釋都可以是一個(gè)語法結(jié)構(gòu)。語法分析過程由com.sun.tools.javac.parser.Parser類實(shí)現(xiàn),這個(gè)階段產(chǎn)出的抽象語法樹由com.sun.tools.javac.tree.JCTree類表示,經(jīng)過這個(gè)步驟之后,編譯器就基本不會再對源碼文件進(jìn)行操作了,后續(xù)的操作都是建立在抽象語法樹之上的
B.填充符號表
完成抽象語法樹之后,下一步就是填充符號表的過程,即enterTrees()方法。符號表是由一組符號地址和符號信息構(gòu)成的表格,類似于哈希表中K-V值對的形式。符號表中所登記的信息在編譯的不同階段都要用到。當(dāng)對符號名進(jìn)行地址分配時(shí),符號表是地址分配的依據(jù)。填充過程由com.sun.tools.javac.comp.Enter類實(shí)現(xiàn)
3)、注解處理器
JDK1.5之后,Java提供了對注解的支持,這些注解與普通的Java代碼一樣,在運(yùn)行期間發(fā)揮作用。 有了編譯器注解處理的標(biāo)準(zhǔn)API后,我們的代碼才有可能干涉編譯器的行為,由于語法樹中的任意元素,甚至包括代碼注釋都可以在插件之中訪問到,所以使用插入式注解處理器在功能上有很大的發(fā)揮空間
4)、語義分析與字節(jié)碼生成
語義分析的主要任務(wù)是對結(jié)構(gòu)上正確的源程序進(jìn)行上下文有關(guān)性質(zhì)的審查,如進(jìn)行類型審查
A.標(biāo)注檢查
Javac的編譯過程中,語義分析過程分為標(biāo)注檢查以及數(shù)據(jù)及控制流分析兩個(gè)步驟,分別是attribute()和flow()方法
標(biāo)準(zhǔn)檢查步驟檢查的內(nèi)容包括諸如變量使用前是否已被聲明、變量與賦值之間的數(shù)據(jù)類型是否能夠匹配等。在標(biāo)準(zhǔn)檢查步驟中,還有一個(gè)重要的動作稱為常量折疊
int a = 1 + 2;
語法樹上仍然能看到字面量“1”、“2”以及操作符“+”,但是在經(jīng)過常量折疊以后,它們將會被折疊為字面量“3”。由于編譯期間進(jìn)行了常量折疊,所以在代碼里面定義“a=1+2”比起直接定義“a=3”,并不會增加程序運(yùn)行期哪怕僅僅一個(gè)CPU指令的運(yùn)算量
標(biāo)注檢查步驟在Javac源碼中的實(shí)現(xiàn)類是com.xun.tools.javac.comp.Attr和com.sun.tools.javac.comp.Check類
B.數(shù)據(jù)及控制流分析
數(shù)據(jù)及控制流分析可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問題
局部變量與字段(實(shí)例變量、類變量)是有區(qū)別的,它在常量池中沒有CONSTANT_Fielddref_info的符號引用,自然就沒有訪問標(biāo)志的信息,因此,將局部變量聲明為final,對運(yùn)行期是沒有影響的,變量的不變性僅僅由編譯器在編譯期間保障。在Javac的源碼中,數(shù)據(jù)及控制流分析的入口是flow()方法,具體操作由com.sun.tools.javac.comp.Flow類來完成
C.解語法糖
語法糖是指在計(jì)算機(jī)語言中添加某種語法,這種語法對語言的功能并沒有影響,但是更方便程序員使用
Java是一種“低糖語言”,常用的語法糖主要是之前提到的泛型、變長參數(shù)、自動裝箱/拆箱等。虛擬機(jī)運(yùn)行時(shí)不支持這些語法,它們在編譯期還原回簡單的基礎(chǔ)語法結(jié)構(gòu),這個(gè)過程稱為解語法糖。解語法糖的過程是由desuger()方法觸發(fā)的
D.字節(jié)碼生成
字節(jié)碼生成是Javac編譯過程的最后一個(gè)階段,由com.sun.tools.javac.jvm,Gen類來完成,字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟所生成的信息(語法樹、符號表)轉(zhuǎn)化為字節(jié)碼寫入磁盤中,編譯器還進(jìn)行了少量代碼添加和轉(zhuǎn)換工作
實(shí)例構(gòu)造器<init>()方法和類構(gòu)造器<client>()方法就是在這個(gè)階段添加到語法樹之中的,這兩個(gè)構(gòu)造器的產(chǎn)生過程實(shí)際上是一個(gè)代碼收斂的過程,編譯器會把語句塊(對于實(shí)例構(gòu)造器而言是{}塊,對于類構(gòu)造器而言是static{}塊)、變量初始化(實(shí)例變量和類變量)、調(diào)用父類的實(shí)例構(gòu)造器等操作收斂到<init>()和<client>()方法之中,并且保證一定是按先執(zhí)行父類的實(shí)例構(gòu)造器,然后初始化變量,最后執(zhí)行語句塊的順序進(jìn)行,上面所述的動態(tài)由Gen.normalizeDefs()方法來實(shí)現(xiàn)
完成對語法樹的遍歷與調(diào)整之后,就會把填充了所有所需信息的符號表交給com.sun.tools.javac.jvm.ClassWriter類,由這個(gè)類的wrtieClass()方法輸出字節(jié)碼,生成最終的Class文件
2、Java語法糖的味道
1)、泛型與類型擦除
泛型是JDK1.5的一項(xiàng)新增特性,它的本質(zhì)是參數(shù)化類型的應(yīng)用,也就是說所操作的數(shù)據(jù)類型被指定為一個(gè)參數(shù)。這種參數(shù)類型可以用在類、接口和方法的創(chuàng)建中,分別稱為泛型類、泛型接口和泛型方法
Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯后的字節(jié)碼文件中,就已經(jīng)替換為原來的原生類型了,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼,因此,對于運(yùn)行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個(gè)類,所以泛型技術(shù)實(shí)際上是Java語言的一顆語法糖,Java語言中的泛型實(shí)現(xiàn)方法稱為類型擦除,基于這種方法實(shí)現(xiàn)的泛型稱為偽泛型
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文件,然后再用字節(jié)碼反編譯工具進(jìn)行反編譯后,代碼如下:
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?"));
}
當(dāng)泛型遇到重載:
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)");
}
這段代碼是不能被編譯的,因此參數(shù)List<String>和List<Integer>編譯之后都被擦除了,變成了一樣的原生類型List<E>,擦除動作導(dǎo)致這兩種方法的特征簽名變得一模一樣
2)、自動裝箱、拆箱與遍歷循環(huán)
自動裝箱、拆箱在編譯之后被轉(zhuǎn)化成了對應(yīng)的包裝和還原方法,遍歷循環(huán)則把代碼還原成了迭代器的實(shí)現(xiàn),這也是為何遍歷循環(huán)需要被遍歷的類實(shí)現(xiàn)Iterable接口的原因,變長參數(shù)在調(diào)用的時(shí)候變成了一個(gè)數(shù)組類型的參數(shù)
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
}
包裝類的“==”運(yùn)算在不遇到算術(shù)運(yùn)算的情況下不會自動拆箱,以及它們equals()方法不處理數(shù)據(jù)轉(zhuǎn)型的關(guān)系
3)、條件編譯
Java語言使用條件為常量的if語句,此代碼中的if語句不同于其他Java代碼,它在編譯階段就會被運(yùn)行,生成的字節(jié)碼之中只包含條件正確的部分
public static void main(String[] args) {
if (true) {
System.out.println("block 1");
} else {
System.out.println("block 2");
}
}
Java語言中條件編譯的實(shí)現(xiàn),也是Java語言的一顆語法糖,根據(jù)布爾常量值的真假,編譯器將會把分支中不成立的代碼塊消除掉,這是在解語法糖階段實(shí)現(xiàn)的
Java語言中還有不少的其他語言糖,如內(nèi)部類、枚舉類、斷言語句、對枚舉和字符串的switch支持、try語句中定義和關(guān)閉資源等等
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對腳本之家的支持。如果你想了解更多相關(guān)內(nèi)容請查看下面相關(guān)鏈接
- JVM:晚期(運(yùn)行期)優(yōu)化的深入理解
- JVM進(jìn)階教程之字段訪問優(yōu)化淺析
- 優(yōu)化Java虛擬機(jī)總結(jié)(jvm調(diào)優(yōu))
- Java虛擬機(jī)JVM優(yōu)化實(shí)戰(zhàn)的過程全記錄
- JVM角度調(diào)試優(yōu)化MyEclipse
- Java虛擬機(jī)JVM性能優(yōu)化(三):垃圾收集詳解
- Java虛擬機(jī)JVM性能優(yōu)化(二):編譯器
- Java虛擬機(jī)JVM性能優(yōu)化(一):JVM知識總結(jié)
- 最全面的JVM優(yōu)化經(jīng)驗(yàn)總結(jié)
相關(guān)文章
springboot連接不同數(shù)據(jù)庫的寫法詳解
這篇文章主要介紹了springboot連接不同數(shù)據(jù)庫的寫法?,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04
Spring MVC 更靈活的控制 json 返回問題(自定義過濾字段)
本篇文章主要介紹了Spring MVC 更靈活的控制 json 返回問題(自定義過濾字段),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02
Windows下將JAVA?jar注冊成windows服務(wù)的方法
這篇文章主要介紹了Windows下將JAVA?jar注冊成windows服務(wù)的方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07
詳解spring封裝hbase的代碼實(shí)現(xiàn)
本篇文章主要介紹了詳解spring封裝hbase的代碼實(shí)現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-05-05
java使用文件流實(shí)現(xiàn)查看下載次數(shù)
這篇文章主要為大家詳細(xì)介紹了java使用文件流實(shí)現(xiàn)查看下載次數(shù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
Java實(shí)戰(zhàn)之客戶信息管理系統(tǒng)
這篇文章主要介紹了Java實(shí)戰(zhàn)之客戶信息管理系統(tǒng),文中有非常詳細(xì)的代碼示例,對正在學(xué)習(xí)java的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04
spring boot啟動時(shí)mybatis報(bào)循環(huán)依賴的錯誤(推薦)
今天小編抽時(shí)間給大家分享spring boot啟動時(shí)mybatis報(bào)循環(huán)依賴的錯誤,非常不錯,具有參考借鑒價(jià)值,需要的朋友參考下吧2017-12-12

