成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

淺談JVM運行期的幾種優化手段

開發 前端
Java 中最典型的聚合量是對象,如果逃逸分析證明一個對象不會被外部訪問,并且這個對象是可分解的,那程序真正執行的時候將可能不創建這個對象,而改為直接創建它的若干個被這個方法使用到的成員變量來代替,拆散后的變量便可以被單獨分析與優化,可以各自分別在棧幀或寄存器上分配空間,原本的對象就無需整體分配空間了。

一、摘要

在之前的文章中我們談到過,相比 C/C++ 語言,Java 語言在運行效率方面要稍遜一些,因為 Java 應用程序是在虛擬機上運行,而 C/C++ 程序是直接編譯成平臺相應的機器碼來運行程序。

從虛擬機對外發布開始,開發團隊一直在努力試圖縮小 Java 與 C/C++ 語言在運行效率上的差距。從實際的結果來看,確實成果顯著。

本文就來聊聊 HotSpot 虛擬機為了提升 Java 程序的執行效率,都實現了哪些激動人心的優化技術。

二、JIT 編譯器的引入

JIT 編譯器,也稱為即時編譯器,它是 JVM 的重要組成部分。與我們經常用的生成 Java 字節碼的javac編譯器不同,JIT 編譯器是實現 Java 程序執行效率提升的核心利器。

經常有面試官會提出這樣的一個問題:Java 程序是解釋執行還是編譯執行?

剛開始學習 Java 的同學,大概率會認為 Java 是編譯執行,其執行流程類似于如下圖。

圖片圖片

源碼程序.java文件,通過javac命令編譯成.class字節碼,最后通過java命令在虛擬機中利用解釋器來執行代碼。其中虛擬機的解釋器作用,就是將字節碼的操作指令和真正的平臺體系之間的指令建立映射,比如把 Java 的load指令轉換成native code的load指令,以此來完成程序的執行。

其實,準確的說,Java 既有解釋執行,也有編譯執行,其工作流程大致可以用如下圖來描述。

圖片圖片

其中,JIT 編譯器會將熱點代碼編譯成本地平臺相關的機器碼,并進行各種層次的優化,從而實現程序執行效率的提升。

JIT 編譯器的出現,可以說補強了虛擬機邊運行邊解釋的低性能問題。

也許有的同學會提出這樣的疑問,既然引入了 JIT 編譯器可以顯著提升程序執行效率,那 HotSpot 為什么不直接采用 JIT 編譯器來執行呢?

簡單的說,解釋器和編譯器各有優勢。

  • 當程序需要迅速啟動和執行時,解釋器可以首先發揮作用,省去編譯的時間,可以立即執行
  • 當程序運行后,隨著時間的推移,JIT 編譯器可以發揮作用,能把越來越多的代碼編譯成本地機器碼,進一步提升程序的執行效率

這就是為什么 Java 程序既有解釋執行,也有編譯執行的原因。

當然,能觸發即時編譯請求的條件比較多,比如方法調用,OSR 編譯請求等。在默認設置下,無論是哪種場景,虛擬機在代碼編譯器還未完成的時候,都仍然按照解釋器來繼續執行,而編譯動作則是在后臺的編譯線程中運行。

用戶可以通過-XX:-BackgroundCompilation參數來禁止后臺編譯,此時所有的編譯請求會等待,直到編譯完成后再開始執行本地機器碼。

2.1、Client 模式與 Server 模式

在 HotSpot 虛擬機中內置了兩款即時編譯器,分別是Client Compiler和Server Compiler,也稱為 C1 編譯器與 C2 編譯器。

在目前的 HotSpot 虛擬機中,默認采用的是解釋器與其中一個即時編譯器直接配合的工作方式,用戶也可以使用-client或者-server參數來指定解釋器與具體的某個編譯器配合工作。

它們之間的區別,可以用如下內容簡要概括:

  • Client Compiler(C1編譯器):它是一個簡單快速的編譯器,主要關注點在于局部性的優化,而放棄了許多耗時間長的全局優化手段
  • Sever Compiler(C2編譯器):它是專門面向服務端的典型應用并為服務端的性能配置特別調整過的編譯器,它會執行所有經典的優化動作,如無用代碼消除、循環展開、常量傳播、基本塊重排序等,還會實施一些與 Java 語言特性密切相關的優化技術,如范圍檢查消除、空值檢查消除等,另外,還有可能根據解釋器或 Client Compiler 提供的性能監控信息,進行一些不穩定的激進優化,如守護內聯、分支頻率預測等

Sever Compiler 即時編譯器,無疑是比較緩慢的,但它的編譯速度依然遠超傳統的靜態優化編譯器,而且它相對于 Client Compiler 編譯器輸出的代碼質量更高,可以減少本地代碼的執行時間,從而抵消額外的編譯時間開銷,因此很多非服務端的虛擬機選擇-server模式來運行。

2.2、編譯對象與觸發條件

在上文我們有提到,JIT 編譯器會將熱點代碼編譯成本地平臺相關的機器碼。

哪些代碼會被 JIT 編譯器判斷為“熱點代碼”呢?主要有兩類:

  • 被多次調用的方法
  • 被多次執行的循環體

這兩種情況都會使即時編譯器以整個方法作為編譯對象。

比較難以理解的可能是第二種情況,對于被多次執行的循環體,可以理解成以一個方法可能只被調用一次或者少量的幾次,但是方法體內部存在循環次數較多的循環體問題,這樣循環體的代碼也會被重復執行多次,因此這些代碼也被認為是“熱點代碼”。

上面提到的都是概念知識,虛擬機如何判斷一段代碼是否是“熱點代碼”呢?主要有兩種辦法:

  • 基于采樣的熱點探測
  • 基于計數器的熱點探測

HotSpot 虛擬機中使用的是第二種基于計數器的熱點探測方法,它為每個方法準備了兩類計數器:方法調用計數器和回邊計數器。

在確認虛擬機運行參數的前提下,這兩類計數器都有一個確認的的閥值,當計數器超過閥值時,就會觸發即時編譯器。

下面我們一起來看看這兩類計數器的實現。

2.2.1、方法調用計數器

方法調用計數器,通常用于統計方法被調用的次數。它的默認閾值在Client模式下是 1500 次,在Server模式下是 10000 次,這個閾值可以通過-XX:CompileThreshold參數來人為設定。

當一個方法被調用時,會檢查方法是否存在被 JIT 編譯過的版本,如果存在,則優先使用編譯后的本地機器碼來執行;如果不存在,將此方法的調用計數器值加 1,然后判斷方法調用計數器和回邊計數器之和是否超過方法調用計數器的閾值,如果超過,向即時編譯器提交一個該方法的代碼編譯請求,在默認不設置的情況下,不會同步等待編譯請求完成,而時直接以解釋方式執行方法。

具體流程,可以用如下圖來概括。

圖片圖片

如果不設置閥值的情況下,方法調用計數器統計的并不是方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間之內方法被調用的次數。

當超過一定的時間限制,如果方法的調用次數不足以讓它提交給即時編譯器編譯,那這個方法的調用計數器就會少一半,這個過程稱為方法的調用計數器熱度衰減,而這段時間就稱為此方法統計的半衰周期。

進行熱度衰減的動作是在虛擬機進行垃圾回收時順便進行的,可以通過-XX:-UseCounterDecay參數來關閉熱度衰減,讓方法計數器統計方法調用的絕對次數,這樣一來,只要系統運行時間足夠長,基本上絕大部分方法都會被編譯成本地機器碼。

此外,用戶也可以通過-XX:CounterHalfLifeTime參數來設置半衰周期的時間,單位是秒。

2.2.2、回邊計數器

回邊計數器,通常用于統計一個方法中循環體代碼執行的次數。在字節碼方法循環體中,遇到控制流向后跳轉的指令成為"回邊",這個過程會產生“棧上替換”的行為,也就是方法棧幀還在棧上,只是方法被替換了,HotSpot 把這個過程觸發的即時編譯,稱之為 OSR 編譯。

關于回邊計數器的閾值設置,虛擬機沒有明確給出對應的參數,但是可以通過-XX:OnStackReplacePercentage參數來間接的調整回邊計數器的閾值,這個參數也稱為 ORS 比率,回邊計數器的閾值計算公式如下:

  • Client 模式:方法調用計數器閾值 × OSR 比率 / 1000,其中 OSR 比率默認值933,如果都取默認值,回邊計數器的閾值應該是 13995
  • Server 模式:方法調用計數器閾值 × ( OSR 比率 - 解釋器監控比率) / 100,其中 OSR 比率默認 140,解釋器監控比率默認33,如果都取默認值,回邊計數器閾值應該是 10700

當解釋器遇到一條回邊指令時,會先查找需要執行的代碼片段中是否有已經編譯的版本,如果有,會優先執行已編譯好的代碼;如果沒有,就會把回邊計數器的值加 1,然后判斷方法調用計數器和回邊計數器值之和是否超過回邊計數器的閾值,如果超過,就會向即時編譯器提交一個 OSR 編譯請求,并且把回邊計數器的值降低一些,以便繼續在解釋器中執行循環。

具體流程,可以用如下圖來概括。

圖片圖片

與方法計數器不同,回邊計數器沒有熱度衰減的過程,因此這個計數器統計的就是該方法循環執行的絕對次數,當回邊計數器溢出的時候,虛擬機還會把方法計數器的值也調整成溢出狀態,這樣下次再進入該方法的時候,就會執行標準的編譯過程。

三、運行期優化技術

HotSpot 虛擬機設計團隊為了實現程序更快的執行效率,列出了很多的優化手段,比如方法內聯、冗余訪問消除、復寫傳播、無用代碼消除、公共子表達式消除、數組邊界檢查消除、逃逸分析等。

下面我們抽取幾個最常見的優化技術,一起來看看相關的實現。

3.1、公共子表達式消除

公共子表達式消除是一個普遍應用于各種編譯器的經典優化技術。

如果一個子表達式已經計算過了,且表達式中變量的值不曾發生變化,那么這個子表達式就可以當做公共子表達式。

對于這種表達式,沒有必要再花時間去對它進行計算,直接用前面計算過的表達式結果替代就可以了。如果這種優化僅限于程序的基本塊內,便稱為局部公共子表達式消除;如果這種優化的范圍涵蓋了多個基本塊,便稱為全局公共子表達式消除。

舉個簡單的例子,假設存在以下代碼。

// 原始代碼
int d = (c * b) * 12 + a + (a + b * c);

如果這段代碼交給 Javac 編譯器則不會進行任何優化,但是這段代碼進入到虛擬機即時編譯器之后,它將會進行如下優化。

// 將 c*b 和 b*c 用 E 表示,消除公共子表達式
int d = E * 12 + a + (a + E);

即時編譯器還可能進行另一種叫做代數簡化的優化,把表達式變為:

// 代數簡化
int d = E * 13 + a + a;

表達式變換之后,再次計算可以節省一些時間。

3.2、數組邊界檢查消除

數組邊界檢查消除也是一個經典優化技術。

Java 語言作為一門動態安全的語言,會自動對數組的讀寫訪問索引合法性做檢查,當超出地址范圍,會拋出java.lang.ArrayIndexOutOfBoundsException異常,這對軟件開發很友好,但對 JVM 卻是一個性能負擔。

如果能在編譯期根據數據流分析判定索引一直在數組邊界內,就可以消除數組上下邊界的檢測,從而節省很多次條件判斷操作。

類似的消除手段還有空指針檢查(NullPointException)、除數為零檢查(ArithmeticException)、自動裝箱消除(Autobox Elimination)、安全點消除(Safepoint Elimination)、消除反射(Dereflection)等等,針對這些檢查的消除方式,可能會采用隱式異常處理的思路。

舉個簡單的例子,假設存在以下代碼。

// 原始偽代碼
if(foo != null) {
    return foo.value;
} else {
    throw new NullPointException();
}

優化后的偽代碼。

// 隱式異常消除后的偽代碼
try {
    return foo.value;
} catch (segment_fault) {
    uncommon_trap();
}

JVM 會注冊一個 Segment Fault 信號的異常處理器(uncommon_trap() 是一個針對進程層面的異常處理器,與 try-catch 的線程級異常處理器不同),當 foo 不為空,可以省去判空的開銷;如果 foo 真為空,會轉到異常處理器恢復中斷并拋出NullPointException異常。

借助 JVM 在運行期收集的性能監控信息,判定 foo 極少為空時,采用這樣的優化方式可以提升程序的執行效率。

3.3、方法內聯

方法內聯是 JVM 最重要的優化手段之一,它可以去除方法調用的成本(如減少建立棧幀等),為其它優化建立了良好的基礎。

虛擬機如果探測到某個方法是熱點方法并且長度不太長時,會進行內聯,所謂的內聯就是把方法內代碼拷貝、粘貼到調用者的位置。

舉個例子!

public final static void method1(){
    handleA();
    handleB();
}
public static void main(String[] args) {
    handle1();
    method1();
    handle2();
}

優化之后可能變成:

public static void main(String[] args) {
    handle1();
    handleA();
    handleB();
    handle2();
}

從效果上看,就是把method1()方法拷貝到main()方法中。未拷貝之前,優化空間非常小,但是合并到一個方法之后,就有了很大的優化空間了。

再比如下面這個例子!

private final  static int method1(final int i) {
    return i * i;
}
public static void main(String[] args) {
    System.out.println(method1(10));
}

優化之后可能變成:

public static void main(String[] args) {
    System.out.println(100);
}

虛擬機還能夠進行常量折疊(constant folding)的優化,減少不必要的代碼執行環節,從而提升代碼執行效率。

再次想到一個問題,在 Java 編程規范里面,可能很多新人不能理解為什么推薦盡量將方法聲明為final?

我們知道 Java 是多態的特性,子類既可以調用父類方法,也可以重寫父類方法,編程方面靈活性非常高,這樣其實會導致一個問題,編譯期間無法確定應該使用哪一個方法,只有在運行時才能確定,這就可能導致虛擬機很難對方法進行內聯操作。

但是如果將方法聲明為final,這些方法是無法被重寫,方法 A 調用方法 B 基本上是可以完全確定的,可以進行方法內聯操作。

3.4、逃逸分析

逃逸分析,在之前對象內存分配的文章中有所簡單的介紹過,我們再次回顧一下相關的知識。

逃逸分析是一項比較前沿的優化技術,它并不是直接優化代碼的手段,而是為其它優化手段提供了分析技術。

逃逸分析的基本行為是分析對象動態作用域。

當一個對象在方法里面被定義后,它可能被外部方法所引用,例如作為調用參數傳遞到其他方法中,這種稱為方法逃逸;甚至還有可能被外部線程訪問到,譬如賦值給可以在其他線程中訪問的實例變量,這種稱為線程逃逸。

如果能證明一個對象不會逃移到方法外或者線程之外,換句話說就是別的方法或線程無法通過任何途徑訪問到這個對象,則可以通過一些途徑為這個變量進行一些不同程度的優化。

3.4.1、棧上分配

在之前的對象創建文章中,我們提及過,對象會優先在堆上分配,垃圾收集器會定期回收堆空間中不再使用的對象,但這塊的內存回收很耗時。如果確定一個對象不會逃逸出方法之外,讓這個對象在棧上分配,對象所占用的內存空間就可以隨著棧幀出棧而銷毀,這樣垃圾收集器的壓力將會小很多。

3.4.2、同步消除

線程同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變量不會逃逸出線程,無法被其他線程訪問,那么這個變量的讀寫肯定就不會有競爭,此時虛擬機會對這個變量,實施的同步措施進行消除。

比如你定義的類的方法上有同步鎖,但在運行時,卻只有一個線程在訪問,此時逃逸分析后,虛擬機會去掉同步鎖來運行。

3.4.3、標量替換

標量是指一個數據已經無法再分解成更小的數據來表示了,比如 Java 虛擬機中的原始數據類型(int,long 等數值類型以及 reference 類型)等都不能進一步分解,它們可以稱為標量。相對的,如果一個數據可以繼續分解,那它稱為聚合量。

Java 中最典型的聚合量是對象,如果逃逸分析證明一個對象不會被外部訪問,并且這個對象是可分解的,那程序真正執行的時候將可能不創建這個對象,而改為直接創建它的若干個被這個方法使用到的成員變量來代替,拆散后的變量便可以被單獨分析與優化,可以各自分別在棧幀或寄存器上分配空間,原本的對象就無需整體分配空間了。

關于逃逸分析的論文早在 1999 年就已經發表,但直到 Sun JDK1.6 才實現了逃逸分析,直到現在這項優化尚未足夠成熟,仍有很大改進余地。

不成熟的原因主要是不能保證逃逸分析的性能收益必定能高于它的消耗。雖然在實際測試結果中,實施逃逸分析后的程序往往能運行出不錯的成績,但是在實際的應用程序,尤其是大型程序中反而發現實施逃逸分析可能出現效果不穩定的情況,或因分析過程耗時但卻無法有效判別出非逃逸對象而導致性能有所下降。

如果有需要并且確認對程序運行有益,用戶可以使用-XX:+DoEscapeAnalysis參數來手動開啟逃逸分析,開啟之后可以通過-XX:+PrintEscapeAnalysis參數來查看分析結果,用戶還可以使用-XX:+EliminateAllocations參數來開啟標量替換,使用-XX:+EliminatLocks參數來開啟同步消除,使用-XX:+PrintEliminateAllocations參數查看標量的替換情況。

四、小結

本文主要圍繞 JVM 在運行期對代碼采取的一些優化手段,進行了一次知識內容整合和總結,希望能幫助到大家。

內容比較多,如果有描述不對的地方,歡迎留言指出,不勝感激。

五、參考

1.https://www.cnblogs.com/xrq730/p/4857820.html

2.https://juejin.cn/post/7236634386568527928

3.https://blog.csdn.net/ChaoMing_H/article/details/129179684

責任編輯:武曉燕 來源: Java極客技術
相關推薦

2024-03-07 17:21:12

HotSpotJVMHot Code

2011-06-01 14:18:41

JVM

2018-06-29 13:24:48

沙箱容器解決方案

2023-11-11 19:07:23

JVMJava

2024-04-17 12:58:15

MySQL索引數據庫

2010-09-17 17:10:30

MS JVM

2010-09-27 09:23:42

JVM調優

2013-11-25 14:57:04

TCPTCP優化

2011-06-20 10:36:29

SEO

2024-04-24 10:24:09

2009-07-10 14:55:34

2024-01-29 08:24:40

2011-12-16 13:45:22

2009-12-10 10:32:43

2011-05-26 13:26:42

if

2009-07-09 16:33:06

eclipse jvm

2009-07-09 17:36:58

jvm.cfg

2009-07-21 17:41:58

JDBC數據源

2012-03-22 09:31:14

Java

2022-06-16 07:31:15

MySQL服務器服務
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久久aⅴ乱码一区二区三区 91综合网 | 一级欧美日韩 | 欧美色成人 | 欧美中文字幕在线观看 | 久久精品一 | 久久国产一区二区三区 | 在线观看h视频 | www.xxxx欧美 | 免费国产视频在线观看 | 欧美一级免费看 | www国产精品 | 亚洲视频1区 | 久久久久久久久久久久91 | 99热这里有精品 | 精品国产乱码久久久久久a丨 | 99视频网 | 91av精品 | 在线观看毛片网站 | 国产三级精品视频 | 精品国产第一区二区三区 | 三级高清| 免费av在线| 日韩视频在线观看中文字幕 | 高清免费在线 | 真人女人一级毛片免费播放 | 久在线 | 亚洲一区影院 | 毛片久久久 | 91麻豆产精品久久久久久夏晴子 | 毛片一区二区三区 | 欧美激情国产日韩精品一区18 | 777毛片| 国产乱码高清区二区三区在线 | 日本三级日产三级国产三级 | 欧美午夜精品理论片a级按摩 | 欧美一区二区三区视频 | 国产激情一区二区三区 | 亚洲欧美在线免费观看 | 国产精品成人在线观看 | 亚洲欧美日韩在线一区二区 | 伊人超碰 |