JVM学习-程序编译与优化
原文鏈接:https://gaoyubo.cn/blogs/89d6d9be.html
一、前端編譯與優(yōu)化
Java技術(shù)下討論“編譯期”需要結(jié)合具體上下文語(yǔ)境,因?yàn)樗赡艽嬖诤芏喾N情況:
-
前端編譯器(叫“編譯器的前端”更準(zhǔn)確一些)把
.java文件轉(zhuǎn)變成.class文件的過(guò)程JDK的Javac、Eclipse JDT中的增量式編譯器(ECJ)
-
即時(shí)編譯器(常稱JIT編譯器,Just In Time Compiler)運(yùn)行期把字節(jié)碼轉(zhuǎn)變成本地機(jī)器碼的過(guò)程
HotSpot虛擬機(jī)的C1、C2編譯器,Graal編譯器
-
提前編譯器(常稱AOT編譯器,Ahead Of Time Compiler)直接把程序編譯成與目標(biāo)機(jī)器指令集相關(guān)的二進(jìn)制代碼的過(guò)程
JDK的Jaotc、GNU Compiler for the Java(GCJ)、Excelsior JET 。
本章標(biāo)題中的“前端”指的是由前端編譯器完成的編譯行為,對(duì)于前端編譯優(yōu)化,有以下說(shuō)法:
-
前端編譯器對(duì)代碼的運(yùn)行效率幾乎沒(méi)有任何優(yōu)化措施可言
-
Java虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)選擇把對(duì)性能的優(yōu)化全部集中到運(yùn)行期的即時(shí)編譯器中
這樣可以讓那些不是由Javac產(chǎn)生的Class文件也同樣能享受到編譯器優(yōu)化措施所帶來(lái)的性能紅利
-
相當(dāng)多新生的Java語(yǔ)法特性,都是靠編譯器的“語(yǔ)法糖”來(lái)實(shí)現(xiàn),而不是依賴字節(jié)碼或者Java虛擬機(jī)的底層改進(jìn)來(lái)支持。
-
Java中即時(shí)編譯器在運(yùn)行期的優(yōu)化過(guò)程,支撐了程序執(zhí)行效率的不斷提升;
-
前端編譯器在編譯期的優(yōu)化過(guò)程,支撐著程序員的編碼效率和語(yǔ)言使用者的幸福感的提高
1.1Javac編譯器
從Javac源代碼的總體結(jié)構(gòu)來(lái)看,編譯過(guò)程大致可以分為1個(gè)準(zhǔn)備過(guò)程和3個(gè)處理過(guò)程,它們分別如下所示:
-
準(zhǔn)備過(guò)程:初始化插入式注解處理器
-
解析與填充符號(hào)表過(guò)程,包括:
? 詞法、語(yǔ)法分析:將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記集合,構(gòu)造出抽象語(yǔ)法樹(shù)
? 填充符號(hào)表:產(chǎn)生符號(hào)地址和符號(hào)信息
-
插入式注解處理器的注解處理過(guò)程:插入式注解處理器的執(zhí)行階段
-
分析與字節(jié)碼生成過(guò)程,包括:
標(biāo)注檢查:對(duì)語(yǔ)法的靜態(tài)信息進(jìn)行檢查。
數(shù)據(jù)流及控制流分析:對(duì)程序動(dòng)態(tài)運(yùn)行過(guò)程進(jìn)行檢查。
解語(yǔ)法糖:將簡(jiǎn)化代碼編寫(xiě)的語(yǔ)法糖還原為原有的形式。
字節(jié)碼生成:將前面各個(gè)步驟所生成的信息轉(zhuǎn)化成字節(jié)碼。
-
對(duì)于以上過(guò)程:執(zhí)行插入式注解時(shí)又可能會(huì)產(chǎn)生新的符號(hào),如果有新的符號(hào)產(chǎn)生,就必須轉(zhuǎn) 回到之前的解析、填充符號(hào)表的過(guò)程中重新處理這些新符號(hào)
-
整個(gè)編譯過(guò)程主要的處理由圖中標(biāo)注的8個(gè)方法來(lái)完成
解析和填充符號(hào)表
詞法語(yǔ)法分析
1.詞法分析:詞法分析是將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記(Token)集合的過(guò)程。
2.語(yǔ)法分析:語(yǔ)法分析是根據(jù)標(biāo)記序列構(gòu)造抽象語(yǔ)法樹(shù)的過(guò)程
-
抽象語(yǔ)法樹(shù):抽象語(yǔ)法樹(shù)(Abstract Syntax Tree,AST)是一 種用來(lái)描述程序代碼語(yǔ)法結(jié)構(gòu)的樹(shù)形表示方式,抽象語(yǔ)法樹(shù)的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語(yǔ)法結(jié)構(gòu)
-
包、類型、修飾符、運(yùn)算符、接口、返回值甚至連代碼注釋等都可以是一種特定的語(yǔ)法結(jié)構(gòu)。
-
抽象語(yǔ)法樹(shù)可通過(guò)Eclipse AST View插件查看,抽象語(yǔ)法樹(shù)是以com.sun.tools.javac.tree.JCTree 類表示的
-
經(jīng)過(guò)詞法和語(yǔ)法分析生成語(yǔ)法樹(shù)以后,編譯器就不會(huì)再對(duì)源碼字符流進(jìn)行操作了,后續(xù)的操作都建立在抽象語(yǔ)法樹(shù)之上
填充符號(hào)表
符號(hào)表(Symbol Table)是由一組符號(hào)地址和符號(hào)信息構(gòu)成的數(shù)據(jù)結(jié)構(gòu)(可以理解成哈希表中的鍵值對(duì)的存儲(chǔ)形式)
符號(hào)表中所登記的信息在編譯的不同階段都要被用到:
- 語(yǔ)義分析的過(guò)程中,符號(hào)表所登記的內(nèi)容將用于語(yǔ)義檢查 (如檢查一個(gè)名字的使用和原先的聲明是否一致)和產(chǎn)生中間代碼
- 目標(biāo)代碼生成階段,當(dāng)對(duì)符號(hào)名進(jìn)行地址分配時(shí),符號(hào)表是地址分配的直接依據(jù)。
注解處理器
可以把插入式注解處理器看作是一組編譯器的插件,當(dāng)這些插件工作時(shí),允許讀取、修改、添加抽象語(yǔ)法樹(shù)中的任意元素。
譬如Java著名的編碼效率工具Lombok,它可以通過(guò)注解來(lái)實(shí)現(xiàn)自動(dòng)產(chǎn)生 getter/setter方法、進(jìn)行空置檢查、生成受查異常表、產(chǎn)生equals()和hashCode()方法,等等.
語(yǔ)義分析與字節(jié)碼生成
語(yǔ)義分析的主要任務(wù)則是對(duì)結(jié)構(gòu)上正確的源 程序進(jìn)行上下文相關(guān)性質(zhì)的檢查,譬如進(jìn)行類型檢查、控制流檢查、數(shù)據(jù)流檢查,等等
int a = 1;
boolean b = false;
char c = 2;
//后續(xù)可能出現(xiàn)的賦值運(yùn)算:
int d = a + c;
int d = b + c; //錯(cuò)誤,
char d = a + c; //錯(cuò)誤
//C語(yǔ)言中,a、b、c的上下文定義不變,第二、三種寫(xiě)法都是可以被正確編譯的
我們編碼時(shí)經(jīng)常能在IDE 中看到由紅線標(biāo)注的錯(cuò)誤提示,其中絕大部分都是來(lái)源于語(yǔ)義分析階段的檢查結(jié)果。
1.標(biāo)注檢查
標(biāo)注檢查步驟要檢查的內(nèi)容包括諸如變量使用前是否已被聲明、變量與賦值之間的數(shù)據(jù)類型是否能夠匹配,等等,剛才3個(gè)變量定義的例子就屬于標(biāo)注檢查的處理范疇
在標(biāo)注檢查中,還會(huì)順便進(jìn)行 一個(gè)稱為常量折疊(Constant Folding)的代碼優(yōu)化,這是Javac編譯器會(huì)對(duì)源代碼做的極少量?jī)?yōu)化措施 之一(代碼優(yōu)化幾乎都在即時(shí)編譯器中進(jìn)行)。
int a = 2 + 1;
由于編譯期間進(jìn)行了常量折疊,所以在代碼里面定 義“a=1+2”比起直接定義“a=3”來(lái),并不會(huì)增加程序運(yùn)行期哪怕僅僅一個(gè)處理器時(shí)鐘周期的處理工作量。
2.數(shù)據(jù)及控制流分析
可以檢查出諸如程序局部變量 在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問(wèn)題。
3.解語(yǔ)法糖
在Javac的源碼中,解語(yǔ)法糖的過(guò)程由desugar()方法觸發(fā)。
Java中最常見(jiàn)的語(yǔ)法糖包括了前面提到過(guò)的泛型、變長(zhǎng)參數(shù)、自動(dòng)裝箱拆箱,等等。
4.字節(jié)碼生成
字節(jié)碼生成是Javac編譯過(guò)程的最后一個(gè)階段,在Javac源碼里面由com.sun.tools.javac.jvm.Gen類來(lái)完成。
字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟所生成的信息(語(yǔ)法樹(shù)、符號(hào)表)轉(zhuǎn)化成字節(jié)碼指令寫(xiě)到磁盤中,編譯器還進(jìn)行了少量的代碼添加和轉(zhuǎn)換工作。
實(shí)例構(gòu)造器()方法和類構(gòu)造器()方法就是在這個(gè)階段被添加到語(yǔ) 法樹(shù)之中的
字符串的加操作替換為StringBuffer或StringBuilder(取決于目標(biāo)代碼的版本是否大于或等于JDK 5)的append()操 作,等等。
1.2語(yǔ)法糖的本質(zhì)
泛型
泛型的本質(zhì)是參數(shù)化類型或者參數(shù)化多態(tài)的應(yīng)用,即可以將操作的數(shù)據(jù)類型指定為方法簽名中的一種特殊參數(shù)。
Java選擇的泛型實(shí)現(xiàn)方式叫作類型擦除式泛型:Java語(yǔ)言中的泛型只在程序源碼中存在,在編譯后的字節(jié)碼文件中,全部泛型都被替換為原來(lái)的裸類型了,并且在相應(yīng)的地方插入了強(qiáng)制轉(zhuǎn)型代碼。
類型擦除
裸類型”(Raw Type)的概念:裸類型應(yīng)被視為所有該類型泛型化實(shí)例的共同父類型(Super Type)
ArrayList<Integer> ilist = new ArrayList<Integer>();
ArrayList<String> slist = new ArrayList<String>();
ArrayList list; // 裸類型
list = ilist;
list = slist;
如何實(shí)現(xiàn)裸類型?
直接在編譯時(shí)把ArrayList
泛型擦除前的例子
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("hello", "你好");
map.put("how are you?", "吃了沒(méi)?");
System.out.println(map.get("hello"));
System.out.println(map.get("how are you?"));
}
把這段Java代碼編譯成Class文件,然后再用字節(jié)碼反編譯工具進(jìn)行反編譯后,將會(huì)發(fā)現(xiàn)泛型都不見(jiàn)了
public static void main(String[] args) {
Map map = new HashMap();//裸類型
map.put("hello", "你好");
map.put("how are you?", "吃了沒(méi)?");
System.out.println((String) map.get("hello"));//強(qiáng)制類型轉(zhuǎn)換
System.out.println((String) map.get("how are you?"));
}
當(dāng)泛型遇到重載
public class GenericTypes {
public static void method(List<String> list) {
System.out.println("invoke method(List<String> list)");
}
public static void method(List<Integer> list) {
System.out.println("invoke method(List<Integer> list)");
}
}
參數(shù)列表在特征簽名中,因此參數(shù)列表不同時(shí),可以進(jìn)行重載,但是由于所有泛型都需要通過(guò)類型擦出轉(zhuǎn)化為裸類型,導(dǎo)致參數(shù)都是List list,所以不能重載。會(huì)報(bào)錯(cuò)。
自動(dòng)裝箱、拆箱與遍歷循環(huán)
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
int sum = 0;
for (int i : list) {
sum += i;
}
System.out.println(sum);
}
編譯后:
public static void main(String[] args) {
List list = Arrays.asList( new Integer[] {
Integer.valueOf(1),
Integer.valueOf(2),
Integer.valueOf(3),
Integer.valueOf(4) });
int sum = 0;
for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
int i = ((Integer)localIterator.next()).intValue();
sum += i;
}
System.out.println(sum);
}
二、后端編譯與優(yōu)化
如果把字節(jié)碼看作是程序語(yǔ)言的一種中間表示形式(Intermediate Representation,IR)的話, 那編譯器無(wú)論在何時(shí)、在何種狀態(tài)下把Class文件轉(zhuǎn)換成與本地基礎(chǔ)設(shè)施(硬件指令集、操作系統(tǒng))相關(guān)的二進(jìn)制機(jī)器碼,都可以視為整個(gè)編譯過(guò)程的后端
2.1即時(shí)編譯器
由于即時(shí)編譯器編譯本地代碼需要占用程序運(yùn)行時(shí)間,通常要編譯出優(yōu)化程度越高的代碼,所花 費(fèi)的時(shí)間便會(huì)越長(zhǎng);
而且想要編譯出優(yōu)化程度更高的代碼,解釋器可能還要替編譯器收集性能監(jiān)控信息,這對(duì)解釋執(zhí)行階段的速度也有所影響。
為了在程序啟動(dòng)響應(yīng)速度與運(yùn)行效率之間達(dá)到最佳平衡:
HotSpot虛擬機(jī)在編譯子系統(tǒng)中加入了分層編譯的功能,分層編譯根據(jù)編譯器編譯、優(yōu)化的規(guī)模與耗時(shí),劃分出不同的編譯層次,其中包 括:
- 第0層。程序純解釋執(zhí)行,并且解釋器不開(kāi)啟性能監(jiān)控功能(Profiling)。
- 第1層。使用客戶端編譯器將字節(jié)碼編譯為本地代碼來(lái)運(yùn)行,進(jìn)行簡(jiǎn)單可靠的穩(wěn)定優(yōu)化,不開(kāi)啟 性能監(jiān)控功能。
- 第2層。仍然使用客戶端編譯器執(zhí)行,僅開(kāi)啟方法及回邊次數(shù)統(tǒng)計(jì)等有限的性能監(jiān)控功能。
- 第3層。仍然使用客戶端編譯器執(zhí)行,開(kāi)啟全部性能監(jiān)控,除了第2層的統(tǒng)計(jì)信息外,還會(huì)收集如分支跳轉(zhuǎn)、虛方法調(diào)用版本等全部的統(tǒng)計(jì)信息。
- 第4層。使用服務(wù)端編譯器將字節(jié)碼編譯為本地代碼,相比起客戶端編譯器,服務(wù)端編譯器會(huì)啟 用更多編譯耗時(shí)更長(zhǎng)的優(yōu)化,還會(huì)根據(jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。
編譯對(duì)象與觸發(fā)條件
會(huì)被即時(shí)編譯器編譯的目標(biāo)是熱點(diǎn)代碼,這里所指的熱點(diǎn)代碼主要有兩類:
- 被多次調(diào)用的方法。
- 被多次執(zhí)行的循環(huán)體。
對(duì)于這兩種情況,編譯的目標(biāo)對(duì)象都是整個(gè)方法體,而不會(huì)是單獨(dú)的循環(huán)體。
這種編譯方式因?yàn)?編譯發(fā)生在方法執(zhí)行的過(guò)程中,因此被很形象地稱為棧上替換(On Stack Replacement,OSR),即方法的棧幀還在棧上,方法就被替換了。
多少次才算“多次”呢?
要知道某段代碼是不是熱點(diǎn)代碼,是不是需要觸發(fā)即時(shí)編譯,這個(gè)行為稱為“熱點(diǎn)探測(cè)”(Hot Spot Code Detection),判定方式:
-
基于采樣的熱點(diǎn)探測(cè)(Sample Based Hot Spot Code Detection)
會(huì)周期性地檢查各個(gè)線程的調(diào)用棧頂,如果發(fā)現(xiàn)某個(gè)方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就是
熱點(diǎn)方法。缺點(diǎn):很難精確地確認(rèn)一個(gè)方法的熱度,容易因?yàn)槭艿骄€程阻塞或別的外界因素的影響而 擾亂熱點(diǎn)探測(cè)
-
基于計(jì)數(shù)器的熱點(diǎn)探測(cè)(Counter Based Hot Spot Code Detection)
為每個(gè)方法(甚至是代碼塊)建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過(guò)一定的閾值就認(rèn)為它是
熱點(diǎn)方法。缺點(diǎn):實(shí)現(xiàn)起來(lái)要麻煩一些,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器,而且不能 直接獲取到方法的調(diào)用關(guān)系
J9用過(guò)第一種采樣熱點(diǎn)探測(cè),而在HotSpot 虛擬機(jī)中使用的是第二種基于計(jì)數(shù)器的熱點(diǎn)探測(cè)方法,
HotSpot 中每個(gè)方法的 2 個(gè)計(jì)數(shù)器
-
方法調(diào)用計(jì)數(shù)器
- 統(tǒng)計(jì)方法被調(diào)用的次數(shù),處理多次調(diào)用的方法的。
- 默認(rèn)統(tǒng)計(jì)的不是方法調(diào)用的絕對(duì)次數(shù),而是方法在一段時(shí)間內(nèi)被調(diào)用的次數(shù),如果超過(guò)這個(gè)時(shí)間限制還沒(méi)有達(dá)到判為熱點(diǎn)代碼的閾值,則該方法的調(diào)用計(jì)數(shù)器值減半。
- 關(guān)閉熱度衰減:
-XX: -UseCounterDecay(此時(shí)方法計(jì)數(shù)器統(tǒng)計(jì)的是方法被調(diào)用的絕對(duì)次數(shù)); - 設(shè)置半衰期時(shí)間:
-XX: CounterHalfLifeTime(單位是秒); - 熱度衰減過(guò)程是在 GC 時(shí)順便進(jìn)行。
- 默認(rèn)閾值在客戶端模式下是1500次,在服務(wù)端模式下是10000次,
- 關(guān)閉熱度衰減:
-
回邊計(jì)數(shù)器
- 統(tǒng)計(jì)一個(gè)方法中 “回邊” 的次數(shù),處理多次執(zhí)行的循環(huán)體的。
- 回邊:在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令(不是所有循環(huán)體都是回邊,空循環(huán)體是自己跳向自己,沒(méi)有向后跳,不算回邊)。
- 調(diào)整回邊計(jì)數(shù)器閾值:
-XX: OnStackReplacePercentage(OSR比率)- Client 模式:
回邊計(jì)數(shù)器的閾值 = 方法調(diào)用計(jì)數(shù)器閾值 * OSR比率 / 100; - Server 模式:
回邊計(jì)數(shù)器的閾值 = 方法調(diào)用計(jì)數(shù)器閾值 * ( OSR比率 - 解釋器監(jiān)控比率 ) / 100;
- Client 模式:
- 統(tǒng)計(jì)一個(gè)方法中 “回邊” 的次數(shù),處理多次執(zhí)行的循環(huán)體的。
編譯過(guò)程
虛擬機(jī)在代碼編譯未完成時(shí)會(huì)按照解釋方式繼續(xù)執(zhí)行,編譯動(dòng)作在后臺(tái)的編譯線程執(zhí)行。
禁止后臺(tái)編譯:-XX: -BackgroundCompilation,打開(kāi)后這個(gè)開(kāi)關(guān)參數(shù)后,交編譯請(qǐng)求的線程會(huì)等待編譯完成,然后執(zhí)行編譯器輸出的本地代碼。
在后臺(tái)編譯過(guò)程中,客戶端編譯器與服務(wù)端編譯器是有區(qū)別的。
客戶端編譯器
是一個(gè)相對(duì)簡(jiǎn)單快速的三段式編譯器,主要的關(guān)注點(diǎn)在于局部性的優(yōu)化,而放棄了許多耗時(shí)較長(zhǎng)的全局優(yōu)化手段。
-
第一個(gè)階段,一個(gè)平*立的前端將字節(jié)碼構(gòu)造成一種高級(jí)中間代碼表示(High-Level Intermediate Representation,HIR,即與目標(biāo)機(jī)器指令集無(wú)關(guān)的中間表示)。HIR使用靜態(tài)單分配 (Static Single Assignment,SSA)的形式來(lái)代表代碼值,這可以使得一些在HIR的構(gòu)造過(guò)程之中和之后進(jìn)行的優(yōu)化動(dòng)作更容易實(shí)現(xiàn)。在此之前編譯器已經(jīng)會(huì)在字節(jié)碼上完成一部分基礎(chǔ)優(yōu)化,如方法內(nèi)聯(lián)、 常量傳播等優(yōu)化將會(huì)在字節(jié)碼被構(gòu)造成HIR之前完成。
-
第二個(gè)階段,一個(gè)平臺(tái)相關(guān)的后端從HIR中產(chǎn)生低級(jí)中間代碼表示(Low-Level Intermediate Representation,LIR,即與目標(biāo)機(jī)器指令集相關(guān)的中間表示),而在此之前會(huì)在HIR上完成另外一些優(yōu)化,如空值檢查消除、范圍檢查消除等,以便讓HIR達(dá)到更高效的代碼表示形式。
-
最后的階段是在平臺(tái)相關(guān)的后端使用線性掃描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窺孔(Peephole)優(yōu)化,然后產(chǎn)生機(jī)器代碼??蛻舳司幾g器大致的執(zhí)行過(guò)程如圖
服務(wù)端編譯器
是專門面向服務(wù)端的典型應(yīng)用場(chǎng)景,執(zhí)行大部分經(jīng)典的優(yōu)化動(dòng)作,如:無(wú)用代碼消除(Dead Code Elimination)、循環(huán)展開(kāi) (Loop Unrolling)、循環(huán)表達(dá)式外提(Loop Expression Hoisting)、消除公共子表達(dá)式(Common Subexpression Elimination)、常量傳播(Constant Propagation)、基本塊重排序(Basic Block Reordering)等,還會(huì)實(shí)施一些與Java語(yǔ)言特性密切相關(guān)的優(yōu)化技術(shù),如范圍檢查消除(Range Check Elimination)、空值檢查消除(Null Check Elimination,不過(guò)并非所有的空值檢查消除都是依賴編譯器優(yōu)化的,有一些是代碼運(yùn)行過(guò)程中自動(dòng)優(yōu)化了)等。
另外,還可能根據(jù)解釋器或客戶端編譯器提供的 性能監(jiān)控信息,進(jìn)行一些不穩(wěn)定的預(yù)測(cè)性激進(jìn)優(yōu)化,如守護(hù)內(nèi)聯(lián)(Guarded Inlining)、分支頻率預(yù)測(cè) (Branch Frequency Prediction)等
服務(wù)端編譯采用的寄存器分配器是一個(gè)全局圖著色分配器,它可以充分利用某些處理器架構(gòu)(如 RISC)上的大寄存器集合。
2.2提前編譯器
現(xiàn)在提前編譯產(chǎn)品和對(duì)其的研究有著兩條明顯的分支:
-
與傳統(tǒng)C、C++編譯器類似的,在程序運(yùn)行之前把程序代碼編譯成機(jī)器碼的靜態(tài)翻譯工作
-
把原本即時(shí)編譯器在運(yùn)行時(shí)要做的編譯工作提前做好并保存下來(lái),下次運(yùn)行到這些代碼(譬如公共庫(kù)代碼在被同一臺(tái)機(jī)器其他Java進(jìn)程使用)時(shí)直接把它加載進(jìn)來(lái)使用。(本質(zhì)是給即時(shí)編譯器做緩存加速,去改善Java程序的啟動(dòng)時(shí)間)
在目前的Java技術(shù)體系里,這種提前編譯已經(jīng)完全被主流的商用JDK支持
困難:這種提前編譯方式不僅要和目標(biāo)機(jī)器相關(guān),甚至還必須與HotSpot虛擬機(jī)的運(yùn)行時(shí)參數(shù)綁定(如生成內(nèi)存屏障代碼)才能正確工作,要做提前編譯的話,自然也要把這些配合的工作平移過(guò)去。
2.3即時(shí)編譯器的優(yōu)勢(shì)
提前編譯的代碼輸出質(zhì)量,一定會(huì)比即時(shí)編譯更高嗎?
以下為即時(shí)編譯器相較于提前編譯器的優(yōu)勢(shì):
-
性能分析制導(dǎo)優(yōu)化(Profile-Guided Optimization,PGO)
抽象類通常會(huì)是什么實(shí)際類型、條件判斷通常會(huì)走哪條分支、方法調(diào)用通常會(huì)選擇哪個(gè)版本、循環(huán)通常會(huì)進(jìn)行多少次等,這些數(shù)據(jù)一般在靜態(tài)分析時(shí)是無(wú)法得到的,或者不可能存在確定且唯一的解, 最多只能依照一些啟發(fā)性的條件去進(jìn)行猜測(cè)。但在動(dòng)態(tài)運(yùn)行時(shí)卻能看出它們具有非常明顯的偏好性。就可以把熱的代碼集中放到 一起,集中優(yōu)化和分配更好的資源(分支預(yù)測(cè)、寄存器、緩存等)給它。
-
激進(jìn)預(yù)測(cè)性優(yōu)化(Aggressive Speculative Optimization)
靜態(tài)優(yōu)化無(wú)論如何都必須保證優(yōu)化后所有的程序外部可見(jiàn)影響(不僅僅是執(zhí)行結(jié)果) 與優(yōu)化前是等效的
然而,即時(shí)編譯的策略就可以不必這樣保守,可以大膽地按照高概率的假設(shè)進(jìn)行優(yōu)化,萬(wàn)一真的走到罕見(jiàn)分支上,大不了退回到低級(jí)編譯器甚至解釋器上去執(zhí)行,并不會(huì)出現(xiàn)無(wú)法挽救的后果。
如果Java虛擬機(jī)真的遇到虛方法就去查虛表而不做內(nèi) 聯(lián)的話,Java技術(shù)可能就已經(jīng)因性能問(wèn)題而被淘汰很多年了。
實(shí)際上虛擬機(jī)會(huì)通過(guò)類繼承關(guān)系分析等 一系列激進(jìn)的猜測(cè)去做去虛擬化(Devitalization),以保證絕大部分有內(nèi)聯(lián)價(jià)值的虛方法都可以順利內(nèi)聯(lián)。
-
鏈接時(shí)優(yōu)化(Link-Time Optimization,LTO)
如C、C++的程序要調(diào)用某個(gè)動(dòng)態(tài)鏈接庫(kù)的某個(gè)方法,就會(huì)出現(xiàn)很明顯的邊界隔閡,還難以優(yōu)化。
這是因?yàn)橹鞒绦蚺c動(dòng)態(tài)鏈接庫(kù)的代碼在它們編譯時(shí)是完全獨(dú)立的,兩者各自編譯、優(yōu)化自己的代碼。然而,Java語(yǔ)言天生就是動(dòng)態(tài)鏈接的,一個(gè)個(gè) Class文件在運(yùn)行期被加載到虛擬機(jī)內(nèi)存當(dāng)中。
三、編譯器優(yōu)化技術(shù)
| 類型 | 優(yōu)化技術(shù) |
|---|---|
| 編譯器策略 (Compiler Tactics) | |
| 延遲編譯 (Delayed Compilation) | |
| 分層編譯 (Tiered Compilation) | |
| 棧上替換 (On-Stack Replacement) | |
| 延遲優(yōu)化 (Delayed Reoptimization) | |
| 靜態(tài)單賦值表示 (Static Single Assignment Representation) | |
| 基于性能監(jiān)控的優(yōu)化技術(shù) (Profile-Based Techniques) | |
| 樂(lè)觀空值斷言 (Optimistic Nullness Assertions) | |
| 樂(lè)觀類型斷言 (Optimistic Type Assertions) | |
| 樂(lè)觀類型增強(qiáng) (Optimistic Type Strengthening) | |
| 樂(lè)觀數(shù)組長(zhǎng)度增強(qiáng) (Optimistic Array Length Strengthening) | |
| 裁剪未被選擇的分支 (Untaken Branch Pruning) | |
| 樂(lè)觀的多態(tài)內(nèi)聯(lián) (Optimistic N-morphic Inlining) | |
| 分支頻率預(yù)測(cè) (Branch Frequency Prediction) | |
| 調(diào)用頻率預(yù)測(cè) (Call Frequency Prediction) | |
| 基于證據(jù)的優(yōu)化技術(shù) (Proof-Based Techniques) | |
| 精確類型推斷 (Exact Type Inference) | |
| 內(nèi)存值推斷 (Memory Value Inference) | |
| 內(nèi)存值跟蹤 (Memory Value Tracking) | |
| 常量折疊 (Constant Folding) | |
| 重組 (Reassociation) | |
| 操作符退化 (Operator Strength Reduction) | |
| 空值檢查消除 (Null Check Elimination) | |
| 類型檢測(cè)退化 (Type Test Strength Reduction) | |
| 類型檢測(cè)消除 (Type Test Elimination) | |
| 代數(shù)簡(jiǎn)化 (Algebraic Simplification) | |
| 公共子表達(dá)式消除 (Common Subexpression Elimination) | |
| 數(shù)據(jù)流敏感重寫(xiě) (Flow-Sensitive Rewrites) | |
| 條件常量傳播 (Conditional Constant Propagation) | |
| 基于流承載的類型縮減轉(zhuǎn)換 (Flow-Carried Type Narrowing) | |
| 無(wú)用代碼消除 (Dead Code Elimination) | |
| 語(yǔ)言相關(guān)的優(yōu)化技術(shù) (Language-Specific Techniques) | |
| 類型繼承關(guān)系分析 (Class Hierarchy Analysis) | |
| 去虛擬化 (Devirtualization) | |
| 符號(hào)常量傳播 (Symbolic Constant Propagation) | |
| 自動(dòng)裝箱消除 (Autobox Elimination) | |
| 逃逸分析 (Escape Analysis) | |
| 鎖消除 (Lock Elision) | |
| 鎖膨脹 (Lock Coarsening) | |
| 消除反射 (De-reflection) | |
| 內(nèi)存及代碼位置變換 (Memory and Placement Transformation) | |
| 表達(dá)式提升 (Expression Hoisting) | |
| 表達(dá)式下沉 (Expression Sinking) | |
| 冗余存儲(chǔ)消除 (Redundant Store Elimination) | |
| 相鄰存儲(chǔ)合并 (Adjacent Store Fusion) | |
| 交匯點(diǎn)分離 (Merge-Point Splitting) | |
| 循環(huán)變換 (Loop Transformations) | |
| 循環(huán)展開(kāi) (Loop Unrolling) | |
| 循環(huán)剝離 (Loop Peeling) | |
| 安全點(diǎn)消除 (Safepoint Elimination) | |
| 迭代范圍分離 (Iteration Range Splitting) | |
| 范圍檢查消除 (Range Check Elimination) | |
| 循環(huán)向量化 (Loop Vectorization) | |
| 全局代碼調(diào)整 (Global Code Shaping) | |
| 內(nèi)聯(lián) (Inlining) | |
| 全局代碼外提 (Global Code Motion) | |
| 基于熱度的代碼布局 (Heat-Based Code Layout) | |
| Switch調(diào)整 (Switch Balancing) | |
| 控制流圖變換 (Control Flow Graph Transformation) | |
| 本地代碼編排 (Local Code Scheduling) | |
| 本地代碼封包 (Local Code Bundling) | |
| 延遲槽填充 (Delay Slot Filling) | |
| 著色圖寄存器分配 (Graph-Coloring Register Allocation) | |
| 線性掃描寄存器分配 (Linear Scan Register Allocation) | |
| 復(fù)寫(xiě)聚合 (Copy Coalescing) | |
| 常量分裂 (Constant Splitting) | |
| 復(fù)寫(xiě)移除 (Copy Removal) | |
| 地址模式匹配 (Address Mode Matching) | |
| 指令窺空優(yōu)化 (Instruction Peepholing) | |
| 基于確定有限狀態(tài)機(jī)的代碼生成 (DFA-Based Code Generator) |
3.1一個(gè)優(yōu)化的例子
原始代碼:
static class B {
int value;
final int get() {
return value;
}
}
public void foo() {
y = b.get();
// ...do stuff...
z = b.get();
sum = y + z;
}
第一步優(yōu)化: 方法內(nèi)聯(lián)(一般放在優(yōu)化序列最前端,因?yàn)閷?duì)其他優(yōu)化有幫助)
目的:
- 去除方法調(diào)用的成本(如建立棧幀等)
- 為其他優(yōu)化建立良好的基礎(chǔ)
public void foo() {
y = b.value;
// ...do stuff...
z = b.value;
sum = y + z;
}
第二步優(yōu)化: 公共子表達(dá)式消除
public void foo() {
y = b.value;
// ...do stuff... // 因?yàn)檫@部分并沒(méi)有改變 b.value 的值
// 如果把 b.value 看成一個(gè)表達(dá)式,就是公共表達(dá)式消除
z = y; // 把這一步的 b.value 替換成 y
sum = y + z;
}
第三步優(yōu)化: 復(fù)寫(xiě)傳播
public void foo() {
y = b.value;
// ...do stuff...
y = y; // z 變量與以相同,完全沒(méi)有必要使用一個(gè)新的額外變量
// 所以將 z 替換為 y
sum = y + z;
}
第四步優(yōu)化: 無(wú)用代碼消除
無(wú)用代碼:
- 永遠(yuǎn)不會(huì)執(zhí)行的代碼
- 完全沒(méi)有意義的代碼
public void foo() {
y = b.value;
// ...do stuff...
// y = y; 這句沒(méi)有意義的,去除
sum = y + y;
}
3.2方法內(nèi)聯(lián)
它是編譯器最重要的優(yōu)化手段,甚至都可以不加 上“之一”。
除了消除方法調(diào)用的成本之外,它更重要的意義是為其他優(yōu)化手段建立良好的基礎(chǔ)
目的是:去除方法調(diào)用的成本(如建立棧幀等),并為其他優(yōu)化建立良好的基礎(chǔ),所以一般將方法內(nèi)聯(lián)放在優(yōu)化序列最前端,因?yàn)樗鼘?duì)其他優(yōu)化有幫助。
為了解決虛方法的內(nèi)聯(lián)問(wèn)題:引入類型繼承關(guān)系分析(Class Hierarchy Analysis,CHA)
用于確定在目前已加載的類中,某個(gè)接口是否有多于一種的實(shí)現(xiàn),某個(gè)類是否存在子類、子類是否為抽象類等。
-
對(duì)于非虛方法:
- 直接進(jìn)行內(nèi)聯(lián),其調(diào)用方法的版本在編譯時(shí)已經(jīng)確定,是根據(jù)變量的靜態(tài)類型決定的。
-
對(duì)于虛方法: (激進(jìn)優(yōu)化,要預(yù)留“逃生門”)
- 向 CHA 查詢此方法在當(dāng)前程序下是否有多個(gè)目標(biāo)可選擇;
- 只有一個(gè)目標(biāo)版本:
- 先對(duì)這唯一的目標(biāo)進(jìn)行內(nèi)聯(lián);
- 如果之后的執(zhí)行中,虛擬機(jī)沒(méi)有加載到會(huì)令這個(gè)方法接收者的繼承關(guān)系發(fā)生改變的新類,則該內(nèi)聯(lián)代碼可以一直使用;
- 如果加載到導(dǎo)致繼承關(guān)系發(fā)生變化的新類,就拋棄已編譯的代碼,退回到解釋狀態(tài)進(jìn)行執(zhí)行,或者重新進(jìn)行編譯。
- 有多個(gè)目標(biāo)版本:
- 使用內(nèi)聯(lián)緩存,未發(fā)生方法調(diào)用前,內(nèi)聯(lián)緩存為空;
- 第一次調(diào)用發(fā)生后,記錄調(diào)用方法的對(duì)象的版本信息;
- 之后的每次調(diào)用都要先與內(nèi)聯(lián)緩存中的對(duì)象版本信息進(jìn)行比較;
- 版本信息一樣,繼續(xù)使用內(nèi)聯(lián)代碼,是一種
單態(tài)內(nèi)聯(lián)緩存(Monomorphic Inline Cache) - 版本信息不一樣,說(shuō)明程序使用了虛方法的多態(tài)特性,退化成
超多態(tài)內(nèi)聯(lián)緩存(Megamorphic Inline Cache),查找虛方法進(jìn)行方法分派。
- 版本信息一樣,繼續(xù)使用內(nèi)聯(lián)代碼,是一種
- 只有一個(gè)目標(biāo)版本:
- 向 CHA 查詢此方法在當(dāng)前程序下是否有多個(gè)目標(biāo)可選擇;
3.3逃逸分析【最前沿】
基本行為
分析對(duì)象的作用域,看它有沒(méi)有能在當(dāng)前作用域之外使用:
- 方法逃逸:對(duì)象在方法中定義之后,能被外部方法引用,如作為參數(shù)傳遞到了其他方法中。
- 線程逃逸:賦值給 static 變量,或可以在其他線程中訪問(wèn)的實(shí)例變量。
對(duì)于不會(huì)逃逸到方法或線程外的對(duì)象能進(jìn)行優(yōu)化
- 棧上分配: 對(duì)于不會(huì)逃逸到方法外的對(duì)象,可以在棧上分配內(nèi)存,這樣這個(gè)對(duì)象所占用的空間可以隨棧幀出棧而銷毀,減小 GC 的壓力。
-
標(biāo)量替換(重要):
- 標(biāo)量:基本數(shù)據(jù)類型和 reference。
- 不創(chuàng)建對(duì)象,而是將對(duì)象拆分成一個(gè)一個(gè)標(biāo)量,然后直接在棧上分配,是棧上分配的一種實(shí)現(xiàn)方式。
- HotSpot 使用的是標(biāo)量替換而不是棧上分配,因?yàn)閷?shí)現(xiàn)棧上分配需要更改大量假設(shè)了 “對(duì)象只能在堆中分配” 的代碼。
-
同步消除
- 如果逃逸分析能夠確定一個(gè)變量不會(huì)逃逸出線程,無(wú)法被其他線程訪問(wèn),對(duì)這個(gè)變量實(shí)施的同步措施也就可以安全地消除掉。
虛擬機(jī)參數(shù)
- 開(kāi)啟逃逸分析:
-XX: +DoEscapeAnalysis - 開(kāi)啟標(biāo)量替換:
-XX: +EliminateAnalysis - 開(kāi)啟鎖消除:
-XX: +EliminateLocks - 查看分析結(jié)果:
-XX: PrintEscapeAnalysis - 查看標(biāo)量替換情況:
-XX: PrintEliminateAllocations
例子
Point類的代碼,這就是一個(gè)包含x和y坐標(biāo)的POJO類型
// 完全未優(yōu)化的代碼
public int test(int x) {
int xx = x + 2;
Point p = new Point(xx, 42);
return p.getX();
}
步驟1:構(gòu)造函數(shù)內(nèi)聯(lián)
public int test(int x) {
int xx = x + 2;
Point p = point_memory_alloc(); // 在堆中分配P對(duì)象的示意方法
p.x = xx; // Point構(gòu)造函數(shù)被內(nèi)聯(lián)后的樣子
p.y = 42;
return p.x; // Point::getX()被內(nèi)聯(lián)后的樣子
}
步驟2:標(biāo)量替換
經(jīng)過(guò)逃逸分析,發(fā)現(xiàn)在整個(gè)test()方法的范圍內(nèi)Point對(duì)象實(shí)例不會(huì)發(fā)生任何程度的逃逸, 這樣可以對(duì)它進(jìn)行標(biāo)量替換優(yōu)化,把其內(nèi)部的x和y直接置換出來(lái),分解為test()方法內(nèi)的局部變量,從 而避免Point對(duì)象實(shí)例被實(shí)際創(chuàng)建
public int test(int x) {
int xx = x + 2;
int px = xx;
int py = 42;
return px;
}
步驟3:無(wú)效代碼消除
通過(guò)數(shù)據(jù)流分析,發(fā)現(xiàn)py的值其實(shí)對(duì)方法不會(huì)造成任何影響,那就可以放心地去做無(wú)效代碼消除得到最終優(yōu)化結(jié)果,
public int test(int x) {
return x + 2;
}
總結(jié)
以上是生活随笔為你收集整理的JVM学习-程序编译与优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 一套模板搞定二叉树算法题--二叉树算法讲
- 下一篇: 不止八股:阿里内部语雀一些有趣的并发编程