Java 編譯與優化

Java 編譯與優化

Java 的編譯器有三類前端

  1. 前端編譯器,將 .java 文件編譯爲 .class 文件
    • Javac、ECJ
  2. 運行時編譯器,JIT
    • HotSpot 的 C一、C2 編譯器
  3. AOT(Ahead Of Time Compiler),直接將 Java 代碼編譯爲本地可執行文件
    • GCJ(GNU Compiler for the Java)、Excelsior JET

下面簡要介紹下 Java 的編譯與優化java

graph TB A[Java 編譯與優化] B[javac JIT AOT] C[Java 特性] D[僞泛型<br/>陷阱] E[裝箱陷阱] F[-128 127] G[運行時優化 C1 C2] I[棧頂採樣<br/>執行計數] J[Java<br/>vs<br/>C++] K[條件編譯] L[調用&回邊] M[逃逸分析] N[頻率&剪枝] A --> B A --> C C --> D C --> E C --> F A --> G G --> I G --> J E --> F C --> K I --> L G --> M J --> N

[TOC]程序員

編譯與優化

詞法 & 語法分析

幾乎是全部編譯器的第一步,通過這一步,編譯器能夠生成初始語法樹,由於註解的存在,最終語法樹的生成在註解處理以後數組

語法糖

泛型

Java 的泛型是僞泛型緩存

C++ 中的模板是泛型的一種形式,編譯時不一樣類型的泛型參數會促使模板生成新的代碼,這種經過類型膨脹的形式實現的泛型是真實泛型函數

Java 的泛型只展示在代碼中,編譯成字節碼後泛型信息就已經丟失了(底層使用 Object 引用實際對象,實際使用時會有強制類型轉換的過程),下面兩行代碼在相同的 java 文件中時是沒法經過編譯的,由於 class 文件中不容許出現簽名相同的函數(此時泛型信息已經丟失)性能

public void method(List<String>  list) { ... }
public void method(List<Integer> list) { ... }

在 C++ 中,上面的兩個函數在編譯時的簽名是不一樣的,但在 Java 的 class 文件中,這兩個函數的簽名相同,故編譯器報錯優化

泛型陷阱

class 格式規定,只要描述符不徹底一致的兩個方法就能夠共存線程

Java 中類型的描述符是包含返回值的,也就是說只要修改上面那兩個方法中的返回值,這兩個方法就能夠共存,這是設計中的 缺陷,雖然對程序的正常運行沒有影響,但違反了返回值不參與重載的規定設計

Java 新標準中提出了一些新的規範來減小這種狀況對語言的影響,如 Signature 等

自動裝箱 & 循環遍歷

拆箱與裝箱的陷阱

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,Java的==比的是引用地址,小整數有緩存故地址相同
    System.out.println(e == f); // false,默認狀況下 Java 緩存 -128~127 之間的全部整數
    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,異類比較,equals不處理類型轉換,故long!=int 
}
條件編譯

Java 的條件編譯很簡單,只能使用 if 且 if 的條件必須爲常量,編譯後 class 文件中只會留下知足需求的語句塊

註解處理

JDK 5 以後 Java 提供了對註解的支持,Java 支持編譯時註解運行時註解,使用編譯時註解咱們能夠干涉編譯器的行爲

運行時優化

部分商用虛擬機中 Java 程序最開始執行時使用解釋器進行解釋執行,若是發現一塊代碼執行頻繁,JVM 會使用 JIT (即時編譯)對其進行編譯,轉化爲平臺相關的機器碼並進行各類優化以提升執行效率

graph LR A[解釋器<br/>Interperter] B[編譯器<br/>C1 & C2] A -->|即時編譯 JIT|B B -->|<b>逆優化</b> 嘗試優化失敗時回退| A

HotSpot 有兩個即時編譯器,分別稱爲 Client Compiler 和 Server Compiler,或者簡稱 C1 和 C2 編譯器,JVM 會根據運行模式選擇不一樣的編譯器,用戶能夠經過 -client 或者 -server 指定編譯器。固然也能夠強制禁止 JVM 使用運行時編譯器,JVM 全程使用解釋方式運行

棧上替換

OSR(On Stack Replacement),JIT 編譯並優化某個方法的形象說法,優化的方法都位於棧,方法優化即替換已有的解釋執行方法,即棧上替換

判斷一個函數是否是熱點代碼片斷有兩種方式:基於採樣的熱點探測和基於計數器的熱點探測。前者 JVM 週期性的檢查棧頂函數類型,後者 JVM 會爲每個函數維護一個計算器,後者更精確嚴謹些

HotSpot 使用第二種方式並使用了兩類計數器:

  1. 方法調用計數器
    • C1 下函數默認被調用 1500 則執行優化,C2 爲 10000 次
    • 方法計數器在一個時間間隔內會被減半,稱之爲計數器熱度衰減,因此在一段時間內未被優化的函數依舊不會被優化
  2. 回邊計數器
    • 統計一個方法中循環體代碼執行的次數

當前大部分 JVM 設計都把代碼的優化重心放在了 JIT 上,除非特殊狀況,不要關閉 JIT

常見編譯優化技術

JIT 編譯使用了大量編譯技術,本文不作過多介紹,下面僅給出幾個便於理解的方法

  1. 公共子表達式消除,若是一個表達式 E 已經計算過了,後面就再也不重複計算
  2. 數組邊界檢查消除
  3. 方法內聯
  4. 逃逸分析,若是能證實一個方法內的對象不會被對象以外的實體(其餘對象或者線程)訪問到,即對象沒有逃逸當前方法,則能夠對這個對象進行深度的優化,例如將對象分配到棧上,避免垃圾回收等機制形成的消耗
    • 這項技術暫時好像還不穩定,儘可能不要在生產環境開啓
    • C++ 不存在這個問題,由於 C++ 須要程序員本身管理內存空間

編譯性能對比

C++ 爲靜態編譯語言而 Java 爲動態編譯,兩者編譯器各有優劣。C++ 能夠在編譯時進行比較耗時的優化而Java卻不行,由於這樣會影響服務性能;Java 能夠蒐集大量運行時信息(調用頻率、分支頻率預測、裁剪未選中分支等等)來優化代碼但 C++ 不行。其餘對比能夠查閱周志明《深刻理解 Java 虛擬機》

相關文章
相關標籤/搜索