是否是不少人的印象中,Java要比C++運行的慢?若是如今還停留在這個想法,那或許該更新下想法了,由於Java近幾年在運行優化方面作了很是多的研究和改進,能夠說已經基本不怎麼輸於C++的運行速度了。前端
咱們參照HotSpot虛擬機的優化來講明,不一樣虛擬機確定是不一樣的,可是也有參考價值。java
個人全部文章同步更新與Github--Java-Notes,想了解JVM,HashMap源碼分析,spring相關,劍指offer題解(Java版),能夠點個star。能夠看個人github主頁,天天都在更新喲。git
邀請您跟我一同完成 repo程序員
所謂無風不起浪,**爲啥你們以爲Java的執行速度要比C++慢呢?**如今又怎麼說兩者如今的執行速度差異不太大了呢?github
由於早期的Java代碼主要都是由解釋器來完成,而且即便用到即時編譯器,即時編譯器的性能優化也作得不是太好,因此早期纔有了上面說的"Java比C++執行慢",因此才說那是早期的思想。,如今Java優化作的很是好,咱們就挨個說下吧算法
如今主流的(不是所有)虛擬機都採用解釋器和編譯器共存的架構,兩者互相配合工做,以下圖所示spring
兩者互有優缺點,優缺點正好相對。後端
因此啊,虛擬機纔會配合兩者進行使用。那些"熱點代碼"(這個概念後面會提到)會被即時編譯器編譯成本地代碼,以此來提升執行速度,"非熱點代碼"就用解釋器來執行。爲啥這樣呢?可能符合"二八定理"吧。20%的代碼是熱點代碼,可是他們卻可能佔用80%的執行資源。數組
咱們看到上面的圖片中,編譯器有兩種,一個是客戶端的,一個是服務端的,前者被稱爲C1編譯器,後者稱爲C2編譯器,兩個的參數設置也不同。優化手段也不太同樣,後面會講到。緩存
咱們以前提到,即時編譯器只編譯那些"熱點代碼",那麼啥是熱點代碼呢?只有如下兩類
前者比較好理解,後者有點很差理解。若是一個方法中,存在循環屢次的循環體,那麼這個循環體的代碼也被執行了不少次,因此也認爲是"熱點代碼"。可是雖然是因爲循環體形成他是熱點代碼的,可是編譯器編譯的時候,是根據整個方法進行編譯(而不是隻編譯循環體)。這種編譯方式因爲編譯發生在方法執行過程當中,所以被形象的稱爲棧上替換(On Stack Replacement,簡稱OSR)。前者也是以整個方法做爲編譯對象。
那麼啥是度呢?多少次才叫屢次?纔會觸發即時編譯條件呢?
判斷一段代碼是否是熱點代碼,是否是須要觸發即時編譯,咱們稱之爲熱點探測,主流的熱點探測斷定方式有兩種:
基於採樣的熱點探測:採用這種方法的虛擬機會週期性的檢查各個線程的棧頂,若是發現某個(或者某些)方法常常出如今棧頂,那這個方法就是"熱點方法"。(有沒有像可達性分析法搜索可達路徑?)
基於計數器的熱點探測:爲每個方法(甚至是代碼塊)創建一個計數器,統計是否超過閾值。還記得兩種熱點代碼嗎?他專門準備了兩種相對的計數器。前者是方法調用計數器,後者是回邊計數器
兩個方法優劣對比:
基於採樣
基於計數器
咱們先看圖
當一個方法執行時,
這裏的方法調用計數器裏面的值不是絕對值,而是一個相對的執行頻率,即一段時間以內方法被調用的次數。超過必定時間,調用次數仍然未達到閾值,那麼方法調用計數器的值就會減小一半,這個過程稱爲方法調用計數器熱度的衰減。這個像不像原子的衰變?而後這個時間,他們就取名爲半衰週期。可是這個是能夠關掉的,相關參數設置在參數那段講
啥是回邊呢?在字節碼中遇到控制流向後跳轉的指令,被稱爲"回邊",這個計數器也是爲了觸發OSR編譯(這個概念,上文講過)
執行步驟和方法調用計數器類似
不一樣的是,這個計數器沒有所謂的半衰
-client/-server
:指定JVM運行哪一種模式-Xint
:強制JVM使用解釋模式,編譯器不工做-Xcomp
:和上面不一樣的是,他只是優先編譯器工做,當編譯沒法進行的狀況下,解釋器仍是會介入-XX: -UseCounterDecay
:關閉熱度衰減-XX: CounterHalfLifeTime
:設置半衰期時長,單位秒-XX: CompileThreshold
:設置方法調用閾值-XX: BackEdgeThreshold
:設置回邊計數器閾值-XX: -BackgroundCompilation
:禁止後臺編譯,達到閾值,等到編譯完再往下執行,且執行編譯好的代碼一行長度爲32bit
後臺編譯過程,Client Compiler (C1)和Server Compiler(C2)的工做是不同的,後者更爲複雜,是全局優化,前者主要是局部性的優化
共分爲三個階段:
一共有很是多的技術
不過最具備表明性的是如下四個:
若是一個表達式E已經計算過了,而且先前的計算到如今E中全部變量的值都沒有發生變化,那麼E的此次出現就成爲了公共子表達式
若是有一下程序代碼
int d = (c * b) * 12 + a + (a + b *c);
當這段代碼進入虛擬機即時編譯器後,他將進行以下優化:
int d = E * 12 + a + (a+E)
int d =E * 13 + a *2
咱們知道,相同的代碼,Java要比C++執行的慢,這也是人們會有一開始的那種想法的緣由之一。那麼爲何呢?
Java程序因爲有虛擬機,不少東西都會替你來作,好比越界檢查,除零檢查,垃圾回收這種。
好比數組 nums[]
,Java中若是你要訪問其中一個元素,你的下標必定是在[0,num.length-1]的,若是超出了,他就會報錯java.lang.ArrayIndexOutOfBoundsException
.可是C或者C++,使用的是裸指針,所以這種判斷須要程序員本身操做。
Java中,若是你要對數組進行讀寫操做,那麼就必定要檢查他的範圍是否超出,這是由JVM進行,可是大量的執行步驟中,都會帶有這個,哪怕你知道不可能超出,可是JVM可不知道。
因此能不能告訴他,"咱們的必定不會超出,你別再檢查了呢?"
事實上,不行,,由於數組越界檢查是必須作的。可是能夠跟他談判,讓他減小次數,提早到編譯期檢查一次
好比咱們有下面的程序 nums[3]
,只要在編譯期間根據數據流分析來肯定nums.length
的值,而且判斷下標 3並無越界,那麼執行的時候就不必判斷了。
用於:
若是有下列代碼
if(num != null){
return num.getVal();
}else {
throw new NullPointerException();
}
複製代碼
使用隱式異常優化後
try{
return num.val;
}catch(segment_fault){
uncommon_trap();
}
複製代碼
虛擬機會註冊一個Segment Fault
信號的異常處理器,
NullPointerException
異常
他的重要意義是爲其餘優化手段打下基礎,爲啥這樣說呢,請看下面的示例:
public class DeadCode {
public static void testInline(String[] args) {
Object object = null;
foo(object);
}
public static void foo(Object object){
if(object != null){
System.out.println("do something");
}
}
}
複製代碼
咱們本身看下就應該知道,testInline()
方法中的代碼根本沒有一點意義。咱們稱這種代碼是"Dead Code"
若是不進行內聯,虛擬機根本不會發現,即便他進行了無用代碼消除,他在即時編譯的時候也不會消除這些代碼。由於分開來看,這兩個方法都是有意義的啊
方法內聯的行爲看起來彷佛很簡單
可是真的這麼簡單嗎?不是的,根據經典編譯原理的優化理論,大部分的Java方法都不能夠內聯
咱們知道方法的調用分爲:解析和分派,尤爲是分派中的動態分派,編譯期是不能知道調用的是哪一個類的方法(能夠參考個人這篇文章,重載和重寫的區別-方法調用層次)
好比下面的代碼
public class DeadCode {
static class ParentB{
int val;
public int getVal(){
return val;
}
public ParentB(int val){
this.val = val;
}
public ParentB(){
}
}
static class SubB extends ParentB{
public int getVal(){
return val;
}
public SubB(int val){
this.val = val;
}
public SubB() {
}
}
public static void main(String[] args) {
ParentB subB = new SubB(5);
System.out.println(subB.getVal());
}
}
複製代碼
編譯期,他是不會知道 getVal()
調用的是 sub的仍是Parent的方法,只有運行期纔會知道
那怎麼辦呢?
首先JVM團隊想到CHA(類型繼承關係分析,Class Hierarchy Analysis)技術。
一種基於整個應用程序的類型肥西技術,用於肯定在目前已加載的類中,某個接口是否有多於一種的實現,某各種是否存在子類、子類是否爲抽象類等信息。
編譯器在內聯時,若是是非虛方法(若是不知道這個概念,能夠參考個人這篇方法調用的文章,重載和重寫的區別-方法調用層次),那麼直接內聯就行
若是遇到虛方法,則會向CHA查詢此方法在當前程序下是否有多個目標版本可供選擇,
若是向CHA查詢的結果是有多個版本的目標方法能夠選擇,
編譯器會進行最後一次嘗試,使用內聯緩存完成內聯
這個是創建在目標方法正常入口以前的緩存
未發生方法調用以前,內聯緩存爲空
發生第一次調用,緩存記錄下方法接收者的版本信息,而且每次調用都比較接收者版本
他和上面的CHA(類型關係繼承分析)同樣,都不是直接優化的手段,而是爲其餘優化手段提供依據的分析技術
逃逸分析的基本行爲就是:分析對象動態做用域
若是能證實一個對象不會有方法逃逸和線程逃逸,那就能夠對其進行高效的優化: