偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

淺談JVM運行期的幾種優(yōu)化手段

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

一、摘要

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

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

本文就來聊聊 HotSpot 虛擬機為了提升 Java 程序的執(zhí)行效率,都實現(xiàn)了哪些激動人心的優(yōu)化技術(shù)。

二、JIT 編譯器的引入

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

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

剛開始學(xué)習(xí) Java 的同學(xué),大概率會認(rèn)為 Java 是編譯執(zhí)行,其執(zhí)行流程類似于如下圖。

圖片圖片

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

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

圖片圖片

其中,JIT 編譯器會將熱點代碼編譯成本地平臺相關(guān)的機器碼,并進(jìn)行各種層次的優(yōu)化,從而實現(xiàn)程序執(zhí)行效率的提升。

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

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

簡單的說,解釋器和編譯器各有優(yōu)勢。

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

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

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

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

2.1、Client 模式與 Server 模式

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

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

它們之間的區(qū)別,可以用如下內(nèi)容簡要概括:

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

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

2.2、編譯對象與觸發(fā)條件

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

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

  • 被多次調(diào)用的方法
  • 被多次執(zhí)行的循環(huán)體

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

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

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

  • 基于采樣的熱點探測
  • 基于計數(shù)器的熱點探測

HotSpot 虛擬機中使用的是第二種基于計數(shù)器的熱點探測方法,它為每個方法準(zhǔn)備了兩類計數(shù)器:方法調(diào)用計數(shù)器和回邊計數(shù)器。

在確認(rèn)虛擬機運行參數(shù)的前提下,這兩類計數(shù)器都有一個確認(rèn)的的閥值,當(dāng)計數(shù)器超過閥值時,就會觸發(fā)即時編譯器。

下面我們一起來看看這兩類計數(shù)器的實現(xiàn)。

2.2.1、方法調(diào)用計數(shù)器

方法調(diào)用計數(shù)器,通常用于統(tǒng)計方法被調(diào)用的次數(shù)。它的默認(rèn)閾值在Client模式下是 1500 次,在Server模式下是 10000 次,這個閾值可以通過-XX:CompileThreshold參數(shù)來人為設(shè)定。

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

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

圖片圖片

如果不設(shè)置閥值的情況下,方法調(diào)用計數(shù)器統(tǒng)計的并不是方法被調(diào)用的絕對次數(shù),而是一個相對的執(zhí)行頻率,即一段時間之內(nèi)方法被調(diào)用的次數(shù)。

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

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

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

2.2.2、回邊計數(shù)器

回邊計數(shù)器,通常用于統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù)。在字節(jié)碼方法循環(huán)體中,遇到控制流向后跳轉(zhuǎn)的指令成為"回邊",這個過程會產(chǎn)生“棧上替換”的行為,也就是方法棧幀還在棧上,只是方法被替換了,HotSpot 把這個過程觸發(fā)的即時編譯,稱之為 OSR 編譯。

關(guān)于回邊計數(shù)器的閾值設(shè)置,虛擬機沒有明確給出對應(yīng)的參數(shù),但是可以通過-XX:OnStackReplacePercentage參數(shù)來間接的調(diào)整回邊計數(shù)器的閾值,這個參數(shù)也稱為 ORS 比率,回邊計數(shù)器的閾值計算公式如下:

  • Client 模式:方法調(diào)用計數(shù)器閾值 × OSR 比率 / 1000,其中 OSR 比率默認(rèn)值933,如果都取默認(rèn)值,回邊計數(shù)器的閾值應(yīng)該是 13995
  • Server 模式:方法調(diào)用計數(shù)器閾值 × ( OSR 比率 - 解釋器監(jiān)控比率) / 100,其中 OSR 比率默認(rèn) 140,解釋器監(jiān)控比率默認(rèn)33,如果都取默認(rèn)值,回邊計數(shù)器閾值應(yīng)該是 10700

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

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

圖片圖片

與方法計數(shù)器不同,回邊計數(shù)器沒有熱度衰減的過程,因此這個計數(shù)器統(tǒng)計的就是該方法循環(huán)執(zhí)行的絕對次數(shù),當(dāng)回邊計數(shù)器溢出的時候,虛擬機還會把方法計數(shù)器的值也調(diào)整成溢出狀態(tài),這樣下次再進(jìn)入該方法的時候,就會執(zhí)行標(biāo)準(zhǔn)的編譯過程。

三、運行期優(yōu)化技術(shù)

HotSpot 虛擬機設(shè)計團(tuán)隊為了實現(xiàn)程序更快的執(zhí)行效率,列出了很多的優(yōu)化手段,比如方法內(nèi)聯(lián)、冗余訪問消除、復(fù)寫傳播、無用代碼消除、公共子表達(dá)式消除、數(shù)組邊界檢查消除、逃逸分析等。

下面我們抽取幾個最常見的優(yōu)化技術(shù),一起來看看相關(guān)的實現(xiàn)。

3.1、公共子表達(dá)式消除

公共子表達(dá)式消除是一個普遍應(yīng)用于各種編譯器的經(jīng)典優(yōu)化技術(shù)。

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

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

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

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

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

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

即時編譯器還可能進(jìn)行另一種叫做代數(shù)簡化的優(yōu)化,把表達(dá)式變?yōu)椋?/p>

// 代數(shù)簡化
int d = E * 13 + a + a;

表達(dá)式變換之后,再次計算可以節(jié)省一些時間。

3.2、數(shù)組邊界檢查消除

數(shù)組邊界檢查消除也是一個經(jīng)典優(yōu)化技術(shù)。

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

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

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

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

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

優(yōu)化后的偽代碼。

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

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

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

3.3、方法內(nèi)聯(lián)

方法內(nèi)聯(lián)是 JVM 最重要的優(yōu)化手段之一,它可以去除方法調(diào)用的成本(如減少建立棧幀等),為其它優(yōu)化建立了良好的基礎(chǔ)。

虛擬機如果探測到某個方法是熱點方法并且長度不太長時,會進(jìn)行內(nèi)聯(lián),所謂的內(nèi)聯(lián)就是把方法內(nèi)代碼拷貝、粘貼到調(diào)用者的位置。

舉個例子!

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

優(yōu)化之后可能變成:

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

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

再比如下面這個例子!

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

優(yōu)化之后可能變成:

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

虛擬機還能夠進(jìn)行常量折疊(constant folding)的優(yōu)化,減少不必要的代碼執(zhí)行環(huán)節(jié),從而提升代碼執(zhí)行效率。

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

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

但是如果將方法聲明為final,這些方法是無法被重寫,方法 A 調(diào)用方法 B 基本上是可以完全確定的,可以進(jìn)行方法內(nèi)聯(lián)操作。

3.4、逃逸分析

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

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

逃逸分析的基本行為是分析對象動態(tài)作用域。

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

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

3.4.1、棧上分配

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

3.4.2、同步消除

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

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

3.4.3、標(biāo)量替換

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

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

關(guān)于逃逸分析的論文早在 1999 年就已經(jīng)發(fā)表,但直到 Sun JDK1.6 才實現(xiàn)了逃逸分析,直到現(xiàn)在這項優(yōu)化尚未足夠成熟,仍有很大改進(jìn)余地。

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

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

四、小結(jié)

本文主要圍繞 JVM 在運行期對代碼采取的一些優(yōu)化手段,進(jìn)行了一次知識內(nèi)容整合和總結(jié),希望能幫助到大家。

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

五、參考

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

責(zé)任編輯:武曉燕 來源: Java極客技術(shù)
相關(guān)推薦

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索引數(shù)據(jù)庫

2010-09-17 17:10:30

MS JVM

2010-09-27 09:23:42

JVM調(diào)優(yōu)

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

2013-11-25 14:57:04

TCPTCP優(yōu)化

2011-12-16 13:45:22

2011-05-26 13:26:42

if

2009-12-10 10:32:43

2024-09-27 08:57:36

2009-05-04 09:52:49

Oracle優(yōu)化排序

2009-07-09 10:01:26

設(shè)置JVM內(nèi)存分配

2009-07-21 17:41:58

JDBC數(shù)據(jù)源

2022-06-16 07:31:15

MySQL服務(wù)器服務(wù)
點贊
收藏

51CTO技術(shù)棧公眾號