逃逸分析首先我們需要知道,逃逸分析并不是直接的優(yōu)化手段,而是通過" />

国产成人精品无码青草_亚洲国产美女精品久久久久∴_欧美人与鲁交大毛片免费_国产果冻豆传媒麻婆精东

15158846557 在線咨詢 在線咨詢
15158846557 在線咨詢
所在位置: 首頁 > 營銷資訊 > 網(wǎng)站運(yùn)營 > JVM系列之:關(guān)于逃逸分析的學(xué)習(xí)

JVM系列之:關(guān)于逃逸分析的學(xué)習(xí)

時間:2023-06-29 06:18:01 | 來源:網(wǎng)站運(yùn)營

時間:2023-06-29 06:18:01 來源:網(wǎng)站運(yùn)營

JVM系列之:關(guān)于逃逸分析的學(xué)習(xí):上文講解完方法內(nèi)聯(lián)后,JIT 即時編譯還有一個最前沿的優(yōu)化技術(shù):逃逸分析(Escape Analysis) 。廢話少說,我們直接步入正題吧。

逃逸分析

首先我們需要知道,逃逸分析并不是直接的優(yōu)化手段,而是通過動態(tài)分析對象的作用域,為其它優(yōu)化手段提供依據(jù)的分析技術(shù)。具體而言就是:

逃逸分析是“一種確定指針動態(tài)范圍的靜態(tài)分析,它可以分析在程序的哪些地方可以訪問到指針”。Java虛擬機(jī)的即時編譯器會對新建的對象進(jìn)行逃逸分析,判斷對象是否逃逸出線程或者方法。即時編譯器判斷對象是否逃逸的依據(jù)有兩種:

  1. 對象是否被存入堆中(靜態(tài)字段或者堆中對象的實例字段),一旦對象被存入堆中,其他線程便能獲得該對象的引用,即時編譯器就無法追蹤所有使用該對象的代碼位置。
    簡單來說就是,如類變量或?qū)嵗兞?,可能被其它線程訪問到,這就叫做線程逃逸,存在線程安全問題。
  2. 對象是否被傳入未知代碼中,即時編譯器會將未被內(nèi)聯(lián)的代碼當(dāng)成未知代碼,因為它無法確認(rèn)該方法調(diào)用會不會將調(diào)用者或所傳入的參數(shù)存儲至堆中,這種情況,可以直接認(rèn)為方法調(diào)用的調(diào)用者以及參數(shù)是逃逸的。(未知代碼指的是沒有被內(nèi)聯(lián)的方法調(diào)用)
    比如說,當(dāng)一個對象在方法中定義之后,它可能被外部方法所引用,作為參數(shù)傳遞到其它方法中,這叫做方法逃逸,
? 方法逃逸我們可以用個案例來演示一下:

//StringBuffer對象發(fā)生了方法逃逸public static StringBuffer createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; } public static String createString(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }關(guān)于逃逸分析技術(shù),本人想過用代碼展示對象是否發(fā)生了逃逸,比如說上述代碼,根據(jù)理論知識可以認(rèn)為 createStringBuffer 方法中發(fā)生了逃逸,但是具體是個什么情況,咱們都不清楚。雖然 JVM 有個參數(shù) PrintEscapeAnalysis 可以顯示分析結(jié)果,但是該參數(shù)僅限于 debug 版本的 JDK 才可以進(jìn)行調(diào)試,多次嘗試后,未能編譯出 debug 版本的 JDK,暫且沒什么思路,所以查看逃逸分析結(jié)果這件事先往后放一放,后續(xù)學(xué)習(xí) JVM 調(diào)優(yōu)再進(jìn)一步來學(xué)習(xí)。

基于逃逸分析的優(yōu)化

即時編譯器可以根據(jù)逃逸分析的結(jié)果進(jìn)行諸如同步消除、棧上分配以及標(biāo)量替換的優(yōu)化。

同步消除(鎖消除)

線程同步本身比較耗費(fèi)資源,JIT 編譯器可以借助逃逸分析來判斷,如果確定一個對象不會逃逸出線程,無法被其它線程訪問到,那該對象的讀寫就不會存在競爭,則可以消除對該對象的同步鎖,通過-XX:+EliminateLocks(默認(rèn)開啟)可以開啟同步消除。 這個取消同步的過程就叫同步消除,也叫鎖消除。

我們還是通過案例來說明這一情況,來看看何種情況需要線程同步。

首先構(gòu)建一個 Worker 對象

@Getterpublic class Worker { private String name; private double money; public Worker() { } public Worker(String name) { this.name = name; } public void makeMoney() { money++; }}測試代碼如下:

public class SynchronizedTest { public static void work(Worker worker) { worker.makeMoney(); } public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); Worker worker = new Worker("hresh"); new Thread(() -> { for (int i = 0; i < 20000; i++) { work(worker); } }, "A").start(); new Thread(() -> { for (int i = 0; i < 20000; i++) { work(worker); } }, "B").start(); long end = System.currentTimeMillis(); System.out.println(end - start); Thread.sleep(100); System.out.println(worker.getName() + "總共賺了" + worker.getMoney()); }}執(zhí)行結(jié)果如下:

52hresh總共賺了28224.0可以看出,上述兩個線程同時修改同一個 Worker 對象的 money 數(shù)據(jù),對于 money 字段的讀寫發(fā)生了競爭,導(dǎo)致最后結(jié)果不正確。像上述這種情況,即時編譯器經(jīng)過逃逸分析后認(rèn)定對象發(fā)生了逃逸,那么肯定不能進(jìn)行同步消除優(yōu)化。

換個對象不發(fā)生逃逸的情況試一下。

//JVM參數(shù):-Xms60M -Xmx60M -XX:+PrintGCDetails -XX:+PrintGCDateStampspublic class SynchronizedTest { public static void lockTest() { Worker worker = new Worker(); synchronized (worker) { worker.makeMoney(); } } public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); new Thread(() -> { for (int i = 0; i < 500000; i++) { lockTest(); } }, "A").start(); new Thread(() -> { for (int i = 0; i < 500000; i++) { lockTest(); } }, "B").start(); long end = System.currentTimeMillis(); System.out.println(end - start); }}輸出結(jié)果如下:

56Heap PSYoungGen total 17920K, used 9554K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000) eden space 15360K, 62% used [0x00000007bec00000,0x00000007bf5548a8,0x00000007bfb00000) from space 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000) to space 2560K, 0% used [0x00000007bfb00000,0x00000007bfb00000,0x00000007bfd80000) ParOldGen total 40960K, used 0K [0x00000007bc400000, 0x00000007bec00000, 0x00000007bec00000) object space 40960K, 0% used [0x00000007bc400000,0x00000007bc400000,0x00000007bec00000) Metaspace used 4157K, capacity 4720K, committed 4992K, reserved 1056768K class space used 467K, capacity 534K, committed 640K, reserved 1048576K在 lockTest 方法中針對新建的 Worker 對象加鎖,并沒有實際意義,經(jīng)過逃逸分析后認(rèn)定對象未逃逸,則會進(jìn)行同步消除優(yōu)化。JDK8 默認(rèn)開啟逃逸分析,我們嘗試關(guān)閉它,再看看輸出結(jié)果。

-Xms60M -Xmx60M -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+PrintGCDateStamps輸出結(jié)果變?yōu)椋?br>
732022-03-01T14:51:08.825-0800: [GC (Allocation Failure) [PSYoungGen: 15360K->1439K(17920K)] 15360K->1447K(58880K), 0.0018940 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] Heap PSYoungGen total 17920K, used 16340K [0x00000007bec00000, 0x00000007c0000000, 0x00000007c0000000) eden space 15360K, 97% used [0x00000007bec00000,0x00000007bfa8d210,0x00000007bfb00000) from space 2560K, 56% used [0x00000007bfb00000,0x00000007bfc67f00,0x00000007bfd80000) to space 2560K, 0% used [0x00000007bfd80000,0x00000007bfd80000,0x00000007c0000000) ParOldGen total 40960K, used 8K [0x00000007bc400000, 0x00000007bec00000, 0x00000007bec00000) object space 40960K, 0% used [0x00000007bc400000,0x00000007bc402000,0x00000007bec00000) Metaspace used 4153K, capacity 4688K, committed 4864K, reserved 1056768K class space used 466K, capacity 502K, committed 512K, reserved 1048576K經(jīng)過對比發(fā)現(xiàn),關(guān)閉逃逸分析后,執(zhí)行時間變長,且內(nèi)存占用變大,同時發(fā)生了垃圾回收。

不過,基于逃逸分析的鎖消除實際上并不多見。一般來說,開發(fā)人員不會直接對方法中新構(gòu)造的對象進(jìn)行加鎖,如上述案例所示,lockTest 方法中的加鎖操作沒什么意義。

事實上,逃逸分析的結(jié)果更多被用于將新建對象操作轉(zhuǎn)換成棧上分配或者標(biāo)量替換。

標(biāo)量替換

在講解 Java 對象的內(nèi)存布局時提到過,Java 虛擬機(jī)中對象都是在堆上分配的,而堆上的內(nèi)容對任何線程大都是可見的(除開 TLAB)。與此同時,Java 虛擬機(jī)需要對所分配的堆內(nèi)存進(jìn)行管理,并且在對象不再被引用時回收其所占據(jù)的內(nèi)存。

如果逃逸分析能夠證明某些新建的對象不逃逸,那么 Java 虛擬機(jī)完全可以將其分配至棧上,并且在 new 語句所在的方法退出時,通過彈出當(dāng)前方法的棧楨來自動回收所分配的內(nèi)存空間。這樣一來,我們便無須借助垃圾回收器來處理不再被引用的對象。

但是目前 Hotspot 并沒有實現(xiàn)真正意義上的棧上分配,而是使用了標(biāo)量替換這么一項技術(shù)。

所謂的標(biāo)量,就是僅能存儲一個值的變量,比如 Java 代碼中的局部變量。與之相反,聚合量則可能同時存儲多個值,其中一個典型的例子便是 Java 對象。

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

標(biāo)量替換這項優(yōu)化技術(shù),可以看成將原本對對象的字段的訪問,替換為一個個局部變量的訪問。

如下述案例所示:

public class ScalarTest { public static double getMoney() { Worker worker = new Worker(); worker.setMoney(100.0); return worker.getMoney() + 20; } public static void main(String[] args) { getMoney(); }}經(jīng)過逃逸分析,Worker 對象未逃逸出 getMoney()的調(diào)用,因此可以對聚合量 worker 進(jìn)行分解,得到局部變量 money,進(jìn)行標(biāo)量替換后的偽代碼:

public class ScalarTest { public static double getMoney() { double money = 100.0; return money + 20; } public static void main(String[] args) { getMoney(); }}對象拆分后,對象的成員變量改為方法的局部變量,這些字段既可以存儲在棧上,也可以直接存儲在寄存器中。標(biāo)量替換因為不必創(chuàng)建對象,減輕了垃圾回收的壓力。

另外,可以手動通過-XX:+EliminateAllocations可以開啟標(biāo)量替換(默認(rèn)是開啟的), -XX:+PrintEliminateAllocations(同樣需要debug版本的JDK)查看標(biāo)量替換情況。

棧上分配

故名思議就是在棧上分配對象,其實目前 Hotspot 并沒有實現(xiàn)真正意義上的棧上分配,實際上是標(biāo)量替換。

在一般情況下,對象和數(shù)組元素的內(nèi)存分配是在堆內(nèi)存上進(jìn)行的。但是隨著 JIT 編譯器的日漸成熟,很多優(yōu)化使這種分配策略并不絕對。JIT編譯器就可以在編譯期間根據(jù)逃逸分析的結(jié)果,來決定是否需要創(chuàng)建對象,是否可以將堆內(nèi)存分配轉(zhuǎn)換為棧內(nèi)存分配。

部分逃逸分析

C2 的逃逸分析與控制流無關(guān),相對來說比較簡單。Graal 則引入了一個與控制流有關(guān)的逃逸分析,名為部分逃逸分析(partial escape analysis)。它解決了所新建的實例僅在部分程序路徑中逃逸的情況。

如下代碼所示:

public static void bar(boolean cond) { Object foo = new Object(); if (cond) { foo.hashCode(); }}// 可以手工優(yōu)化為:public static void bar(boolean cond) { if (cond) { Object foo = new Object(); foo.hashCode(); }}假設(shè) if 語句的條件成立的可能性只有 1%,那么在 99% 的情況下,程序沒有必要新建對象。其手工優(yōu)化的版本正是部分逃逸分析想要自動達(dá)到的成果。

部分逃逸分析將根據(jù)控制流信息,判斷出新建對象僅在部分分支中逃逸,并且將對象的新建操作推延至對象逃逸的分支中。這將使得原本因?qū)ο筇右荻鵁o法避免的新建對象操作,不再出現(xiàn)在只執(zhí)行 if-else 分支的程序路徑之中。

我們通過一個完整的測試案例來間接驗證這一優(yōu)化。

public class PartialEscapeTest { long placeHolder0; long placeHolder1; long placeHolder2; long placeHolder3; long placeHolder4; long placeHolder5; long placeHolder6; long placeHolder7; long placeHolder8; long placeHolder9; long placeHoldera; long placeHolderb; long placeHolderc; long placeHolderd; long placeHoldere; long placeHolderf; public static void foo(boolean flag) { PartialEscapeTest o = new PartialEscapeTest(); if (flag) { o.hashCode(); } } public static void main(String[] args) { for (int i = 0; i < 1000000; i++) { foo(false); } }}本次測試選用的是 JDK11,開啟 Graal 編譯器需要配置如下參數(shù):

-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler分別輸出使用 C2 編譯器或 Graal 編譯器的 GC 日志,對應(yīng)命令為:

java -Xlog:gc* PartialEscapeTestjava -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -Xlog:gc* PartialEscapeTest通過對比 GC 日志可以發(fā)現(xiàn)內(nèi)存占用情況不一致,Graal 編譯器下內(nèi)存占用更小一點(diǎn)。

C2

[0.012s][info][gc,heap] Heap region size: 1M[0.017s][info][gc ] Using G1[0.017s][info][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3[0.345s][info][gc,heap,exit ] Heap[0.345s][info][gc,heap,exit ] garbage-first heap total 262144K, used 21504K [0x0000000700000000, 0x0000000800000000)[0.345s][info][gc,heap,exit ] region size 1024K, 18 young (18432K), 0 survivors (0K)[0.345s][info][gc,heap,exit ] Metaspace used 6391K, capacity 6449K, committed 6784K, reserved 1056768K[0.345s][info][gc,heap,exit ] class space used 552K, capacity 571K, committed 640K, reserved 1048576KGraal

[0.019s][info][gc,heap] Heap region size: 1M[0.025s][info][gc ] Using G1[0.025s][info][gc,heap,coops] Heap address: 0x0000000700000000, size: 4096 MB, Compressed Oops mode: Zero based, Oop shift amount: 3[0.611s][info][gc,start ] GC(0) Pause Young (Normal) (G1 Evacuation Pause)[0.612s][info][gc,task ] GC(0) Using 6 workers of 10 for evacuation[0.615s][info][gc,phases ] GC(0) Pre Evacuate Collection Set: 0.0ms[0.615s][info][gc,phases ] GC(0) Evacuate Collection Set: 3.1ms[0.615s][info][gc,phases ] GC(0) Post Evacuate Collection Set: 0.2ms[0.615s][info][gc,phases ] GC(0) Other: 0.6ms[0.615s][info][gc,heap ] GC(0) Eden regions: 24->0(150)[0.615s][info][gc,heap ] GC(0) Survivor regions: 0->3(3)[0.615s][info][gc,heap ] GC(0) Old regions: 0->4[0.615s][info][gc,heap ] GC(0) Humongous regions: 5->5[0.615s][info][gc,metaspace ] GC(0) Metaspace: 8327K->8327K(1056768K)[0.615s][info][gc ] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 29M->11M(256M) 3.941ms[0.615s][info][gc,cpu ] GC(0) User=0.01s Sys=0.01s Real=0.00sCannot use JVMCI compiler: No JVMCI compiler found[0.616s][info][gc,heap,exit ] Heap[0.616s][info][gc,heap,exit ] garbage-first heap total 262144K, used 17234K [0x0000000700000000, 0x0000000800000000)[0.616s][info][gc,heap,exit ] region size 1024K, 9 young (9216K), 3 survivors (3072K)[0.616s][info][gc,heap,exit ] Metaspace used 8336K, capacity 8498K, committed 8832K, reserved 1056768K[0.616s][info][gc,heap,exit ] class space used 768K, capacity 802K, committed 896K, reserved 1048576K查看 Graal 在 JDK11 上的編譯結(jié)果,可以執(zhí)行下述命令:

java -XX:+PrintCompilation -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler -cp /Users/xxx/IdeaProjects/java_deep_learning/src/main/java/com/msdn/java/javac/escape ScalarTest > out-jvmci.txt

總結(jié)

本文介紹了 Java 虛擬機(jī)中即時編譯器的逃逸分析,以及基于逃逸分析的優(yōu)化:同步消除、標(biāo)量替換和棧上分配。另外還擴(kuò)展了解了一下 Graal 編譯器下的部分逃逸分析。

參考文獻(xiàn)

極客時間 鄭雨迪 《深入拆解Java虛擬機(jī)》 逃逸分析

關(guān)鍵詞:分析,學(xué)習(xí),系列,逃逸

74
73
25
news

版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。

為了最佳展示效果,本站不支持IE9及以下版本的瀏覽器,建議您使用谷歌Chrome瀏覽器。 點(diǎn)擊下載Chrome瀏覽器
關(guān)閉