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