jvm(11)-晚期(运行期)优化
生活随笔
收集整理的這篇文章主要介紹了
jvm(11)-晚期(运行期)优化
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
【0】README
0.1)本文部分文字描述轉自 “深入理解 jvm”,旨在學習 ?晚期(運行期)優化 ?的基礎知識;
【1】概述
1)即時編譯器(JIT=just in time compiler)定義:為了提高熱點代碼的執行效率,在運行時,虛擬機將把這些代碼編譯成與 本地平臺相關的機器碼,并進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器; 2)熱點代碼:當虛擬機發現某個方法或代碼塊運行特別頻繁,就會把這些代碼認定為熱點代碼;(干貨——即時編譯器和熱點代碼的定義) Attention)本文提及的編譯器,即時編譯器都是指HotSpot 虛擬機內的即時編譯器,虛擬機也是特指HotSpot虛擬機;【2】 HotSpot 虛擬機內的即時編譯器 0)我們需要解決以下問題:
- problem1)為何HotSpot虛擬機要使用解釋器與編譯器并存的架構?
- problem2)為何HotSpot虛擬機要實現兩個不同的即時編譯器?
- problem3)程序何時使用解釋器執行?何時使用編譯器執行?
- problem4)哪些程序代碼會被編譯為本地代碼? 如何編譯為本地代碼?
- problem5)如何從外部觀察即時編譯器的編譯過程和編譯結果?
- 1.1)解釋器的優勢:當程序需要迅速啟動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即執行;
- 1.2)編譯器的優勢:在程序運行后,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲得更高的執行效率;
- 3.1)解釋模式:使用參數 -Xint 強制虛擬機運行在解釋模式, 這時編譯器完全不介入工作;
- 3.2)編譯模式:使用參數 -Xcomp?強制虛擬機運行在編譯模式, 這時將優先采用編譯方式執行程序,但解釋器仍然要求在編譯無法進行的情況下介入執行過程;
- 3.3)通過虛擬機的 -version 命令的輸出結果顯示出這3種模式:
- 4.1)第0層:程序解釋執行,解釋器不開啟性能監控功能,可觸發第1層編譯。
- 4.2)第1層:也成為C1編譯,將字節碼編譯為本地代碼,進行簡單、可靠的優化,如有必要時將加入性能監控的邏輯。
- 4.3)第2層(或2層以上):也成為C2編譯,也是將字節碼編譯為本地代碼,但是會啟動一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化。?
- 4.4)事實上:?用Client Compiler獲取更高的編譯速度,用Server Compiler來獲取更好的編譯質量,在解釋執行的時候也無須再承擔收集性能監控信息的任務。(干貨——Client Compiler 和?Server Compiler的用途)
- 1.1)被多次調用的方法;
- 1.2)被多次執行的循環體;
- 3.1)問題一:被多次調用,在這里的多次具體是多少次?
- 3.2)問題二:并且虛擬機如何統計一個方法或一段代碼被執行過多少次??
- 3.3)解決了以上問題,就回答了即時編譯器被觸發的條件;
- method1)基于采樣的熱點探測:虛擬機會周期性地檢查各種線程的棧頂,如果發現某個或者某些方法經常出現在棧頂,那這個方法就是“熱點方法”。?
- method2)基于計數器的熱點探測:虛擬機會為每個方法建立計數器,統計方法的執行次數,如果執行次數超過一定的闕值就認為它是熱點方法。?
- 5.1.1)定義:這個計數器就用于統計方法被調用的次數,他的默認閾值在 Client模式下是1500次,在 Server模式下是10000次,可以通過 虛擬機參數 -XX: CompileThreshold 來認為設定;
- Attention)JIT編譯的交互過程:
- A1)當一個方法被調用時,會先檢查該方法是否存在被JIT 編譯過的版本:如果存在,則優先使用編譯后的本地代碼來執行。如果不存在已被編譯過的版本,則將此方法的調用計數器值加1,然后判斷方法調用計數器與回邊計數器值之和是否超過方法計數器的閾值。若超過了,則將會向即時編譯器提交一個該方法的代碼編譯請求;
- A2)如果不做任何設置:執行引擎并不會同步等待編譯請求完成,而是繼續進入解釋器按照解釋方式執行字節碼,直到提交的請求被編譯器編譯完成。當編譯工作完成之后,這個方法的調用入口地址就會被系統自動改寫成新的,下一次調用該方法時就會使用已變異的版本,整個JIT 編譯的交互過程如下圖所示:
- 5.2.1)作用:是統計一個方法中循環體代碼執行的次數,在字節碼中遇到控制流向后跳轉的指令稱為”回邊“;顯然,建立回邊計數器統計的目的是為了觸發OSR 編譯(==棧上替換編譯);(干貨——回邊計數器的作用)
- 5.2.2)設置參數 -XX:OnStackReplacePercentage 來簡介調整回邊計數器的閾值,回邊計數器的閾值的計算公式如下:
- case1)虛擬機運行在Client模式下:閾值=方法調用計數器閾值*OSR比率/100;
- case2)虛擬機運行在Server模式下:閾值=方法調用計數器閾值*(OSR比率-解釋器監控比率)/100;
- 5.2.3)回邊計數器觸發即時編譯的過程:當解釋器遇到一條回邊指令時,會先查找將要執行的代碼片段是否有已經編譯好的version,如果有,他將會優先執行已編譯的代碼,否則就把回邊計數器的值加1,然后判斷方法調用計數器與回邊計數器之和是否超過回邊計數器的閾值。當超過閾值的時候,將會提交一個OSR編譯請求,并且把回邊計數器的值降低一些,以便繼續在解釋器中執行循環,等待編譯器輸出編譯結果,整個執行過程如下圖所示:
【2.3】編譯過程 1)無論是方法調用產生的即時編譯請求,還是OSR編譯請求:虛擬機在代碼編譯器還未完成之前,都將按照解釋方式繼續執行,而編譯動作則在后臺的編譯線程中進行; 2)在后臺執行編譯的過程中,編譯器做了什么事情呢? 3)對于Client Compiler來說,它是一個簡單快速的三段式編譯器;(干貨——Client 編譯器的編譯過程)
- step1)在第一個階段:一個平臺獨立的前端將字節碼構造成一種高級中間代表表示(High Level Intermediate Representation,HIR)。
- step2)在第二個階段:一個平臺相關的后端從 HIR中產生低級中間代碼表示(Low Level Intermediate Representation, LIR);
- step3)在第三個階段:最后是在平臺相關的后端使用線性掃描算法在LIR 上分配寄存器,并在LIR上做窺孔優化,然后產生機器代碼;
- Conclusion)整個 Client Compiler 的大致執行過程如下圖所示:
(此處有文字省略)
【3】編譯優化技術 0)java程序員有一個共識:以編譯方式執行本地代碼比解釋方式更快,之所以有這樣的共識,除去虛擬機解釋執行字節碼時額外消耗時間的原因外,還有一個重要的原因是 虛擬機設計團隊幾乎把對代碼的所有優化措施都集中在了即時編譯器中。所以編譯器產生的本地代碼會比Javac 產生的字節碼更加優秀;(干貨——編譯器產生的本地代碼會比Javac 產生的字節碼更加優秀)
【3.1】優化技術概覽 1)看個荔枝 1.0)源代碼
- step1)優化第一步,進行方法內聯:主要目的有兩個:一是去除方法調用的成本,二是為其他優化建立良好的基礎,方法內聯膨脹后可以便于在更大范圍上采取后續的優化手段;
- step2)優化第二步,進行冗余訪問消除:(看成是公共子表達式的消除)
- step3)復寫傳播: 因為在這段程序的邏輯中并沒有必要使用一個額外的變量z,它與變量y 是完全相等的,因此可以使用 y 來代替;
- step4)無用代碼消除:無用代碼可能是永遠不會被執行的代碼,也可能是完全沒有意義的代碼,因此,它又形象地稱為 Dead Code;
- C1)公共子表達式消除;
- C2)數組范圍檢查消除;
- C3)方法內聯;
- C4)逃逸分析;
【3.3】 數組邊界檢查 1)定義:在java語言中,訪問數組元素的時候,系統將會自動進行上下界的范圍檢查,即檢查i 必須滿足 大于等于0且小于數組長度;否則拋出一個運行時異常: java.lang.ArrayIndexOutOfBoundsException; 2)如果編譯器只要通過數據流分析就可以判定循環變量的取值范圍永遠在區間 [0,foo.length]內,那在整個循環中就可以吧數組的上下界檢查消除; 3)problem+solution
- problem)在安全檢查方面,java要比C 或C++做更多的事情,這些事情成為了一種隱式開銷,如果處理不好它們,就很可能成為一個 java語言比 C/C++ 更慢的因素;
- solution)要消除這些隱式開銷,其中之一還要避免思路——隱式異常處理,java中空指針檢查和算術運算中除數為零的檢查都采用了這種思路;(干貨——隱式開銷,隱式異常處理的概念);
- 2.1)無法內聯的原因:只有使用 invokespecial 指令調用的私有方法, 實例構造器,父類方法以及使用invokestaitc指令進行調用的靜態方法才是在編譯期進行解析的,除了上述4種方法外,其他的java方法調用都需要在運行時進行方法接收者的多態選擇,并且都有可能存在多于一個版本的方法接收者;簡而言之,java語言中默認的實例方法是虛方法;
- 2.2)對于一個虛方法:?編譯期做內聯的時候根本無法確定應該使用哪個方法版本;
- C1)非虛方法:只要能被invokestatic 和 invokespecial 指令調用的方法,都可以在解析階段中確定唯一的調用版本,符合這個條件的有靜態方法,私有方法,實例構造器,父類方法4類,他們在類加載的時候就會把符號引用解析為對該方法的直接引用。這些方法稱為非虛方法;
- C2)虛方法:其他的方法稱為虛方法(除去final方法);
對上圖的分析(Analysis):
- A1)CHA==類型繼承關系分析(Class Hierarchy Analysis, CHA):這是一種基于整個應用程序的類型分析技術,它用于確定在目前已經加載的類中,某個接口是否有多于一種的實現,某個類是否存在子類,子類是否為抽象類等信息;
- A2)方法內聯(Inline Cache):工作原理是,在未發生方法調用之前,內聯緩存狀態為空,當第一次調用發生后,緩存記錄下方法接收者的版本信息,并且每次進行方法調用時都比較接收者版本,如果以后進來的每次調用的方法接收者版本都是一樣的,那這個內聯還可以一直用下去。如果發生了方法接收者不一致的 case, 就說明程序真正使用了虛方法的多態特性,這時才會取消內聯,查找虛方法表進行方法分派;
- 1.1)方法逃逸:分析對象的動態作用域,當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調用參數傳遞給其他方法,稱為方法逃逸;
- 1.2)線程逃逸:甚至還有可能被外部線程訪問到,比如賦值給類變量或可以在其他線程中訪問到的實例變量,稱為線程逃逸;
- 2.1)棧上分配:如果確定一個對象不會逃逸出方法之外,那讓這個對象在棧上分配內存將是一個不錯的主意,對象所占用的內存空間就可以隨棧幀出棧而銷毀;
- 2.2)同步消除:線程同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變量不會逃逸出線程,那這個變量的讀寫肯定不會有競爭,對這個變量實施的同步措施就可以消除掉;
- 2.3)標量替換
- 2.3.1)標量定義:它是指一個數據已經無法再分解成更小的數據來表示了,java虛擬機中的原始數據類型都不能再進一步分解,他們就可以成為標量;
- 2.3.2)聚合量定義:如果一個數據可以繼續分解,它被稱為聚合量;
- 2.3.3)標量替換:如果把一個java對象拆散,根據程序訪問的情況,將其使用到的成員變量恢復原始類型來訪問就叫做標量替換;那程序真正執行的時候,將可能不創建這個對象,而改為直接創建它的若干個被這個方法使用到的成員變量來代替。
- Attention)
- A1)逃逸分析技術還不成熟,不推薦使用;
- A2)不成熟的原因:無法保證逃逸分析的性能收益必定高于它的消耗;
- 3.1)用戶使用參數 -XX: +DoEscapeAnalysis 來手動開啟逃逸分析;
- 3.2)開啟之后可以通過參數 -XX: +PrintEscapeAnalysis 來查看分析結果;
- 3.3)有了逃逸分析支持后,用戶可以使用參數 -XX: +EliminateAllocations 來開啟標量替換:
- 3.4)使用 +XX:+EliminateLocks 來開啟同步消除;
- 3.5)使用參數 -XX:+PrintElimianteAllocations 查看標量的替換情況;
- Attention)盡管目前逃逸分析的技術不是很成熟,但它卻是即時編譯器優化技術的一個重要發展方向,在今后的虛擬機中,逃逸分析技術肯定會支撐起一些列實用有效的優化技術
- (干貨——逃逸分析技術前途無量)
- C1)因為即時編譯器運行占用的是用戶程序的運行時間:具有很大的時間壓力,它能提供的優化手段嚴重受制于編譯成本;?
- C2)java語言是動態的類型安全語言:這就意味著需要由虛擬機來確保程序不會違反語言語義后訪問非結構化內存。從實現層面上來看,這就意味著虛擬機必須頻繁地進行動態檢查,如實例方法訪問時檢查空指針,數組元素訪問時檢查上下界訪問,類型轉換時檢查基礎關系等。總體上仍然要消耗不少的時間;
- C3)java語言中沒有virtual關鍵字,但使用虛方法的頻率卻遠遠大于 C 或 C++:這意味著運行時對方法接收者進行多態選擇的頻率要遠遠大于C或C++,也意味著 即時編譯器在進行一些優化時的難度要遠遠大于C 或 C++ 的靜態優化編譯器;
- C4)java語言是可以動態擴展的語言,運行時加載新的類可能改變程序類型的繼承關系: 這使得很多全局的優化都難以進行,因為編譯器無法看見程序的全貌,許多全局的優化措施都只能以激進優化的方式來完成,編譯器不得不時刻注意并隨著類型的變化而在運行時撤銷或重新進行一些優化;
- C5)java語言中對象的內存分配都是在堆上進行的:只有方法中的局部變量才能在棧上分配;而C 或 C++的對象則有多種內存分配方式,既可能在堆上分配,也可能在棧上分配,如果可以在棧上分配線程私有的對象,將減輕內存回收的壓力;另外,C或C++中主要由用戶程序代碼來回收分配的內存,這就不存在無用對象篩選的過程,因此在效率上,也比垃圾收集機制要高;
- C1)java 語言上的這些性能上的劣勢都是為了換取開發效率上的優勢而付出的代價;動態安全, 動態擴展, 垃圾回收這些拖后腿的特性都為 java 的開發效率做出了貢獻;
- C2)許多優化時 Java的即時編譯器能做而C 或 C++的靜態優化編譯器不能做或做的不好的;
- C3)java編譯器另外一個紅利是由它的動態性帶來的: 由于C或C++編譯器所有優化都在編譯器完成,以運行期性能監控為基礎的優化措施它都無法進行;
總結
以上是生活随笔為你收集整理的jvm(11)-晚期(运行期)优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 备案百度百科(指定备案)
- 下一篇: ddos高防ip原理(ddos防ip原理