概述
編譯器是一段“不確定”的操作過程
編譯器類型
- 前端編譯器:將Java代碼編譯為class字節(jié)碼
- 代表:
sun公司的javac(Java語言編寫)枫绅、Eclipse JDT中的增量編譯器ECJ
- 后端編譯器(JIT編譯器):將字節(jié)碼轉(zhuǎn)變?yōu)闄C(jī)器碼
- 代表:
HotSpot VM的C1志珍、C2編譯器
- 靜態(tài)提前編譯器(AOT編譯器 Ahead of Time Compiler):將Java代碼編譯為機(jī)器碼
-代表:GNU Compiler for the java、Excelsior JET
1. 早期(編譯期)優(yōu)化
主要說明Sun Javac的大概編譯過程
- 解析與填充符號表過程
1.1 解析:
1.1.1 詞法分析:將源代碼的字符流轉(zhuǎn)為標(biāo)記(Token)集合(例如:關(guān)鍵字塞弊,變量名宪躯,運算符笼痛,字面量)
1.1.2 語法分析:根據(jù)Token序列構(gòu)造抽象語法樹捞奕,語法樹的每一個節(jié)點都是一個語法結(jié)構(gòu)(例如:包,類型桥狡,修飾符搅裙,運算符,接口裹芝,返回值呈宇,代碼注釋)
1.2 符號表填充:符號表由符號地址和符號信息組成的表格(可以看成K-V鍵值對) - 注解處理
插入式注解處理器的標(biāo)準(zhǔn)API在編譯期間對注解進(jìn)行處理 - 分析與生成字節(jié)碼
因為由于由語法分析所生成的抽象語法樹不能保證邏輯性,故而語義分析是對正確性的審查
int a =1;
boolean b = false;
int c = a+b;//編譯不能通過
- 3.1 標(biāo)注檢查
主要檢查:變量使用前是否已經(jīng)被聲明局雄、變量與賦值之間的數(shù)據(jù)類型是否能匹配 - 3.2 數(shù)據(jù)及控制流分析
主要檢查:對程序的上下文更進(jìn)一步的驗證,如:局部變量在使用前是否已經(jīng)賦值存炮、方法的每條路徑是否都有返回值炬搭、是否所有的受檢異常都被正確的處理等問題
//這里的兩個方法蜈漓,編譯出的字節(jié)碼沒有一點區(qū)別,只是有final修飾的不能被改變
public void test(final int a) {}
public void test(int a) {}
- 3.3 解語法糖
泛型擦除(實際上就是將結(jié)果強(qiáng)轉(zhuǎn))宫盔、變長參數(shù)融虽、自動裝箱/拆箱、內(nèi)部類灼芭、枚舉類有额、斷言語句、對枚舉和字符串的支持彼绷、try語句定義和關(guān)閉資源等
條件編譯:使用條件為常量的if語句
//1.
public static void main (String[] args) {
if (true) {
System.out.println("1");
} else {
System.out.println("2");
}
}
//在編譯之后的結(jié)果
public static void main (String[] args) {
System.out.println("1");
}
//2. 下面語句將會拒絕編譯
public static void main(String[] args) {
while (false) {
System.out.println("錯誤");
}
}
- 字節(jié)碼生成
字節(jié)碼生成不僅僅將前面步驟生成的信息轉(zhuǎn)換為字節(jié)碼寫到磁盤中巍佑,還要進(jìn)行少量的代碼添加和轉(zhuǎn)換工作
2. 晚期(運行期)優(yōu)化
部分商用虛擬機(jī)(Sun HotSpot、IBM J9)中寄悯,Java程序最初通過解釋器(interpreter)解釋執(zhí)行萤衰,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個方法或者代碼運行特別頻繁時,就會把這些代碼定義為“熱點代碼”猜旬。為了提高熱點代碼的執(zhí)行效率脆栋,在運行時,虛擬機(jī)會把這些代碼編譯成與本地平臺相關(guān)的機(jī)器碼洒擦,并進(jìn)行各種層次的優(yōu)化椿争。完成這個任務(wù)的編譯器就是即時編譯器(JIT just int time complier)
JRockit沒有解釋器,因此啟動時間較長
- 為何HotSpot 虛擬機(jī)要使用解釋器與編譯器并存的架構(gòu)熟嫩?
兩種編譯器各有優(yōu)勢: 解釋器:啟動快秦踪、執(zhí)行快; 編譯器:執(zhí)行效率高
Client模式啟動速度較快邦危,Server模式啟動較慢洋侨,但是啟動進(jìn)入穩(wěn)定期長期運行之后Server模式的程序運行速度比Client要快很多 - 為何HotSpot 虛擬機(jī)要實現(xiàn)兩個不同的即使編譯器?
client compiler:獲取更快的編譯速度倦蚪;server compiler:獲取更好的編譯質(zhì)量希坚。具體使用哪種,虛擬機(jī)會根據(jù)自身版本和宿主機(jī)的硬件性能自由選擇陵且,當(dāng)然也可以強(qiáng)制運行某種模式裁僧,無論編譯器采用client compiler 還是server compiler,解釋器與編譯器搭配使用都稱為“混合模式(mixed mode)”慕购,也可以強(qiáng)制虛擬機(jī)運行編譯模式(-Xcomp)或者解釋模式(-Xint)
MacBook-Pro:~ shizhenshuang$ java -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, mixed mode)
MacBook-Pro:~ shizhenshuang$ java -Xint -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, interpreted mode)
MacBook-Pro:~ shizhenshuang$ java -Xcomp -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, compiled mode)
- 程序什么時候用解釋器執(zhí)行聊疲?什么時候用編譯器執(zhí)行?
當(dāng)程序剛開始啟動的時候沪悲,解釋器優(yōu)先執(zhí)行获洲,省去編譯時間,當(dāng)程序運行后殿如,隨著時間的推移贡珊,編譯器逐漸發(fā)揮作用最爬,把越來越多的代碼編譯成本地代碼。那么门岔,什么時候編譯器開始執(zhí)行呢爱致?
編譯對象(熱點代碼)與觸發(fā)條件
- 被多次調(diào)用的方法
- 被多次調(diào)用的循環(huán)體(實際編譯的事整個方法)
方法替換使用的是棧上替換(On Stack Replacement)
判斷一段代碼是不是熱點代碼,是否需要觸發(fā)即時編譯寒随,這樣的行為成為熱點探測糠悯。熱點探測目前流行的有兩種方法
- 基于采樣的熱點探測
虛擬機(jī)周期性檢查各個線程的棧頂,如果出現(xiàn)某個(或某些)方法經(jīng)常出現(xiàn)在棧頂妻往,那就是熱點方法 - 基于計數(shù)器的熱點探測
虛擬機(jī)為某個方法或者代碼塊建立計數(shù)器互艾,統(tǒng)計執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閾值就認(rèn)為它是熱點代碼蒲讯。(閾值:client模式是1500忘朝,server模式是10000,可以通過參數(shù):-XX:CompileThreshold 設(shè)置)
計數(shù)器又分為:
2.1 方法調(diào)用計數(shù)器
2.2 回邊計數(shù)器
- 如何從外部觀察即時編譯器編譯過程和編譯結(jié)果判帮?
使用debug局嘁,fastdebug版本的虛擬機(jī)(JDK6u25之后就不提供下載了),運行時晦墙,添加參數(shù)-XX:+PrintCompilation
- 編譯優(yōu)化項
代表
- 1 方法內(nèi)聯(lián):1. 除去方法調(diào)用的成本悦昵;2. 為其他優(yōu)化建立良好的基礎(chǔ)
class A {
int age;
public int getAge() {
return age;
}
}
public static void main(String[] args) {
A a = new A();
int y = a.getAge();
}
//優(yōu)化后的代碼如下(用Java代碼表示)
public static void main(String[] args) {
A a = new A();
int y = a.age;
}
- 2 消除冗余代碼
- 3 代碼復(fù)寫傳播
int y = 2;
z = y;
int sum = y+ z;
//復(fù)寫傳播
y=y;
int sum = y+y;
//消除冗余代碼后
int sum = y+y;
典型代表
1. 公共子表達(dá)式消除:如果一個表達(dá)式E已經(jīng)計算過了,并且從先前的計算到現(xiàn)在E中的所有變量的值都沒有改變晌畅,那么E的這次出現(xiàn)就成為了公共子表達(dá)式但指,也就沒必要再花時間對他進(jìn)行計算了(若a+b=c, 則int i = a+b+1 --> int i = c+1)
2. 數(shù)組范圍檢查消除:1. 編譯時檢查,2. 通過數(shù)據(jù)流分析
3. 方法內(nèi)聯(lián):就是把目標(biāo)方法的代碼拷貝到調(diào)用方抗楔,避免發(fā)生真實的方法調(diào)用
4. 逃逸分析(JDK1.6開始):它并不是直接優(yōu)化代碼的手段棋凳,而是為其他優(yōu)化手段提供依據(jù)的分析技術(shù)。逃逸分析的基本行為就是分析對象動態(tài)作用域连躏。當(dāng)一個對象在方法中被定義剩岳,通過參數(shù)的形式傳遞到其他方法中,稱為方法逃逸入热。甚至有可能被外部線程訪問到拍棕,譬如賦值給類變量或在其他線程中訪問實例變量,稱為線程逃逸勺良。如果能夠確定一個對象不會逃逸到方法或者線程之外绰播,則可以為這個對象做一些高效的優(yōu)化
4.1 棧上分配:將對象分配在棧上,內(nèi)存空間隨著棧幀出棧而銷毀尚困,減少gc回收的壓力
4.2 同步消除:如果確定一個對象不會逃逸出線程蠢箩,就無須對這個對象實施通過的措施(線程同步是一個相對耗時的過程)
4.3 標(biāo)量替換:標(biāo)量是指不能再拆解的數(shù)據(jù)類型纤控,如原始數(shù)據(jù)類型厉斟。如果一個數(shù)據(jù)還能被分解商蕴,它就稱為聚合量蒿柳,如對象径缅。標(biāo)量替換就是將對象的成員變量替換為原始的數(shù)據(jù)類型粘拾。逃逸分析證明一個對象不會被外部訪問撒犀,并且這個對象可以被拆解伶丐,那么程序執(zhí)行的時候可能就不會真正的創(chuàng)建這個對象跨跨,而是創(chuàng)建它的若干個被這個方法訪問的成員變量
- Java與C++的編譯器對比
即:即時編譯器與靜態(tài)編譯器的對比
- 即時編譯器占用的是用戶運行的時間潮峦,具有很大的時間壓力。而編譯時間成本在靜態(tài)編譯器中并不是主要關(guān)注點
- Java語言是動態(tài)類型安全語言勇婴。這就意味著由虛擬機(jī)來確保程序不會違反語義和非結(jié)構(gòu)化內(nèi)存忱嘹,這就使得虛擬機(jī)得頻繁的動態(tài)檢查空指針,數(shù)組下標(biāo)范圍耕渴,類型轉(zhuǎn)換等
- Java中雖然沒有virtual關(guān)鍵字拘悦,但是接受者進(jìn)行動態(tài)選擇的頻率要遠(yuǎn)遠(yuǎn)大于C/C++語言,優(yōu)化難度要大于靜態(tài)編譯器
- Java語言是動態(tài)擴(kuò)展語言橱脸,運行時加載新的類可能會改變程序類型的繼承關(guān)系
- Java語言中的對象都是在堆上分配础米,只有方法中的局部變量才在棧上分配。而C/C++則有多種內(nèi)存分配添诉,既可以在堆上屁桑,也可以在棧上。C/C++主要是用戶程序代碼回收內(nèi)存分配栏赴,因此效率上要高于Java