Java常見面試題匯總-----------JVM專題(JVM編譯器優(yōu)化、JVM逃逸分析)

32、JVM編譯器優(yōu)化

32.1、JVM編譯的過程

??1、解析與填充符號表過程
??1)、詞法、語法分析
??詞法分析將源代碼的字符流轉(zhuǎn)變?yōu)闃擞浖?,單個字符是程序編寫過程的最小元素,而標記則是編譯過程的最小元素,javac中由com.sun.tools.javac.parser.Scanner類實現(xiàn)。
語法分析是根據(jù)token序列構(gòu)造抽象語法樹的過程。抽象語法樹(AST)是一種用來描述程序代碼語法結(jié)構(gòu)的樹形表示方式,語法樹中的每一個節(jié)點都代表著程序代碼中的語法結(jié)構(gòu),javac中,語法分析過程由com.sun.tools.javac.tree.parser.Parser類實現(xiàn),這個階段產(chǎn)生出的抽象語法樹由com.sun.tools.javac.tree.JCTree類表示。
??2)、填充符號表
??enterTree()方法,符號表是由一組符號地址和符號信息構(gòu)成的表格,符號表中登記的信息在編譯的不同階段都要用到。在語義分析中,符號表所登記的內(nèi)容將用于語義檢查和產(chǎn)生中間代碼,在目標代碼生成階段,當對符號進行地址分配時,符號表是地址分配的依據(jù)。javac源碼中由com.sun.tools.javac.comp.Enter類實現(xiàn)。

??2、插入式注解處理器的注解處理過程
??注解在運行期間發(fā)揮作用,通過插入式注解處理器標準API可以讀取、修改、添加抽象語法樹種的任意元素,若在處理注解期間對語法樹進行修改,編譯器將回到解析即填充符號表的過程重新處理,直到所有插入式注解處理器都沒有再對語法樹進行修改為止,每一次循環(huán)稱為一個round。javac源碼中插入式注解處理器的初始化過程是在initProrcessAnnotation()方法中完成的,而它的執(zhí)行過程則是在processAnnotation()方法中完成。

??3、分析與字節(jié)碼生成過程
??1)、標注檢查
??attribute()方法,標注檢查步驟檢查的內(nèi)容包括諸如變量使用前是否已經(jīng)被聲明、變量與賦值之間的數(shù)據(jù)類型是否夠匹配以及常量折疊。javac中實現(xiàn)類是com.sun.tools.javac.comp.Attr類和com.sun.tools.javac.comp.Check類。
??2)、數(shù)據(jù)及控制流分析
??flow()方法,對程序上下文邏輯更進一步的驗證,他可以檢查出諸如程序局部變量在使用前是否賦值、方法的每條路徑是否都有返回值、是否所有的受檢查異常都被正確處理了問題。
??局部變量在常量池中沒有CONSTANT_Fieldref_info的符號引用,自然沒有訪問標志的信息,甚至可能連名稱都不會保存下來。
將局部變量聲明為final,對運行期是沒有影響的,變量的不變性僅僅由編譯器在編譯期間保障。
??3)、解語法糖
??也稱糖衣語法,指在計算機中添加某種語法,這種語法對語言的功能沒有影響,但是更方便程序員使用,通常來說,使用語法糖能夠增加程序的可讀性,從而減少程序代碼出錯的機會。java中最常用的是泛型、變長參數(shù)、自動裝箱/拆箱等。
??4)、字節(jié)碼生成
??javac編譯的最后一個階段,javac源碼里面由com.sun.tools.javac.jvm.Gen類來完成,這個階段不僅僅把前面各個步驟所生成的信息轉(zhuǎn)化成字節(jié)碼寫到磁盤中,編譯器還進行少量的代碼添加轉(zhuǎn)換工作。
??保證一定是按先執(zhí)行父類的實例構(gòu)造器,然后初始化變量,最后執(zhí)行語句塊的順序進行。

32.2、javac語法糖

??1、泛型與類擦除
??java中的泛型它只在程序源碼中存在,在編譯后的字節(jié)碼文件中,就已經(jīng)替換為原來的原生類型,并在相應的地方插入了強制轉(zhuǎn)換代碼,對于運行期的java來說ArrayList<T>與ArrayList就是同一個類,java語言中的泛型實現(xiàn)方法稱為類型擦除,基于這種方法的叫偽泛型。
??在Class文件格式中,只要描述符不是完全一致的兩個方法就可以共存。
??Signature是解決伴隨泛型而來的參數(shù)類型的識別問題中最重要的一項屬性,它的作用就是存儲一個方法在字節(jié)碼層面的特征簽名,這個屬性中保存的參數(shù)類型并不是原生類型,而是包括了參數(shù)化類型的信息、擦除法所謂的擦除,僅僅是對方法的Code屬性中的字節(jié)碼進行擦除,實際上元數(shù)據(jù)中還是保留了泛型信息,這也是我們能夠通過反射手段取得參數(shù)化類型的根本依據(jù)。

??2、自動裝箱、拆箱與循環(huán)遍歷

??3、條件編譯
??java編譯器并非一個個地編譯Java文件,而是將所有編譯單元的語法樹頂級節(jié)點輸入到待處理列表后再進行編譯,因此各個文之間能夠互相提供符號信息。
??java中根據(jù)布爾常量值的真假,編譯器會把分支中不成立的代碼塊擦除掉。這一工作將在編譯器解除語法糖階段完成。

??4、常用語法糖
??泛型、自動裝箱、自動拆箱、遍歷循環(huán)、變長參數(shù)、條件編譯、內(nèi)部類、枚舉類、斷言語句、對枚舉、字符串的switch,try與catch定義和關(guān)閉資源。

32.3、運行期JIT編譯器

??java程序最初是通過解釋器進行解釋執(zhí)行的,當虛擬機發(fā)現(xiàn)某個方法或者代碼塊的運行特別頻繁時,就好把這些代碼認定為“熱點代碼”,為了提高熱點代碼的執(zhí)行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關(guān)的機器碼,并進行各種層次的優(yōu)化,完成這個任務的編譯器稱為即時編譯器(JIT編譯器),他是虛擬機中最核心且最能體現(xiàn)虛擬機水平的部分。
??JIT 是 just in time 的縮寫,也就是即時編譯器。使用即時編譯器技術(shù),能夠加速Java 程序的執(zhí)行速度。
??首先,我們大家都知道,通常通過 javac 將程序源代碼編譯,轉(zhuǎn)換成 java 字節(jié)碼,JVM 通過解釋字節(jié)碼將其翻譯成對應的機器指令,逐條讀入,逐條解釋翻譯。很顯然,經(jīng)過解釋執(zhí)行,其執(zhí)行速度必然會比可執(zhí)行的二進制字節(jié)碼程序慢很多。為了提高執(zhí)行速度,引入了 JIT 技術(shù)。
??在運行時 JIT 會把翻譯過的機器碼保存起來,以備下次使用,因此從理論上來說,采用該 JIT 技術(shù)可以接近以前純編譯技術(shù)。下面我們看看,JIT 的工作過程。

32.3.1、JIT 編譯過程

??當 JIT 編譯啟用時(默認是啟用的),JVM 讀入.class 文件解釋后,將其發(fā)給 JIT 編譯器。JIT 編譯器將字節(jié)碼編譯成本機機器代碼,下圖展示了該過程。



32.3.2、Hot Spot 編譯

??當 JVM 執(zhí)行代碼時,它并不立即開始編譯代碼。這主要有兩個原因:
??首先,如果這段代碼本身在將來只會被執(zhí)行一次,那么從本質(zhì)上看,編譯就是在浪費精力。 因為將代碼翻譯成 java 字節(jié)碼相對于編譯這段代碼并執(zhí)行代碼來說,要快很多。
??當然,如果一段代碼頻繁的調(diào)用方法,或是一個循環(huán),也就是這段代碼被多次執(zhí)行,那么編譯就非常值得了。因此,編譯器具有的這種權(quán)衡能力會首先執(zhí)行解釋后的代碼,然后再去分辨哪些方法會被頻繁調(diào)用來保證其本身的編譯。其實說簡單點,就是 JIT 在起作用,我們知道,對于 Java 代碼,剛開始都是被編譯器編譯成字節(jié)碼文件,然后字節(jié)碼文件會被交由 JVM 解釋執(zhí)行,所以可以說 Java 本身是一種半編譯半解釋執(zhí)行的語言。Hot Spot VM 采用了 JIT compile 技術(shù),將運行頻率很高的字節(jié)碼直接編譯為機器指令執(zhí)行以提高性能,所以當字節(jié)碼被 JIT 編譯為機器碼的時候,要說它是編譯執(zhí)行的也可以。也就是說,運行時,部分代碼可能由 JIT 翻譯為目標機器指令(以 method 為翻譯單位,還會保存起來,第二次執(zhí)行就不用翻譯了)直接執(zhí)行。
??第二個原因是最優(yōu)化,當 JVM 執(zhí)行某一方法或遍歷循環(huán)的次數(shù)越多,就會更加了解代碼結(jié)構(gòu),那么 JVM 在編譯代碼的時候就做出相應的優(yōu)化。
??我們將在后面講解這些優(yōu)化策略,這里,先舉一個簡單的例子:我們知道 equals() 這個方法存在于每一個 Java Object 中(因為是從 Object class 繼承而來)而且經(jīng)常被覆寫。當解釋器遇到 b = obj1.equals(obj2) 這樣一句代碼,它則會查詢 obj1 的類型從而得知到底運行哪一個 equals() 方法。而這個動態(tài)查詢的過程從某種程度上說是很耗時的。
??例如JVM 注意到每次運行代碼時,obj1 都是 java.lang.String 這種類型,那么JVM 生成的被編譯后的代碼則是直接調(diào)用 String.equals() 方法。這樣代碼的執(zhí)行將變得非???,因為不僅它是被編譯過的,而且它會跳過查找該調(diào)用哪個方法的步驟。
??當然過程并不是上面所述這樣簡單,如果下次執(zhí)行代碼時,obj1 不再是 String 類型了,JVM 將不得不再生成新的字節(jié)碼。盡管如此,之后執(zhí)行的過程中,還是會變的更快,因為同樣會跳過查找該調(diào)用哪個方法的步驟。這種優(yōu)化只會在代碼被運行和觀察一段時間之后發(fā)生。這也就是為什么 JIT 編譯器不會直接編譯代碼而是選擇等待然后再去編譯某些代碼片段的第二個原因。

32.3.3、寄存器和主存

??其中一個最重要的優(yōu)化策略是編譯器可以決定何時從主存取值,何時向寄存器存值。考慮下面這段代碼:
??清單 1、主存 or 寄存器測試代碼

public class RegisterTest {
private int sum;

public void calculateSum(int n) {
        for (int i = 0; i < n; ++i) {
            sum += i;
        }
    }
}

??在某些時刻,sum 變量居于主存之中,但是從主存中檢索值是開銷很大的操作,需要多次循環(huán)才可以完成操作。正如上面的例子,如果循環(huán)的每一次都是從主存取值,性能是非常低的。相反,編譯器加載一個寄存器給 sum 并賦予其初始值,利用寄存器里的值來執(zhí)行循環(huán),并將最終的結(jié)果從寄存器返回給主存。這樣的優(yōu)化策略則是非常高效的。但是線程的同步對于這種操作來說是至關(guān)重要的,因為一個線程無法得知另一個線程所使用的寄存器里變量的值,線程同步可以很好的解決這一問題。
??寄存器的使用是編譯器的一個非常普遍的優(yōu)化。

32.3.4、初級調(diào)優(yōu):客戶模式或服務器模式

??JIT 編譯器在運行程序時有兩種編譯模式可以選擇,并且其會在運行時決定使用哪一種以達到最優(yōu)性能。這兩種編譯模式的命名源自于命令行參數(shù)(eg: -client 或者-server)。JVM Server 模式與 client 模式啟動,最主要的差別在于:-server 模式啟動時,速度較慢,但是一旦運行起來后,性能將會有很大的提升。原因是:當虛擬機運行在-client 模式的時候,使用的是一個代號為 C1 的輕量級編譯器,而-server 模式啟動的虛擬機采用相對重量級代號為 C2 的編譯器。C2 比 C1 編譯器編譯的相對徹底,服務起來之后,性能更高。
??通過 java -version 命令行可以直接查看當前系統(tǒng)使用的是 client 還是 server 模式。例如:


32.3.5、中級編譯器調(diào)優(yōu)

??大多數(shù)情況下,優(yōu)化編譯器其實只是選擇合適的 JVM 以及為目標主機選擇合適的編譯器(-cient,-server 或是-xx:+TieredCompilation)。多層編譯經(jīng)常是長時運行應用程序的最佳選擇,短暫應用程序則選擇毫秒級性能的 client 編譯器。
??1)、優(yōu)化代碼緩存
??當 JVM 編譯代碼時,它會將匯編指令集保存在代碼緩存。代碼緩存具有固定的大小,并且一旦它被填滿,JVM 則不能再編譯更多的代碼。

??我們可以很容易地看到如果代碼緩存很小所具有的潛在問題。有些熱點代碼將會被編譯,而其他的則不會被編譯,這個應用程序?qū)赃\行大量的解釋代碼來結(jié)束。
??這是當使用 client 編譯器模式或分層編譯時很頻繁的一個問題。當使用普通server編譯器模式時,編譯合格的類的數(shù)量將被填入代碼緩存,通常只有少量的類會被編譯。但是當使用 client 編譯器模式時,編譯合格的類的數(shù)量將會高很多。
??在 Java 7 版本,分層編譯默認的代碼緩存大小經(jīng)常是不夠的,需要經(jīng)常提高代碼緩存大小。大型項目若使用 client 編譯器模式,則也需要提高代碼緩存大小。
??現(xiàn)在并沒有一個好的機制可以確定一個特定的應用到底需要多大的代碼緩存。因此,當需要提高代碼緩存時,這將是一種湊巧的操作,一個通常的做法是將代碼緩存變成默認大小的兩倍或四倍。
??可以通過 –XX:ReservedCodeCacheSize=Nflag(N 就是之前提到的默認大小)來最大化代碼緩存大小。代碼緩存的管理類似于 JVM 中的內(nèi)存管理:有一個初始大小(用-XX:InitialCodeCacheSize=N 來聲明)。代碼緩存的大小從初始大小開始,隨著緩存被填滿而逐漸擴大。代碼緩存的初始大小是基于芯片架構(gòu)(例如 Intel 系列機器,client 編譯器模式下代碼緩存大小起始于 160KB,server 編譯器模式下代碼緩存大小則起始于 2496KB)以及使用的編譯器的。重定義代碼緩存的大小并不會真正影響性能,所以設置 ReservedCodeCacheSize 的大小一般是必要的。
??再者,如果 JVM 是 32 位的,那么運行過程大小不能超過 4GB。這包括了 Java 堆,JVM 自身所有的代碼空間(包括其本身的庫和線程棧),應用程序分配的任何的本地內(nèi)存,當然還有代碼緩存。
??所以說代碼緩存并不是無限的,很多時候需要為大型應用程序來調(diào)優(yōu)(或者甚至是使用分層編譯的中型應用程序)。比如 64 位機器,為代碼緩存設置一個很大的值并不會對應用程序本身造成影響,應用程序并不會內(nèi)存溢出,這些額外的內(nèi)存預定一般都是被操作系統(tǒng)所接受的。

??2)、編譯閾值
??在 JVM 中,編譯是基于兩個計數(shù)器的:一個是方法被調(diào)用的次數(shù),另一個是方法中循環(huán)被回彈執(zhí)行的次數(shù)。
回彈可以有效的被認為是循環(huán)被執(zhí)行完成的次數(shù),不僅因為它是循環(huán)的結(jié)尾,也可能是因為它執(zhí)行到了一個分支語句,例如 continue。
??當 JVM 執(zhí)行一個 Java 方法,它會檢查這兩個計數(shù)器的總和以決定這個方法是否有資格被編譯。如果有,則這個方法將排隊等待編譯。這種編譯形式并沒有一個官方的名字,但是一般被叫做標準編譯。
??但是如果方法里有一個很長的循環(huán)或者是一個永遠都不會退出并提供了所有邏輯的程序會怎么樣呢?這種情況下,JVM 需要編譯循環(huán)而并不等待方法被調(diào)用。所以每執(zhí)行完一次循環(huán),分支計數(shù)器都會自增和自檢。如果分支計數(shù)器計數(shù)超出其自身閾值,那么這個循環(huán)(并不是整個方法)將具有被編譯資格。
??這種編譯叫做棧上替換(OSR),因為即使循環(huán)被編譯了,這也是不夠的:JVM 必須有能力當循環(huán)正在運行時,開始執(zhí)行此循環(huán)已被編譯的版本。換句話說,當循環(huán)的代碼被編譯完成,若 JVM 替換了代碼(前棧),那么循環(huán)的下個迭代執(zhí)行最新的被編譯版本則會更加快。
??標準編譯是被-XX:CompileThreshold=Nflag 的值所觸發(fā)。Client 編譯器模式下,N 默認的值 1500,而 Server 編譯器模式下,N 默認的值則是 10000。改變 CompileThreshold 標志的值將會使編譯器相對正常情況下提前(或推遲)編譯代碼。在性能領(lǐng)域,改變 CompileThreshold 標志是很被推薦且流行的方法。事實上,您可能知道 Java 基準經(jīng)常使用此標志(比如:對于很多 server 編譯器來說,經(jīng)常在經(jīng)過 8000 次迭代后改變此標志)。
??我們已經(jīng)知道 client 編譯器和 server 編譯器在最終的性能上有很大的差別,很大程度上是因為編譯器在編譯一個特定的方法時,對于兩種編譯器可用的信息并不一樣。降低編譯閾值,尤其是對于 server 編譯器,承擔著不能使應用程序運行達到最佳性能的風險,但是經(jīng)過測試應用程序我們也發(fā)現(xiàn),將閾值從 8000 變成 10000,其實有著非常小的區(qū)別和影響。

??3)、檢查編譯過程
??中級優(yōu)化的最后一點其實并不是優(yōu)化本身,而是它們并不能提高應用程序的性能。它們是 JVM(以及其他工具)的各個標志,并可以給出編譯工作的可見性。它們中最重要的就是--XX:+PrintCompilation(默認狀態(tài)下是 false)。
??如果 PrintCompilation 被啟用,每次一個方法(或循環(huán))被編譯,JVM 都會打印出剛剛編譯過的相關(guān)信息。不同的 Java 版本輸出形式不一樣,我們這里所說的是基于 Java 7 版本的。
??編譯日志中大部分的行信息都是下面的形式:
??清單 2. 日志形式

timestamp compilation_id attributes (tiered_level) method_name size depot

??這里 timestamp 是編譯完成時的時間戳,compilation_id 是一個內(nèi)部的任務 ID,且通常情況下這個數(shù)字是單調(diào)遞增的,但有時候?qū)τ?server 編譯器(或任何增加編譯閾值的時候),您可能會看到失序的編譯 ID。這表明編譯線程之間有些快有些慢,但請不要隨意推斷認為是某個編譯器任務莫名其妙的非常慢。

??4)、用 jstat 命令檢查編譯
??要想看到編譯日志,則需要程序以-XX:+PrintCompilation flag 啟動。如果程序啟動時沒有 flag,您可以通過 jstat 命令得到有限的可見性信息。
??Jstat 有兩個選項可以提供編譯器信息。其中,-compile 選項提供總共有多少方法被編譯的總結(jié)信息(下面 6006 是要被檢查的程序的進程 ID):
??清單 3 進程詳情

% jstat -compiler 6006
CompiledFailedInvalid TimeFailedTypeFailedMethod
206 0 0 1.97 0

??注意,這里也列出了編譯失敗的方法的個數(shù)信息,以及編譯失敗的最后一個方法的名稱。
??另一種選擇,您可以使用-printcompilation 選項得到最后一個被編譯的方法的編譯信息。因為 jstat 命令有一個參數(shù)選項用來重復其操作,您可以觀察每一次方法被編譯的情況。舉個例子:
??Jstat 對 6006 號 ID 進程每 1000 毫秒執(zhí)行一次: %jstat –printcompilation 6006 1000,具體的輸出信息在此不再描述。

32.3.6、高級編譯器調(diào)優(yōu)

??這一節(jié)我們將介紹編譯工作剩下的細節(jié),并且過程中我們會探討一些額外的調(diào)優(yōu)策略。調(diào)優(yōu)的存在很大程度上幫助了 JVM 工程師診斷 JVM 自身的行為。如果您對編譯器的工作原理很感興趣,這一節(jié)您一定會喜歡。
??1)、編譯線程
??從前文中我們知道,當一個方法(或循環(huán))擁有編譯資格時,它就會排隊并等待編譯。這個隊列是由一個或很多個后臺線程組成,這也就是說編譯是一個異步的過程,它允許程序在代碼正在編譯時被繼續(xù)執(zhí)行。如果一個方法被標準編譯方式所編譯,那么下一個方法調(diào)用則會執(zhí)行已編譯的方法。如果一個循環(huán)被棧上替換方式所編譯,那么下一次循環(huán)迭代則會執(zhí)行新編譯的代碼。
??這些隊列并不會嚴格的遵守先進先出原則:哪一個方法的調(diào)用計數(shù)器計數(shù)更高,哪一個就擁有優(yōu)先權(quán)。所以即使當一個程序開始執(zhí)行,并且有大量的代碼需要編譯,這個優(yōu)先權(quán)順序?qū)椭⒈WC最重要的代碼被優(yōu)先編譯(這也是為什么編譯 ID 在 PrintComilation 的輸出結(jié)果中有時會失序的另一個原因)。
??當使用 client 編譯器時,JVM 啟動一個編譯線程,而 server 編譯器有兩個這樣的線程。當分層編譯生效時,JVM 會基于某些復雜方程式默認啟動多個 client 和 server 線程,涉及雙日志在目標平臺上的 CPU 數(shù)量。如下圖所示:
??分層編譯下 C1 和 C2 編譯器線程默認數(shù)量:

??編譯器線程的數(shù)量可以通過-XX:CICompilerCount=N flag 進行調(diào)節(jié)設置。這個數(shù)量是 JVM 將要執(zhí)行隊列所用的線程總數(shù)。對于分層編譯,三分之一的(至少一個)線程被用于執(zhí)行 client 編譯器隊列,剩下的(也是至少一個)被用來執(zhí)行 server 編譯器隊列。
??在何時我們應該考慮調(diào)整這個值呢?如果一個程序被運行在單 CPU 機器上,那么只有一個編譯線程會更好一些:因為對于某個線程來說,其對 CPU 的使用是有限的,并且在很多情況下越少的線程競爭資源會使其運行性能更高。然而,這個優(yōu)勢僅僅局限于初始預熱階段,之后,這些具有編譯資格的方法并不會真的引起 CPU 爭用。當一個股票批處理應用程序運行在單 CPU 機器上并且編譯器線程被限制成只有一個,那么最初的計算過程將比一般情況下快 10%(因為它沒有被其他線程進行 CPU 爭用)。迭代運行的次數(shù)越多,最初的性能收益就相對越少,直到所有的熱點方法被編譯完性能收益也隨之終止。



33、JVM逃逸分析

??逃逸分析(Escape Analysis)是目前Java虛擬機中比較前沿的優(yōu)化技術(shù)。
??逃逸分析的基本行為就是分析對象動態(tài)作用域:當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他地方中,稱為方法逃逸。例如:

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不逃出方法,可以這樣寫:return sb.toString();
??不直接返回 StringBuffer,那么StringBuffer將不會逃逸出方法。
??如果能證明一個對象不會逃逸到方法或線程外,則可能為這個變量進行一些高效的優(yōu)化。

33.1、棧上分配

??我們都知道Java中的對象都是在堆上分配的,而垃圾回收機制會回收堆中不再使用的對象,但是篩選可回收對象,回收對象還有整理內(nèi)存都需要消耗時間。如果能夠通過逃逸分析確定某些對象不會逃出方法之外,那就可以讓這個對象在棧上分配內(nèi)存,這樣該對象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀,就減輕了垃圾回收的壓力。
??在一般應用中,如果不會逃逸的局部對象所占的比例很大,如果能使用棧上分配,那大量的對象就會隨著方法的結(jié)束而自動銷毀了。

33.2、標量替換

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

33.3、總結(jié)

??雖然概念上的JVM總是在Java堆上為對象分配空間,但并不是說完全依照概念的描述去實現(xiàn);只要最后實現(xiàn)處理的“可見效果”與概念中描述的一致就沒問題了。所以說,“you can cheat as long as you don’t get caught”。Java對象在實際的JVM實現(xiàn)中可能在GC堆上分配空間,也可能在棧上分配空間,也可能完全就消失了。這種行為從Java源碼中看不出來,也無法顯式指定,只是聰明的JVM自動做的優(yōu)化而已。
??但是逃逸分析會有時間消耗,所以性能未必提升多少,并且由于逃逸分析比較耗時,目前的實現(xiàn)都是采用不那么準確但是時間壓力相對較小的算法來完成逃逸分析,這就可能導致效果不穩(wěn)定,要慎用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內(nèi)容