JVM进阶之路, 不然又要被面试官吊打了
JVM基本常識
1. 什么使用JVM?
jvm編譯流程
2. 字節(jié)碼和機器碼的區(qū)別
機器碼是電腦CPU直接讀取運行的機器指令, 運行速度最快, 但是非常晦澀難懂, 也比較難編寫, 一般從業(yè)人員接觸不到。
字節(jié)碼是一種中建狀態(tài)(中間碼)的二進制代碼(文件)。需要直譯器轉(zhuǎn)義后才能成為機器碼
3. JDK, JRE, ?JVM的關(guān)系
JDK,JRE,JVM的關(guān)系
4. OracleJDK和OpenJDK
4.1.查看JDK的版本
java -version如果是SUN/OracleJDK, 顯示信息為:
說明:
Java HotSpot(TM) 64-Bit Server VM 表明, 此JDK的JVM是Oracle的64位HotSpot****虛擬機,
運行在Server模式下(虛擬機有Server和Client兩種運行模式) Java(TM) SE Runtime Environment (build 1.8.0_162-b12) 是Java運行時環(huán)境(即JRE)的版
本信息.
如果是OpenJDK, 顯示信息為:
4.2. ?OpenJDK和OracleJDK的區(qū)別
????1. OpenJDK的來歷
Java由SUN公司(Sun Microsystems, 發(fā)起于美國斯坦福大學, SUN是Stanford University Network
的縮寫)發(fā)明, 2006年SUN公司將Java開源, 此時的JDK即為OpenJDK. 也就是說, OpenJDK是Java SE的開源實現(xiàn), 它由SUN和Java社區(qū)提供支持, 2009年Oracle收購了Sun
公司, 自此Java的維護方之一的SUN也變成了Oracle . 大多數(shù)JDK都是在OpenJDK的基礎(chǔ)上編寫實現(xiàn)的, 比如IBM J9, Azul Zulu, Azul Zing和Oracle JDK.
幾乎現(xiàn)有的所有JDK都派生自O(shè)penJDK, 它們之間不同的是許可證:
OpenJDK根據(jù)許可證GPL v2發(fā)布; Oracle JDK根據(jù)Oracle二進制代碼許可協(xié)議獲得許可。
????2. Orcale JDK的來歷
Oracle JDK之前被稱為SUN JDK, 這是在2009年Oracle收購SUN公司之前, 收購后被命名為OracleJDK。
實際上, Oracle JDK是基于OpenJDK源代碼構(gòu)建的, 因此Oracle JDK和OpenJDK之間沒有重大的技術(shù)差異。
Oracle的項目發(fā)布經(jīng)理Joe Darcy在OSCON 2011 上對兩者關(guān)系的介紹也證實了OpenJDK 7和Oracle JDK 7在程序上是非常接近的, 兩者共用了大量相同的代碼(如下圖), 注意: 圖中提示了兩者共同代碼的 占比要遠高于圖形上看到的比例, 所以我們編譯的OpenJDK基本上可以認為性能、功能和執(zhí)行邏輯上 都和官方的Oracle JDK是一致的。
????3. OpenJDK和OracleJDK的區(qū)別
OpenJDK使用的是開源免費的FreeType, 可以按照GPL v2許可證使用.GPL V2允許在商業(yè)上使用;
Oracle JDK則采用JRL(Java Research License,Java研究授權(quán)協(xié)議) 放出.JRL只允許個人研究使 用,要獲得Oracle JDK的商業(yè)許可證, 需要聯(lián)系Oracle的銷售人員進行購買。
4.3. JVM和Hotspot的關(guān)系
JVM是《JVM虛擬機規(guī)范》中提出來的規(guī)范
Hotspot是使用JVM規(guī)范的商用產(chǎn)品, 除此之外還有Orcacle JRockit, IBMde J9也是JVM產(chǎn)品
4.4. JVM和Java的關(guān)系
jvm和java的關(guān)系.jpg
4.5. JVM的運行模式
JVM有兩種運行模式:Server模式與Client模式。
兩種模式的區(qū)別在于:
Client模式啟動速度較快,Server模式啟動較慢;
但是啟動進入穩(wěn)定期長期運行之后Server模式的程序運行速度比Client要快很多。
因為Server模式啟動的JVM采用的是重量級的虛擬機,對程序采用了更多的優(yōu)化;
而Client模式啟 動的JVM采用的是輕量級的虛擬機。所以Server啟動慢,但穩(wěn)定后速度比Client遠遠要快。
4.6. 程序執(zhí)行方式有哪些?
主要有三種:靜態(tài)編譯執(zhí)行、動態(tài)編譯執(zhí)行和動態(tài)解釋執(zhí)行。
JVM架構(gòu)理解
JVM架構(gòu)圖.png
JVM程序執(zhí)行流程
Java編譯成字節(jié)碼、動態(tài)編譯和解釋為機器碼的過程分析:
java編譯執(zhí)行過程.jpg
編譯器和解釋器的協(xié)調(diào)工作流程
編譯器和解釋器的協(xié)調(diào)工作流程.jpg
在部分商用虛擬機中(如HotSpot),Java程序最初是通過解釋器(Interpreter)進行解釋執(zhí)行的,當虛擬機發(fā)現(xiàn)某個方法或代碼塊的運行特別頻繁時,就會把這些代碼認定為“熱點代碼”。為了提高熱點代 碼的執(zhí)行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關(guān)的機器碼,并進行各種層次的 優(yōu)化,完成這個任務的編譯器稱為 (Just In Time Compiler,下文統(tǒng)稱JIT編譯器)
由于Java虛擬機規(guī)范并沒有具體的約束規(guī)則去限制即使編譯器應該如何實現(xiàn),所以這部分功能完全是與 虛擬機具體實現(xiàn)相關(guān)的內(nèi)容,如無特殊說明,我們提到的編譯器、即時編譯器都是指Hotspot虛擬機內(nèi) 的即時編譯器,虛擬機也是特指HotSpot虛擬機。
我們的JIT是屬于動態(tài)編譯方式的, (dynamic compilation)指的是“在運行時進行編譯”;與 之相對的是事前編譯(ahead-of-time compilation,簡稱AOT),也叫 (static compilation)。
JIT編譯(just-in-time compilation)狹義來說是當某段代碼即將第一次被執(zhí)行時進行編譯,因而叫“即 時編譯”。JIT 。JIT編譯一詞后來被泛化, 時常與動態(tài)編譯等價;但要注意廣 義與狹義的JIT編譯所指的區(qū)別。
1. 哪些程序代碼會被即時編譯
程序中的代碼只有是熱點代碼時,才會編譯為本地代碼,那么什么是 呢? 運行過程中會被即時編譯器編譯的“熱點代碼”有兩類:
被多次調(diào)用的方法。
被多次執(zhí)行的循環(huán)體。
兩種情況,編譯器都是以整個方法作為編譯對象。這種編譯方法因為編譯發(fā)生在方法執(zhí)行過程之中,因 此形象的稱之為棧上替換(On Stack Replacement,OSR),即方法棧幀還在棧上,方法就被替換 了。
2. 如何判斷熱點代碼呢?
要知道方法或一段代碼是不是熱點代碼,是不是需要觸發(fā)即時編譯,需要進行Hot Spot Detection(熱點探測)。
目前主要的熱點探測方式有以下兩種:
基于采樣的熱點探測
采用這種方法的虛擬機會周期性地檢查各個線程的棧頂,如果發(fā)現(xiàn)某些方法經(jīng)常出現(xiàn)在棧頂,那這 個方法就是“熱點方法”。這種探測方法的好處是實現(xiàn)簡單高效,還可以很容易地獲取方法調(diào)用關(guān)系 (將調(diào)用堆棧展開即可),缺點是很難精確地確認一個方法的熱度,容易因為受到線程阻塞或別的 外界因素的影響而擾亂熱點探測。
基于計數(shù)器的熱點探測
采用這種方法的虛擬機會為每個方法(甚至是代碼塊)建立計數(shù)器,統(tǒng)計方法的執(zhí)行次數(shù),如果執(zhí) 行次數(shù)超過一定的閥值,就認為它是“熱點方法”。這種統(tǒng)計方法實現(xiàn)復雜一些,需要為每個方法建 立并維護計數(shù)器,而且不能直接獲取到方法的調(diào)用關(guān)系,但是它的統(tǒng)計結(jié)果相對更加精確嚴謹。
3. 熱點檢測方式
在HotSpot虛擬機中使用的是第二種——基于計數(shù)器的熱點探測方法,因此它為每個方法準備了兩個計 數(shù)器: 方法調(diào)用計數(shù)器和回變計數(shù)器 。在確定虛擬機運行參數(shù)的前提下,這兩個計數(shù)器都有一個確定的 閾值,當計數(shù)器超過閾值溢出了,就會觸發(fā)JIT編譯。
方法調(diào)用計數(shù)器 顧名思義,這個計數(shù)器用于統(tǒng)計方法被調(diào)用的次數(shù)。
回邊計數(shù)器
它的作用就是統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為“回邊”。
4. JIT使用
4.1. 為什么要使用解釋器與編譯器并存的架構(gòu)
盡管并不是所有的Java虛擬機都采用解釋器與編譯器并存的架構(gòu),但許多主流的商用虛擬機(如
HotSpot**),都同時包含解釋器和編譯器**。
解釋器與編譯器特點
當程序需要迅速啟動和執(zhí)行的時候,解釋器可以首先發(fā)揮作用,省去編譯的時間,立即執(zhí)行。
在程 序運行后,隨著時間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后,可以 獲取更高的執(zhí)行效率。
當程序運行環(huán)境中內(nèi)存資源限制較大(如部分嵌入式系統(tǒng)中), 可以使用解釋器執(zhí)行節(jié)約內(nèi)存, 反之可以使用編譯執(zhí)行來提升效率
編譯的時間開銷
解釋器的執(zhí)行, 抽象的看成是這樣的
輸入代碼-> 解釋器, 解釋執(zhí)行 -> 執(zhí)行結(jié)果
而要JIT編譯然后在執(zhí)行的話, 抽象的看則是:
輸入代碼-> 編譯器編譯-> 編譯后的代碼 -> 執(zhí)行-> 執(zhí)行結(jié)果
說JIT比解釋快,其實說的是“執(zhí)行編譯后的代碼”比“解釋器解釋執(zhí)行”要快,并不是說“編譯”這個動作 比“解釋”這個動作快。JIT編譯再怎么快,至少也比解釋執(zhí)行一次略慢一些,而要得到最后的執(zhí)行結(jié)果還 得再經(jīng)過一個“執(zhí)行編譯后的代碼”的過程。所以,對“只執(zhí)行一次”的代碼而言,解釋執(zhí)行其實總是比JIT 編譯執(zhí)行要快
怎么算是“只執(zhí)行一次的代碼”呢?粗略說,下面兩個條件同時滿足時就是嚴格的“只執(zhí)行一次”
只被調(diào)用一次,例如類的構(gòu)造器(class initializer,())
沒有循環(huán)
對只執(zhí)行一次的代碼做JIT編譯再執(zhí)行,可以說是得不償失。對只執(zhí)行少量次數(shù)的代碼,JIT編譯帶來的執(zhí)行速度的提升也未必能抵消掉最初編譯帶來的開銷。
只有對頻繁執(zhí)行的代碼, JIT編譯才能保證有正面的收益
編譯的空間開銷
對一般的Java方法而言,編譯后代碼的大小相對于字節(jié)碼的大小,膨脹比達到10x是很正常的。同上面 說的時間開銷一樣,這里的空間開銷也是,只有對執(zhí)行頻繁的代碼才值得編譯,如果把所有代碼都編譯 則會顯著增加代碼所占空間,導致“代碼爆炸”。
這個就解釋了為什么有些JVM在選擇不總是做JIT編譯, 而是選擇用解釋器+JIT編譯器的混合執(zhí)行引擎
4.2. 為何要實現(xiàn)兩個不同的即時編譯器
HotSpot虛擬機中內(nèi)置了兩個即時編譯器:Client Complier和Server Complier,簡稱為C1、C2編譯 器,分別用在客戶端和服務端。
目前主流的HotSpot虛擬機中默認是采用解釋器與其中一個編譯器直接配合的方式工作。程序使用哪個 編譯器,取決于虛擬機運行的模式。HotSpot虛擬機會根據(jù)自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也可以使用“-client”或“-server”參數(shù)去強制指定虛擬機運行在Client模式或Server模式。
用Client Complier獲取更高的編譯速度,用Server Complier 來獲取更好的編譯質(zhì)量。為什么提供多個 即時編譯器與為什么提供多個垃圾收集器類似,都是為了適應不同的應用場景
4.3. 如何編譯本地代碼
Server Compiler和Client Compiler兩個編譯器的編譯過程是不一樣的。
對Client Compiler來說,它是一個簡單快速的編譯器,主要關(guān)注點在于局部優(yōu)化,而放棄許多耗時較長 的全局優(yōu)化手段。
而Server Compiler則是專門面向服務器端的,并為服務端的性能配置特別調(diào)整過的編譯器,是一個充 分優(yōu)化過的高級編譯器。
4.4. JIT優(yōu)化
HotSpot 虛擬機使用了很多種優(yōu)化技術(shù),這里只簡單介紹其中的幾種,完整的優(yōu)化技術(shù)介紹可以參考官網(wǎng)內(nèi)容。
公共子表達式的消除
公共子表達式消除是一個普遍應用于各種編譯器的經(jīng)典優(yōu)化技術(shù),他的含義是:如果一個表達式E已經(jīng) 計算過了,并且從先前的計算到現(xiàn)在E中所有變量的值都沒有發(fā)生變化,那么E的這次出現(xiàn)就成為了公共子表達式。對于這種表達式,沒有必要花時間再對他進行計算,只需要直接用前面計算過的表達式結(jié)果 代替E就可以了。
如果這種優(yōu)化僅限于程序的基本塊內(nèi),便稱為**局部公共子表達式消除(**Local Common Subexpression Elimination)
如果這種優(yōu)化范圍涵蓋了多個基本塊,那就稱為**全局公共子表達式消除(**Global Common Subexpression Elimination)。
舉個簡單的例子來說明他的優(yōu)化過程,假設(shè)存在如下代碼:
int d = (c*b)*12+a+(a+b*c);如果這段代碼交給Javac編譯器則不會進行任何優(yōu)化,那生成的代碼如下所示,是完全遵照Java源碼的寫 法直譯而成的。
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當這段代碼進入到虛擬機即時編譯器后,他將進行如下優(yōu)化:編譯器檢測到”cb“ ”bc“是一樣的表達 式,而且在計算期間b與c的值是不變的。因此,這條表達式就可能被視為:
int d = E*12+a+(a+E);這時,編譯器還可能(取決于哪種虛擬機的編譯器以及具體的上下文而定)進行另外一種優(yōu)化:代數(shù)化 簡(Algebraic Simplification),把表達式變?yōu)?
int d = E*13+a*2;表達式進行變換之后,再計算起來就可以節(jié)省一些時間了。
方法內(nèi)聯(lián)
在使用JIT進行即時編譯時,將方法調(diào)用直接使用方法體中的代碼進行替換,這就是方法內(nèi)聯(lián),減少了方 法調(diào)用過程中壓棧與入棧的開銷。同時為之后的一些優(yōu)化手段提供條件。如果JVM監(jiān)測到一些小方法被 頻繁的執(zhí)行,它會把方法的調(diào)用替換成方法體本身。
比如說下面這個
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; }可以肯定的是運行一段時間后JVM會把add2方法去掉,并把你的代碼翻譯成:
private int add4(int x1, int x2, int x3, int x4) { return x1 + x2 + x3 + x4; }逃逸分析
逃逸分析(Escape Analysis)是目前Java虛擬機中比較前沿的優(yōu)化技術(shù)。這是一種可以有效減少Java 程序 中同步負載和內(nèi)存堆分配壓力的跨函數(shù)全局數(shù)據(jù)流分析算法。通過逃逸分析,Java Hotspot編譯器能夠 分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。
逃逸分析的基本行為就是分析對象動態(tài)作用域:當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他地方中,稱為方法逃逸。
逃逸分析包括:
全局變量賦值逃逸
方法返回值逃逸
實例引用發(fā)生逃逸 線程逃逸:賦值給類變量或可以在其他線程中訪問的實例變量
例如:
public class EscapeAnalysis { //全局變量public static Object object;public void globalVariableEscape(){//全局變量賦值逃逸object = new Object();}public Object methodEscape(){ //方法返回值逃逸return new Object();}public void instancePassEscape(){ //實例引用發(fā)生逃逸this.speak(this);}public void speak(EscapeAnalysis escapeAnalysis){System.out.println("Escape Hello");} }使用方法逃逸的案例進行分析:
public static StringBuffer craeteStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb; }StringBuffer sb是一個方法內(nèi)部變量,上述代碼中直接將sb返回,這樣這個StringBuffer有可 能被其他方法所改變,這樣它的作用域就不只是在方法內(nèi)部,雖然它是一個局部變量,稱其逃逸 到了方法外部。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的 實例變量,稱為線程逃逸。
上述代碼如果想要StringBuffer sb不逃出方法,可以這樣寫:
public static String createStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString(); }不直接返回 StringBuffer,那么StringBuffer將不會逃逸出方法。
使用逃逸分析,編譯器可以對代碼做如下優(yōu)化:
一、同步省略。如果一個對象被發(fā)現(xiàn)只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同 步。
二、將堆分配轉(zhuǎn)化為棧分配。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對 象可能是棧分配的候選,而不是堆分配。
三、分離對象或標量替換。有的對象可能不需要作為一個連續(xù)的內(nèi)存結(jié)構(gòu)存在也可以被訪問到,那么對象 的部分(或全部)可以不存儲在內(nèi)存,而是存儲在CPU寄存器中。
在Java代碼運行時,通過JVM參數(shù)可指定是否開啟逃逸分析,
-XX:+DoEscapeAnalysis : 表示開啟逃逸分析 -XX:-DoEscapeAnalysis : 表示關(guān)閉逃逸分析從jdk 1.7開始已經(jīng)默認開始逃逸分析,如需關(guān)閉,需要指定 -XX:-DoEscapeAnalysis
對象的棧上內(nèi)存分配
我們知道,在一般情況下,對象和數(shù)組元素的內(nèi)存分配是在堆內(nèi)存上進行的。但是隨著JIT編譯器的日漸 成熟,很多優(yōu)化使這種分配策略并不絕對。JIT編譯器就可以在編譯期間根據(jù)逃逸分析的結(jié)果,來決定是 否可以將對象的內(nèi)存分配從堆轉(zhuǎn)化為棧。
我們來看以下代碼:
public class EscapeAnalysisTest {public static void main(String[] args) {long a1 = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {alloc();}// 查看執(zhí)行時間long a2 = System.currentTimeMillis();System.out.println("cost " + (a2 - a1) + " ms");// 為了方便查看堆內(nèi)存中對象個數(shù),線程sleeptry {Thread.sleep(100000);} catch (InterruptedException e1) {e1.printStackTrace();}}//此方法內(nèi)的User對象,未發(fā)生逃逸private static void alloc() {User user = new User();}static class User {} }其實代碼內(nèi)容很簡單,就是使用for循環(huán),在代碼中創(chuàng)建100萬個User對象。我們在alloc方法中定義了User對象,但是并沒有在方法外部引用他。也就是說,這個對象并不會
逃逸到alloc外部。經(jīng)過JIT**的逃逸分析之后,就可以對其內(nèi)存分配進行優(yōu)化
我們指定以下JVM參數(shù)并運行:
-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError在程序打印出 cost XX ms 后,代碼運行結(jié)束之前,我們使用jmap命令,來查看下當前堆內(nèi)存中有多 少個User對象:
~ jps 2809 StackAllocTest 2810 Jps ~ jmap -histo 2809 num #instances #bytes class name ---------------------------------------------- 1: 524 87282184 [I 2: 1000000 16000000 StackAllocTest$User 3: 6806 2093136 [B 4: 8006 1320872 [C 5: 4188 100512 java.lang.String 6: 581 66304 java.lang.Class從上面的jmap執(zhí)行結(jié)果中我們可以看到,堆中共創(chuàng)建了100萬個 StackAllocTest$User 實例。
在關(guān)閉逃避分析的情況下(-XX:-DoEscapeAnalysis),雖然在alloc方法中創(chuàng)建的User對象并沒 有逃逸到方法外部,但是還是被分配在堆內(nèi)存中。也就說,如果沒有JIT編譯器優(yōu)化,沒有逃逸分 析技術(shù),正常情況下就應該是這樣的。即所有對象都分配到堆內(nèi)存中
接下來,我們開啟逃逸分析,再來執(zhí)行下以上代碼。
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError在程序打印出 cost XX ms 后,代碼運行結(jié)束之前,我們使用 jmap 命令,來查看下當前堆內(nèi)存中有多 少個User對象:
~ jps 709 2858 Launcher 2859 StackAllocTest 2860 Jps ~ jmap -histo 2859 num #instances #bytes class name --------------------------------------------- 1: 524 101944280 [I 2: 6806 2093136 [B 3: 83619 1337904 StackAllocTest$User 4: 8006 1320872 [C 5: 4188 100512 java.lang.String 6: 581 66304 java.lang.Class從以上打印結(jié)果中可以發(fā)現(xiàn),開啟了逃逸分析之后(-XX:+DoEscapeAnalysis),在堆內(nèi)存中只有 8萬多個 StackAllocTest$User 對象。也就是說在經(jīng)過JIT優(yōu)化之后,堆內(nèi)存中分配的對象數(shù)量, 從100萬降到了8萬。
除了以上通過jmap驗證對象個數(shù)的方法以外,還可以嘗試將堆內(nèi)存調(diào)小,然后執(zhí)行以上代碼,根 據(jù)GC的次數(shù)來分析,也能發(fā)現(xiàn),開啟了逃逸分析之后,在運行期間,GC次數(shù)會明顯減少。正是 因為很多堆上分配被優(yōu)化成了棧上分配,所以GC次數(shù)有了明顯的減少。
總結(jié)
所以,如果以后再有人問你:是不是所有的對象和數(shù)組都會在堆內(nèi)存分配空間?
那么你可以告訴他:不一定,隨著JIT編譯器的發(fā)展,在編譯期間,如果JIT經(jīng)過逃逸分析,發(fā)現(xiàn)有些對象 沒有逃逸出方法,那么有可能堆內(nèi)存分配會被優(yōu)化成棧內(nèi)存分配。但是這也并不是絕對的。就像我們前 面看到的一樣,在開啟逃逸分析之后,也并不是所有User對象都沒有在堆上分配。
4.5. 標量替換
標量(Scalar**)**是指一個無法再分解成更小的數(shù)據(jù)的數(shù)據(jù) 。
在JIT階段,如果經(jīng)過逃逸分析,發(fā)現(xiàn)一個對象不會被外界訪問的話,那么經(jīng)過JIT優(yōu)化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。
//有一個類A public class A{public int a=1;public int b=2 } //方法getAB使用類A里面的a,b private void getAB(){A x = new A();x.a;x.b; } //JVM在編譯的時候會直接編譯成 private void getAB(){ a = 1; b = 2; } //這就是標量替換4.6. 同步鎖消除
同樣基于逃逸分析,當加鎖的變量不會發(fā)生逃逸,是線程私有的完全沒有必要加鎖。在JIT編譯時期就 可以將同步鎖去掉,以減少加鎖與解鎖造成的資源開銷。
public class TestLockEliminate { public static String getString(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString(); }public static void main(String[] args) {long tsStart = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {getString("TestLockEliminate ", "Suffix");}System.out.println("一共耗費:" + (System.currentTimeMillis() - tsStart) + " ms");} }getString()方法中的StringBuffer數(shù)以函數(shù)內(nèi)部的局部變量,進作用于方法內(nèi)部,不可能逃逸出該 方法,因此他就不可能被多個線程同時訪問,也就沒有資源的競爭,但是StringBuffer的append 操作卻需要執(zhí)行同步操作,
StringBuffer中append方法的代碼如下:
@Override public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this; }逃逸分析和鎖消除分別可以使用參數(shù) -XX:+DoEscapeAnalysis 和 -XX:+EliminateLocks (鎖消除必須 在-server模式下)開啟。使用如下參數(shù)運行上面的程序:
-XX:+DoEscapeAnalysis -XX:-EliminateLocks得到如下結(jié)果:
一共耗費:244ms
使用如下命令運行程序:
-XX:+DoEscapeAnalysis -XX:+EliminateLocks一共耗費:220ms
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的JVM进阶之路, 不然又要被面试官吊打了的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JS-打点计时器
- 下一篇: C:输入数字计数(数组方法)