概述
編譯器是一段“不確定”的操作過程
編譯器類型
- 前端編譯器:將Java代碼編譯為class字節(jié)碼
- 代表:
sun公司的javac(Java語言編寫)、Eclipse JDT中的增量編譯器ECJ
- 后端編譯器(JIT編譯器):將字節(jié)碼轉變?yōu)闄C器碼
- 代表:
HotSpot VM的C1、C2編譯器
- 靜態(tài)提前編譯器(AOT編譯器 Ahead of Time Compiler):將Java代碼編譯為機器碼
-代表:GNU Compiler for the java、Excelsior JET
1. 早期(編譯期)優(yōu)化
主要說明Sun Javac的大概編譯過程
- 解析與填充符號表過程
1.1 解析:
1.1.1 詞法分析:將源代碼的字符流轉為標記(Token)集合(例如:關鍵字,變量名,運算符,字面量)
1.1.2 語法分析:根據(jù)Token序列構造抽象語法樹,語法樹的每一個節(jié)點都是一個語法結構(例如:包,類型,修飾符,運算符,接口,返回值,代碼注釋)
1.2 符號表填充:符號表由符號地址和符號信息組成的表格(可以看成K-V鍵值對) - 注解處理
插入式注解處理器的標準API在編譯期間對注解進行處理 - 分析與生成字節(jié)碼
因為由于由語法分析所生成的抽象語法樹不能保證邏輯性,故而語義分析是對正確性的審查
int a =1;
boolean b = false;
int c = a+b;//編譯不能通過
- 3.1 標注檢查
主要檢查:變量使用前是否已經(jīng)被聲明、變量與賦值之間的數(shù)據(jù)類型是否能匹配 - 3.2 數(shù)據(jù)及控制流分析
主要檢查:對程序的上下文更進一步的驗證,如:局部變量在使用前是否已經(jīng)賦值、方法的每條路徑是否都有返回值、是否所有的受檢異常都被正確的處理等問題
//這里的兩個方法,編譯出的字節(jié)碼沒有一點區(qū)別,只是有final修飾的不能被改變
public void test(final int a) {}
public void test(int a) {}
- 3.3 解語法糖
泛型擦除(實際上就是將結果強轉)、變長參數(shù)、自動裝箱/拆箱、內(nèi)部類、枚舉類、斷言語句、對枚舉和字符串的支持、try語句定義和關閉資源等
條件編譯:使用條件為常量的if語句
//1.
public static void main (String[] args) {
if (true) {
System.out.println("1");
} else {
System.out.println("2");
}
}
//在編譯之后的結果
public static void main (String[] args) {
System.out.println("1");
}
//2. 下面語句將會拒絕編譯
public static void main(String[] args) {
while (false) {
System.out.println("錯誤");
}
}
- 字節(jié)碼生成
字節(jié)碼生成不僅僅將前面步驟生成的信息轉換為字節(jié)碼寫到磁盤中,還要進行少量的代碼添加和轉換工作
2. 晚期(運行期)優(yōu)化
部分商用虛擬機(Sun HotSpot、IBM J9)中,Java程序最初通過解釋器(interpreter)解釋執(zhí)行,當虛擬機發(fā)現(xiàn)某個方法或者代碼運行特別頻繁時,就會把這些代碼定義為“熱點代碼”。為了提高熱點代碼的執(zhí)行效率,在運行時,虛擬機會把這些代碼編譯成與本地平臺相關的機器碼,并進行各種層次的優(yōu)化。完成這個任務的編譯器就是即時編譯器(JIT just int time complier)
JRockit沒有解釋器,因此啟動時間較長
- 為何HotSpot 虛擬機要使用解釋器與編譯器并存的架構?
兩種編譯器各有優(yōu)勢: 解釋器:啟動快、執(zhí)行快; 編譯器:執(zhí)行效率高
Client模式啟動速度較快,Server模式啟動較慢,但是啟動進入穩(wěn)定期長期運行之后Server模式的程序運行速度比Client要快很多 - 為何HotSpot 虛擬機要實現(xiàn)兩個不同的即使編譯器?
client compiler:獲取更快的編譯速度;server compiler:獲取更好的編譯質(zhì)量。具體使用哪種,虛擬機會根據(jù)自身版本和宿主機的硬件性能自由選擇,當然也可以強制運行某種模式,無論編譯器采用client compiler 還是server compiler,解釋器與編譯器搭配使用都稱為“混合模式(mixed mode)”,也可以強制虛擬機運行編譯模式(-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í)行?
當程序剛開始啟動的時候,解釋器優(yōu)先執(zhí)行,省去編譯時間,當程序運行后,隨著時間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼。那么,什么時候編譯器開始執(zhí)行呢?
編譯對象(熱點代碼)與觸發(fā)條件
- 被多次調(diào)用的方法
- 被多次調(diào)用的循環(huán)體(實際編譯的事整個方法)
方法替換使用的是棧上替換(On Stack Replacement)
判斷一段代碼是不是熱點代碼,是否需要觸發(fā)即時編譯,這樣的行為成為熱點探測。熱點探測目前流行的有兩種方法
- 基于采樣的熱點探測
虛擬機周期性檢查各個線程的棧頂,如果出現(xiàn)某個(或某些)方法經(jīng)常出現(xiàn)在棧頂,那就是熱點方法 - 基于計數(shù)器的熱點探測
虛擬機為某個方法或者代碼塊建立計數(shù)器,統(tǒng)計執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閾值就認為它是熱點代碼。(閾值:client模式是1500,server模式是10000,可以通過參數(shù):-XX:CompileThreshold 設置)
計數(shù)器又分為:
2.1 方法調(diào)用計數(shù)器

2.2 回邊計數(shù)器

- 如何從外部觀察即時編譯器編譯過程和編譯結果?
使用debug,fastdebug版本的虛擬機(JDK6u25之后就不提供下載了),運行時,添加參數(shù)-XX:+PrintCompilation - 編譯優(yōu)化項
代表
- 1 方法內(nèi)聯(lián):1. 除去方法調(diào)用的成本;2. 為其他優(yōu)化建立良好的基礎
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 代碼復寫傳播
int y = 2;
z = y;
int sum = y+ z;
//復寫傳播
y=y;
int sum = y+y;
//消除冗余代碼后
int sum = y+y;
典型代表
1. 公共子表達式消除:如果一個表達式E已經(jīng)計算過了,并且從先前的計算到現(xiàn)在E中的所有變量的值都沒有改變,那么E的這次出現(xiàn)就成為了公共子表達式,也就沒必要再花時間對他進行計算了(若a+b=c, 則int i = a+b+1 --> int i = c+1)
2. 數(shù)組范圍檢查消除:1. 編譯時檢查,2. 通過數(shù)據(jù)流分析
3. 方法內(nèi)聯(lián):就是把目標方法的代碼拷貝到調(diào)用方,避免發(fā)生真實的方法調(diào)用
4. 逃逸分析(JDK1.6開始):它并不是直接優(yōu)化代碼的手段,而是為其他優(yōu)化手段提供依據(jù)的分析技術。逃逸分析的基本行為就是分析對象動態(tài)作用域。當一個對象在方法中被定義,通過參數(shù)的形式傳遞到其他方法中,稱為方法逃逸。甚至有可能被外部線程訪問到,譬如賦值給類變量或在其他線程中訪問實例變量,稱為線程逃逸。如果能夠確定一個對象不會逃逸到方法或者線程之外,則可以為這個對象做一些高效的優(yōu)化
4.1 棧上分配:將對象分配在棧上,內(nèi)存空間隨著棧幀出棧而銷毀,減少gc回收的壓力
4.2 同步消除:如果確定一個對象不會逃逸出線程,就無須對這個對象實施通過的措施(線程同步是一個相對耗時的過程)
4.3 標量替換:標量是指不能再拆解的數(shù)據(jù)類型,如原始數(shù)據(jù)類型。如果一個數(shù)據(jù)還能被分解,它就稱為聚合量,如對象。標量替換就是將對象的成員變量替換為原始的數(shù)據(jù)類型。逃逸分析證明一個對象不會被外部訪問,并且這個對象可以被拆解,那么程序執(zhí)行的時候可能就不會真正的創(chuàng)建這個對象,而是創(chuàng)建它的若干個被這個方法訪問的成員變量
- Java與C++的編譯器對比
即:即時編譯器與靜態(tài)編譯器的對比
- 即時編譯器占用的是用戶運行的時間,具有很大的時間壓力。而編譯時間成本在靜態(tài)編譯器中并不是主要關注點
- Java語言是動態(tài)類型安全語言。這就意味著由虛擬機來確保程序不會違反語義和非結構化內(nèi)存,這就使得虛擬機得頻繁的動態(tài)檢查空指針,數(shù)組下標范圍,類型轉換等
- Java中雖然沒有virtual關鍵字,但是接受者進行動態(tài)選擇的頻率要遠遠大于C/C++語言,優(yōu)化難度要大于靜態(tài)編譯器
- Java語言是動態(tài)擴展語言,運行時加載新的類可能會改變程序類型的繼承關系
- Java語言中的對象都是在堆上分配,只有方法中的局部變量才在棧上分配。而C/C++則有多種內(nèi)存分配,既可以在堆上,也可以在棧上。C/C++主要是用戶程序代碼回收內(nèi)存分配,因此效率上要高于Java