一、JVM介紹
(一)JVM簡述
Java代碼編譯生成class文件,然后在JVM上運行;但是並不是只有Java一種語言可以編譯成為class文件。
1、JVM、JRE、JDK:
JVM:Java虛擬機,提供了class文件的運行支持
JRE:Java運行環境,提供了java應用程序運行所必須的軟件環境,含有JVM和豐富的類庫
JDK:Java開發工具包,包含編寫Java程序鎖必須的編譯、運行等開發工具和JRE(用於編譯程序的javac命令、用於啟動JVM運行Java程序的Java命令、用於生成文檔的Javadoc命令、用於打包的jar命令等)
三者的關系是JDK包含JRE,JRE包含JVM
2、JVM JIT運行方式
JVM有兩種運行方式,Server模式和Client模式,可以通過-server或-client設置JVM的運行參數
Server模式和Client模式的區別:
(1)Server VM模式的初始堆空間會大一點,默認使用的是並行垃圾回收器,啟動慢、運行快
(2)Client VM相對會保守一些,其初始堆空間會小一點,其使用串行的垃圾回收器,目的就是為了讓JVM快速啟動,但是運行速度會比Server模式慢
如果在不指定參數的情況下,JVM在啟動時會根據硬件及操作系統自動選擇模式。
如果是32位操作系統:
Windows系統:使用Client模式啟動
其他操作系統:機器配置超過2核+2G時,默認使用Server模式,斗則使用Client模式
如果是64位操作系統:只有Server模式,沒有Client模式。
以下是使用64位Windows操作系統演示
D:\>java -client -showversion testApplication java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode) D:\>java -server -showversion testApplication java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)
(二)JVM架構
JVM由類加載器、運行時數據區、執行引擎、本地接口、本地庫組成。
類加載器:在JVM啟動時或者類運行時將需要的class文件加載到JVM中
運行時數據區(內存區):存儲運行時的各種數據;將內存划分為若干個區以模擬實際機器上的存儲、記錄、調度等功能,如實際機器上的各種功能寄存器或者PC指針的記錄器等。
執行引擎:負責執行class文件中包含的字節碼指令,類似於CPU
本地接口 & 本地庫:調用使用C或C++實現的接口
(三)JVM執行流程
1、Java編譯器將java文件編譯成虛擬機可運行的class字節碼文件
2、類加載器的字節碼驗證,驗證通過后將其加載到JVM虛擬機中
3、JVM虛擬機運行class文件,並行邏輯處理並與操作系統交互
這里同時存在解釋器和即時編譯器,這里解釋一下編譯執行、解釋執行、即時編譯執行
解釋執行:將class文件一行一行翻譯成機器碼進行,並交由操作系統執行。優點是可以跨平台(這正是Java的優點),缺點是解析需要時間,執行效率低。
編譯執行:將class文件全部編譯成機器碼文件,然后交由操作系統執行,此時操作系統可以直接執行。但是機器碼文件不保存。優點是執行速度快、效率超高、占用內存小,缺點是不能跨平台。
即時編譯執行:將class文件編譯成機器碼文件,並存入內存,以便后續使用。
(四)熱點代碼
上面JVM虛擬機運行時,同時存在使用解釋器進行解釋執行和使用即時編譯器邊編譯邊執行;其是通過判斷代碼是否是熱點代碼來進行不同的處理的,如果是熱點代碼,則使用即時編譯器邊編譯邊執行,如果非熱點代碼,則使用解釋器進行處理
程序中的代碼只有是熱點代碼時,才會被編譯為本地代碼。熱點代碼有兩類:被多次調用的代碼和被多次執行的循環體。
目前主要的熱點探測方式主要有兩種:
1、基於采樣的熱點探測
采用這種探測方式的虛擬機會周期性的檢查各個線程的棧頂,如果發現某些方法經常出現在棧頂,那這個方法就是熱點方法。這種探測方法的好處是簡單有效,可以很方便的獲取方法的調用關系(將調用堆棧展開即可);缺點是不夠精確,很容易受到線程阻塞或者其他外界因素的影響
2、基於計數器的
采用這種探測方式的虛擬機會為每個方法甚至代碼塊建立一個計數器,方法每次被調用,計數器就會加一,如果調用次數達到了閾值,就認為該方法為熱點方法。這種探測方法的優點是足夠准確;缺點是實現比較復雜,且拿不到方法的調用關系。
在Hotspot虛擬機中采用的是基於計數器的熱點探測。其為方法提供了兩個計數器,方法調用計數器可回邊計數器,方法調用計數器是用來統計方法調用次數,回邊計數器是用來統計循環體被循環的次數。當計數器的技術結果達到閾值,則會出發JIT即時編譯。
在Client模式下,熱點代碼計數閾值默認為1500次,Server模式下為10000次。但是也可以通過參數來進行設置:-XX:CompileThreshold=3000,但是JVM中存在熱度衰減,時間段內調用方法的次數減小,計數器就減小。
二、JVM運行參數
JVM的參數主要分為三類:標准參數、-X非標准參數、-XX參數
(一)標准參數
JVM的標准參數是非常穩定的,基本上在后續的版本中都不會改變這些參數,可以使用java -help查看所有的標准參數
C:\Users\licl81>java -help 用法: java [-options] class [args...] (執行類) 或 java [-options] -jar jarfile [args...] (執行 jar 文件) 其中選項包括: -d32 使用 32 位數據模型 (如果可用) -d64 使用 64 位數據模型 (如果可用) -server 選擇 "server" VM 默認 VM 是 server. -cp <目錄和 zip/jar 文件的類搜索路徑> -classpath <目錄和 zip/jar 文件的類搜索路徑> 用 ; 分隔的目錄, JAR 檔案 和 ZIP 檔案列表, 用於搜索類文件。 -D<名稱>=<值> 設置系統屬性 -verbose:[class|gc|jni] 啟用詳細輸出 -version 輸出產品版本並退出 -version:<值> 警告: 此功能已過時, 將在 未來發行版中刪除。 需要指定的版本才能運行 -showversion 輸出產品版本並繼續 -jre-restrict-search | -no-jre-restrict-search 警告: 此功能已過時, 將在 未來發行版中刪除。 在版本搜索中包括/排除用戶專用 JRE -? -help 輸出此幫助消息 -X 輸出非標准選項的幫助 -ea[:<packagename>...|:<classname>] -enableassertions[:<packagename>...|:<classname>] 按指定的粒度啟用斷言 -da[:<packagename>...|:<classname>] -disableassertions[:<packagename>...|:<classname>] 禁用具有指定粒度的斷言 -esa | -enablesystemassertions 啟用系統斷言 -dsa | -disablesystemassertions 禁用系統斷言 -agentlib:<libname>[=<選項>] 加載本機代理庫 <libname>, 例如 -agentlib:hprof 另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help -agentpath:<pathname>[=<選項>] 按完整路徑名加載本機代理庫 -javaagent:<jarpath>[=<選項>] 加載 Java 編程語言代理, 請參閱 java.lang.instrument -splash:<imagepath> 使用指定的圖像顯示啟動屏幕 有關詳細信息, 請參閱 http://www.oracle.com/technetwork/java/javase/documentation/index.html。
在輸出的標准參數中有-version(查看版本)、-showversion(查看版本並繼續)、-D(初始化配置參數)等
例如編寫以下代碼:
public class JavaTest { public static void main(String[] args) { String s = System.getProperty("strKey"); s = s == null ? "" : s; System.out.println("strKey=========================" + s); } }
使用javac進行編譯,並使用java運行(如果出現使用java運行時報無法加載主類的情況,可以看下這篇文章:https://www.cnblogs.com/wangxiaoha/p/6293340.html)
C:\Users\licl81>javac JavaTest.java C:\Users\licl81>java JavaTest strKey=========================
然后開始驗證上面的參數,可以使用-version查看版本
C:\Users\licl81>java -version java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)
使用-D設置參數
C:\Users\licl81>java -DstrKey=123abc JavaTest
strKey=========================123abc
使用-showversion查看版本並做其他事情
C:\Users\licl81>java -showversion -DstrKey=123 JavaTest java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode) strKey=========================123
使用-client或-server設置JVM運行方式
C:\Users\licl81>java -showversion -DstrKey=123 -server JavaTest java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode) strKey=========================123
(二)-X非標准參數
JVM的-X參數是非標准的參數,在不同版本的JVM中,參數會有所不同,可以通過java -X來查看參數
C:\Users\licl81>java -X -Xmixed 混合模式執行(默認) -Xint 僅解釋模式執行 -Xbootclasspath:<用 ; 分隔的目錄和 zip/jar 文件> 設置引導類和資源的搜索路徑 -Xbootclasspath/a:<用 ; 分隔的目錄和 zip/jar 文件> 附加在引導類路徑末尾 -Xbootclasspath/p:<用 ; 分隔的目錄和 zip/jar 文件> 置於引導類路徑之前 -Xdiag 顯示附加診斷消息 -Xnoclassgc 禁用類垃圾收集 -Xincgc 啟用增量垃圾收集 -Xloggc:<file> 將 GC 狀態記錄在文件中(帶時間戳) -Xbatch 禁用后台編譯 -Xms<size> 設置初始 Java 堆大小 -Xmx<size> 設置最大 Java 堆大小 -Xss<size> 設置 Java 線程堆棧大小 -Xprof 輸出 cpu 分析數據 -Xfuture 啟用最嚴格的檢查,預計會成為將來的默認值 -Xrs 減少 Java/VM 對操作系統信號的使用(請參閱文檔) -Xcheck:jni 對 JNI 函數執行其他檢查 -Xshare:off 不嘗試使用共享類數據 -Xshare:auto 在可能的情況下使用共享類數據(默認) -Xshare:on 要求使用共享類數據,否則將失敗。 -XshowSettings 顯示所有設置並繼續 -XshowSettings:system (僅限 Linux)顯示系統或容器 配置並繼續 -XshowSettings:all 顯示所有設置並繼續 -XshowSettings:vm 顯示所有與 vm 相關的設置並繼續 -XshowSettings:properties 顯示所有屬性設置並繼續 -XshowSettings:locale 顯示所有與區域設置相關的設置並繼續 -X 選項是非標准選項。如有更改,恕不另行通知。
在里面挑選幾個比較重要的進行說明:
-Xinit、-Xcomp、-Xmixed:
在解釋執行模式下,-Xint會強制JVM執行所有的字節碼,這樣會降低運行速度;也就是強制使用解釋模式
-Xcomp與-Xint相反,JVM會在第一次使用時將所有的字節碼編譯成本地代碼,從而帶來最大程度的優化(然而很多應用在使用-Xcomp也會有一些性能損失,當然這個損失對比-Xint會小很多,因為-Xcomp並沒有讓JVM啟動所JIT編譯器的所有功能,JIT編譯器會對是否需要編譯做判斷,對於只執行少數次數的代碼不會進行編譯);也就是強制使用編譯模式
-Xmixed:混合模式,將解釋執行和編譯執行進行混合使用,由jvm自己決定使用哪種方式。這也是JVM默認的、推薦的方式。
使用如下,這里需要說明一下,在第一次使用時-Xcomp方式會比-Xint方式慢一點
C:\Users\licl81>java -showversion -Xint JavaTest java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, interpreted mode) strKey========================= C:\Users\licl81>java -showversion -Xcomp JavaTest java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, compiled mode) strKey========================= C:\Users\licl81>java -showversion -Xmixed JavaTest java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode) strKey=========================
-Xsm和-Xsx參數:
-Xsm和-Xsx分別用來設置堆的初始值和最大值。-Xsm:2048m等價於XX:InitialHeapSize,-Xsx:2048m等價於XX:MaxHeapSize
(三)-XX參數
-XX也是非標准參數,用來對JVM調優和debug操作。
-XX的參數有兩種,一種是boolean類型,格式:-XX:[+-],例如-XX:+DisableexplicitGC,用來禁止手動調用System.gc()操作;一種是非boolean類型,格式:-X,例如-XX:NewRatio=1,表示新生代和老年代的比值。
C:\Users\licl81>java -showversion -XX:+DisableExplicitGC JavaTest java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode) strKey========================= C:\Users\licl81>java -showversion -XX:NewRatio=1 JavaTest java version "1.8.0_281" Java(TM) SE Runtime Environment (build 1.8.0_281-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode) strKey=========================
(四)查看JVM運行參數
有些時候我們需要查看jvm的運行參數,這個需求一般有兩種情況,在啟動的時候查看和查看運行中的JVM參數
1、啟動時查看JVM參數
C:\Users\licl81>java -XX:+PrintFlagsFinal JavaTest [Global flags] intx ActiveProcessorCount = -1 {product} uintx AdaptiveSizeDecrementScaleFactor = 4 {product} ...... strKey=========================
2、查看運行中的JVM參數
如果想要查看運行中的JVM參數,就需要使用jinfo命令進行查看了;
首先使用 jps 命令查看正在運行的java進程,也可以使用 jps -l 來查看詳情
C:\Users\licl81>jps 20960 StatisticsApplication 14292 Launcher 11256 Jps 12760 C:\Users\licl81>jps -l 20960 com.taikang.tkpo2o.statistics.StatisticsApplication 29664 sun.tools.jps.Jps 14292 org.jetbrains.jps.cmdline.Launcher 12760
然后使用jinfo命令查看指定java進程的JVM參數,也可以查看指定參數
C:\Users\licl81>jinfo -flags 20960 Attaching to process ID 20960, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.281-b09 Non-default VM flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:CICompilerCount=4 -XX:CompileThreshold=3000 -XX:InitialHeapSize=130023424 -XX:+ManagementServer -XX:MaxHeapSize=2065694720 -XX:MaxNewSize=688390144 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=42991616 -XX:OldSize=87031808 -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC Command line: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:53976,suspend=y,server=n -XX:CompileThreshold=3000 -XX:TieredStopAtLevel=1 -Xverify:none -Dspring.output.ansi.enabled=always -javaagent:C:\Users\licl81\AppData\Local\JetBrains\IntelliJIdea2021.1\captureAgent\debugger-agent.jar -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dfile.encoding=UTF-8 C:\Users\licl81>jinfo -flag MaxHeapSize 20960 -XX:MaxHeapSize=2065694720
四、JIT使用及優化
(一)JIT編譯器簡述
在現在流行的JVM產品中,比如HotSpot,都是既有解釋器又有編譯器,其特點在上面已經說過,解釋器的優點是啟動快,內存占用少,缺點是長時間運行慢;編譯器的優點是長時間運行時,運行速度快,缺點是啟動慢,占用內存比解釋器大。因此解釋器和編譯器的作用場景:
(1)在混合模式下,當程序需要迅速啟動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行。在程序運行后,編譯器開始逐漸發揮作用,把越來越多的代碼編譯后放到本地,從而獲取更高的執行效率。
(2)當程序運行環境中內存資源限制比較大時(比如嵌入式開發),可以使用解釋器執行,從而節省內存空間。如果內存資源比較充足的情況,可以使用編譯器來提升程序運行效率(以空間換時間)。
在HotSpot虛擬機中,有C1(Client Complier)、C2(Server Complier)兩個即時編譯器,分別用在客戶端模式和服務端模式下。至於采用的是哪一種,就看JVM是以哪種模式啟動的。
兩者的區別是:用C1編譯器可以獲取更高的編譯速度,用C2編譯器可以獲得更好的編譯質量,因為C1編譯器主要關注點在局部優化,而放棄耗時較長的全局優化手段;而C2編譯器是專門針對服務端的編譯器,並為服務端的性能配置特別調整過,是一個充分優化過的高級編譯器。
(二)JIT編譯器優化
JIT即時編譯器會通過公共子表達式消除,方法內斂、方法逃逸分析來優化編譯后的代碼。
1、公共子表達式消除
如果一個表達式前面已經計算過,后面也有相同的表達式,那么該表達式就是公共子表達式。公共子表達式分為局部公共子表達式(僅限於程序的基本塊內)和全局公共子表達式(優化范圍涵蓋了多個基本塊)。
舉個栗子:int d = (c*b)*12+a+(a+b*c) 編譯成解機器碼指令如下:
iload_2 // b imul // 計算b*c bipush 12 // 推入12 imul // 計算(c*b)*12 iload_1 // a iadd // 計算(c*b)*12+a iload_1 // a iload_2 // b iload_3 // c imul // 計算b*c iadd // 計算a+b*c iadd // 計算(c*b)*12+a+(a+b*c) istore 4
由於代碼中c*b 和 b*c 是一樣的結果,就可以將 b*c 看作公共子表達式,就會優化成 int d = E*12+a+(a+E)
同時JIT還可以繼續使用“代數簡化”進行優化,再將其優化為:int d = E*13+2a
表達式改變后,機器碼就會少很多,就達到了節省時間的目的。
2、方法內斂
在使用JIT即時編譯時,將方法調用替換為直接使用方法中的內容,這就是方法內斂,他減少了方法調用過程中壓棧和入棧的開銷,同時可以為之后的一些優化手段提供條件。
舉個栗子:下面的代碼可以在使用方法內斂優化前后的對比
private int add4(int x1, int x2, int x3, int x4) { return add2(x1, x2) + add2(x3, x4); } private int add2(int x1, int x2) { return x1 + x2; }
private int add4(int x1, int x2, int x3, int x4) { return x1 + x2 + x3 + x4; }
3、方法逃逸分析
逃逸分析是動態分析對象作用域的分析算法。當一個對象在方法中被定義后,它可以被外部方法所引用,例如作為調用參數傳入到其他方法中,就叫做方法逃逸。也就是說方法外是否可以用到這個對象。
逃逸分析包括:全局變量賦值逃逸、方法返回值逃逸、實例引用發生逃逸、線程逃逸(賦值給類變量或可以在其他線程中訪問的實例變量)
各種逃逸樣例代碼如下所示:
@Slf4j public class MethodEscape { //全局變量 public static Object object; //全局變量賦值逃逸 public void globalVariableEscape(){ object = new Object(); } //方法返回值逃逸 public Object getObject(){ return new Object(); } //實例引用發生逃逸 public void instancePassEscape(){ this.speak(this); } private void speak(MethodEscape methodEscape) { log.info("======"); } }
那么如何可以避免方法逃逸呢,例如下面的代碼
public StringBuffer geneStringBuffer(String s1, String s2){ StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; }
其實可以直接返回String,而不是返回在方法中創建的StringBuffer對象,就可以避免方法逃逸。
public String geneString(String s1, String s2){ StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); }
開啟方法逃逸分析:
上面說了逃逸分析的方法,那么作用呢?其作用就是可以根據分析出來是否存在方法逃逸的結果,來做同步省略、將堆分配轉化為棧分配、分離對象或標量替換。
(1)同步省略 & 同步鎖消除:
如果一個對象只能被一個線程訪問到,那么對該對象的操作可以不考慮同步。
public class EscapeAnalysisTest { public static void main(String[] args) { long a = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { getString("TestLockEliminate ", "Suffix"); } System.out.println("============" + (System.currentTimeMillis() - a)); try { System.out.println("============" + (System.currentTimeMillis() - a)); } catch (Exception e) { System.out.println(e); } } public static String getString(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); } }
如果在運行的時候使用逃逸分析,同時使用了同步鎖消除,執行時長為60毫秒。
C:\Users\licl81>java -XX:+DoEscapeAnalysis -XX:+EliminateLocks EscapeAnalysisTest ============60 ============61
但是不使用同步鎖消除,執行時長為87毫秒
C:\Users\licl81>java -XX:+DoEscapeAnalysis -XX:-EliminateLocks EscapeAnalysisTest ============87 ============87
這里有個常識,sb.append(s1)是會加鎖的
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
(2)將堆分配轉化為棧分配
如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,那么首選則是棧分配,而不是堆分配。
例如下面的代碼:
public class EscapeAnalysisTest { public static void main(String[] args) { long a = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { alloc(); } System.out.println("============" + (System.currentTimeMillis() - a)); try { Thread.sleep(20000); System.out.println("============" + (System.currentTimeMillis() - a)); } catch (Exception e) { System.out.println(e); } } private static void alloc() { UserDemo user = new UserDemo(); } static class UserDemo { } }
在不開啟逃逸分析運行代碼時
java -Xmx2G -Xms2G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError EscapeAnalysisTest
使用jmap查看堆中對象分配情況
C:\Users\licl81>jps 22704 Jps 28376 45032 Launcher 284 EscapeAnalysisTest C:\Users\licl81>jmap -histo 284 num #instances #bytes class name ---------------------------------------------- 1: 1000000 16000000 EscapeAnalysisTest$UserDemo 2: 428 15357144 [I 3: 3311 588224 [C 4: 2322 55728 java.lang.String 5: 481 55144 java.lang.Class 6: 107 40800 [B 7: 792 31680 java.util.TreeMap$Entry 8: 533 31384 [Ljava.lang.Object; 9: 213 9384 [Ljava.lang.String;
可以看到堆中分配了100萬個 EscapeAnalysisTest$UserDemo對象。
如果開啟逃逸分析:
java -Xmx2G -Xms2G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError EscapeAnalysisTest
然后再用jmap查看
C:\Users\licl81>jps 34544 EscapeAnalysisTest 29860 Jps 28376 45032 Launcher C:\Users\licl81>jmap -histo 34544 num #instances #bytes class name ---------------------------------------------- 1: 427 18675832 [I 2: 120837 1933392 EscapeAnalysisTest$UserDemo 3: 3311 588224 [C 4: 2322 55728 java.lang.String 5: 481 55144 java.lang.Class
可以發下只有12萬的數據被分配在堆上。
可以看到上述的對比,在未開啟方法逃逸分析時,雖然UserDemo對象雖然沒有方法逃逸,但是仍然在堆上分配了一百萬個UserDemo對象。開啟方法逃逸分析后,則只在堆上創建了12萬個UserDemo對象。
那么,所有的對象和數組都會被分配到堆上,這句話是不完全正確的,要看是否開啟了方法逃逸分析。
(3)分離對象或標量替換
有的對象可能不需要連續的內存結構存在也可以被訪問到,那么對象的部分內容可以不存儲在內存中,而可以存儲在CPU寄存器上。
在JIT階段,如果經過逃逸分析,發現一個對象不會被外界訪問,那么經過JIT優化,就會把這個對象拆解成若干個其中包含若干個成員變量來替換。
以下面的代碼為例:
public class A{ public int a=1; public int b=2 } public class B { //方法getAB使用類A里面的a,b private void getAB() { A x = new A(); x.a; x.b; } }
由於對象A不會被其他對象訪問,那么就會被標量替換為
public class B { private void getAB(){ a = 1; b = 2; } }