老馬的JVM筆記(六)----編譯器優化

轉眼一本書記了一半了,看了個囫圇吞棗,但也不算全無收穫。只是過於硬核,希望可以有實用價值。

這一章其實是告訴你編譯器怎麼優化,不是教你怎麼優化自己的代碼。

6.1 早期優化--Javac編譯器

Java的編譯期有:前期編譯器,把.java編譯成.class文件(javac編譯器);後期運行期編譯器,JIT編譯器(Just In Time Compiler),用於將class文件中的字節碼轉變成機器碼(C1,C2編譯器);靜態提前編譯器,AOT(Ahead Of Time Compiler),可以直接把.java編譯成機器碼(GNU Compiler for the JAVA)。本章主要針對前期編譯器。(早期優化)

1.解析與填充符號表

詞法、語法分析:將語句字節流解析成標記(token)的集合,token爲語句中能拆解出的最小單位(大概就是能被space分隔開的單位)。語法分析會將token序列構造成抽象語法樹(AST)。

填充符號表:符號表是一組裝有符號地址和符號信息的表格,類似鍵值對錶。每個階段都會用到符號表,javac編譯器可以填寫。

2.註解處理器

讀取Annotation。

3.語義分析與字節碼生成

標註檢查:檢查變量在使用時是否被聲明,變量和變量值的類型是否匹配等。編譯期間就會進行變量摺疊。

數據及控制流分析:分析上下文邏輯是否順暢。局部變量使用時是否已被賦值,每個方法是否有返回語句...

解語法糖:語法糖,就是一些可以幫忙偷懶的語句。翻譯成正常語句。

字節碼生成:把前面生成的東西轉化成字節碼,寫入磁盤中。

語法糖

1.泛型與類型擦除

把類型模糊成一種,類似List<E>,本質上都是Object,硬轉,不算是硬核泛型。而且是Java自己把類型擦掉了,就是說List<Integer>和List<String>在編譯期屬於一種東西,會引起混亂,尤其在重載時,分不清。

2.自動裝箱、拆箱與遍歷循環

// 自動裝箱
List<Integer> list = [1,2,3,4];
// 遍歷循環與拆箱
for(int i: list){
    System.out.println(i);
}

3.條件編譯

條件爲常量時編譯器會自己判斷代碼可達性。

if(true){
    System.out.println("true");
}else {
    // unreachable
    System.out.println("false");
}

while(false){
    // unreachable
    System.out.println("false");
}

6.2 晚期(運行期)優化---JIT編譯器

6.2.1 熱點代碼

即時編譯器不是虛擬機必需組件,卻是衡量虛擬機性能的重要組件。Java使用編譯器將代碼預編譯成字節碼,再用字節碼解釋器運行。

在運行過程中,被多次調用的方法,被多次執行的循環體會被標記爲“熱點代碼”。雖然循環體就在方法裏,但java自有辦法看出來一個方法中的多次循環。具體怎麼算多?

1.基於採樣的熱點探測(sample based hot spot detection):週期性檢查每個線程的棧頂,如果該方法經常出現在棧頂則爲熱點代碼,然而怎麼算經常?這是不不準確的。

2.基於計數器的熱點探測(counter based hot spot detectoin):具體就是用閾值。使用計數器統計方法的執行次數,超過閾值就是熱點,那怎麼取閾值?就在程序員選擇了。計數器分兩種,方法調用計數器(invocation counter)與回邊計數器(back edge counter),閾值是兩個計數器之和。方法調用計數器統計方法被調用的次數,如果方法未被編譯,則+1,直到兩計數器之和達到閾值,交給編譯器編譯該方法。如果一定時間(也是自設參數)爲達到閾值,將該方法的方法調用計數器減半,稱爲衰減(counter decay),這段時間叫半衰期(counter half life time)。回邊計數器用於統計循環體,每次循環結束後跳記爲一次回邊。回邊計數器沒有衰減機制。同樣,兩計數器和超過閾值,交給編譯器編譯。

6.2.2 編譯過程

具體編譯器的編譯過程暫且跳過。  就是字節碼->高級中間代碼(High-level intermediate representation)->低級中間代碼(low-level intermediate representation),最後掃描成既期待嗎。

6.2.3 編譯優化

Java虛擬機的優化措施都集中在了JIT中,所以編譯的代碼肯定比解釋器運行快。編譯器有很多方法優化代碼,是JVM給程序員的福利。程序員想優化自己的代碼還要去看別的書,所以這部分一直在跳過。但也看看JVM的優化方式,給自己找找靈感,也給JVM省省事。

內聯(Method Inlining):方法的內聯可以省去調用方法的成本,因爲調用方法就要建立棧幀;另外可以爲其他優化做基礎。內聯是編譯器優化的最佳工具。簡單說就是把方法中的簡單語句直接拽到調用者這裏,省去了調用的過程。多說無益,寫個例子一層層扒去沒用的代碼就完了。

static class B{
    int value;
    final int get(){
        return value;
    }
}

public void foo(){
    y = b.get();
    z = b.get();
    sum = y + z;
}

第一次優化,省去get()的調用。

public void foo(){
    y = b.value;
    z = b.value;
    sum = y + z;
}

y,z等值,沒必要再調用一次value。

public void foo(){
    y = b.value;
    z = y;
    sum = y + z;
}

還要給z開闢內存空間,沒必要。

public void foo(){
    y = b.value;
    sum = y + y;
}

最後就非常簡單了,但這是編譯器做的事,你能這麼寫代碼嗎?不可能。

1.公共子表達式消除

一個表達式如果已經求出來值賦給一個變量,那這個表達式就再也不需要被計算了。這個編譯器可以做到,但程序員能做還是不要經常一個表達式寫好幾次,應該是這個意思。

2.數組邊界檢查消除

不是每次查找數組元素都一定要檢查邊界值,如果已知index只會在[0,array.length)範圍內,就不會超過邊界值。但偷懶一定會有代價。

3.方法內聯

方法內聯不只是簡單的把方法內的代碼複製過來,因爲涉及到多態時選擇會成爲問題。具體怎麼內聯,就交給JVM開發人員了。

“類型繼承關係分析”(Class Hierarchy Analysis),用於確定目前加載出來的:類中,是否存在子類,子類中是否存在抽象類;接口中,是否有多於一種實現。這樣在內聯時,如果沒有遇到虛方法,直接聯;如果是虛方法,在CHA中查,爲了防止查不到,還設計了“逃生門”用於後退。還是很有才的。

4.逃逸分析

逃逸分析用於分析對象的動態作用域。什麼是逃逸?對象在方法中被創建後,被其他線程或外部方法訪問到,稱爲逃逸。逃逸不是一件好事,代碼要低耦合。如果可以證明對象不能逃逸,就可以針對其做出一些優化:

·棧上分配:雖然對象理應被分配到堆上,但如果不逃逸,可以在棧上分配,這樣方法結束,棧銷燬,對象也銷燬。

·同步消除: 沒有逃逸,就不會被裝進同步區。

·標量替換:如果該對象不會逃逸,可以將其拆分成若干標量(標量指Java中最小單位,int,char等)。

6.2.4 Java與C/C++編譯器對比

說Java速度慢,曾經主要因爲解釋器的速度慢。現在的區別主要出於兩者的編譯器速度,Java使用即時編譯器,C++使用靜態編譯器。

Java不行的地方:

1.即時編譯器需要與運行搶時間,勢必增長運行時間。無論怎麼優化,都會在運行時感到延遲。

2.Java是類型安全語言,在運行時需要頻繁檢查變量空指針、數組越界、類型繼承等問題,像一個操心的老母親。

3.因爲面向對象,所以多態,所以虛方法很多。在多態方面,頻率遠高於C++,所以在編譯時需要時間,例如上文提到的內聯。

4.Java支持動態擴展,因此在運行中會改變現有的類繼承關係,而因爲動態,所以不能看到實時全貌,所以變化時會有調整。

5.Java的對象都在堆上分配(非逃逸的棧上對象不提),只有局部變量在棧上分配。而C/C++可以在堆上棧上分配,棧上的對象易於回收。主要在C++的對象都是程序員主動回收,成敗在個人。

Java行的地方:

所有速度不行的原因,都是Java行的地方。時間不是白花的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章