天天看點

假裝會優化之jvmjvm

jvm

  • jvm優化首先要知道一些類加載,編譯器.堆外記憶體.計數器的概念和優化點

類加載

  • 類加載的過程需要連接配接和初始化最後才會使用
    • 連接配接:
      • 1:驗證:檢查規範
      • 2:準備 給類的靜态變量賦初始值,final修飾的直接指派
      • 3:解析:符号引用->直接引用
    • 初始化:jvm首先執行構造方法,編譯器在把java檔案編譯成class檔案時,就把靜态變量,靜态方法.靜态代碼塊一起組成 ,會按照源碼的順序來執行

      子類初始化會先執行父類的方法,再執行自己的

      父類靜态方法或靜态代碼塊(按照源碼順序)->子類靜态方法或靜态代碼塊->父類構造方法->子類構造方法

      在初始化中,如果是建立一個執行個體對象,則會調用方法,并執行對應的構造方法

編譯

  • 即時編譯:在位元組碼轉換成機器碼的過程中,還存在着一個編譯,即時編譯

    剛開始是由解釋器來進行編譯的,虛拟機發現某些方法經常被執行,就成為熱點代碼,為了提高效率,(JIT)即時編譯器會将代碼編譯成相關的機器碼,然後優化之後,存放到記憶體

分層編譯

  • java7之前有c1和c2兩個即時編譯器,之後就有了分層編譯
    • c1:關注局部性的優化,适用于執行時間較短或對啟動性能有要求的程式
    • c2:适用于執行時間長或對峰值性能有要求的程式
  • 分層編譯:
    • 0層:程式由解釋器執行,預設開啟監控功能(profiling),如果不開啟,觸發第二層編譯
    • 1層:c1編譯,将位元組碼編譯成本地代碼,進行簡單可靠的優化,不開啟profiling
    • 2層:c1編譯,開啟profling.僅執行帶方法調回次數和循環彙編執行次數profiling的c1編譯
    • 3層:c1編譯,執行所有帶profiling的c1編譯
    • 4層c2編譯,将位元組碼編譯成本地代碼,會啟用一些編譯耗時較長的優化,會根據新能監控資訊進行一些不可靠的激進優化
  • java8預設開啟了分層編譯,如果想要開啟c2,關閉分層編譯(-XX:-TieredCmpilation),如果隻想用c1,可以在打開分成編譯的同時,使用參數:

    -XX:TieredStopAtLevel=1.

    -Xint強制運作于隻有解釋器的編譯模式,

    -Xcomp強制運作隻有JIT的編譯模式下(無分層模式)

計數器

  • 熱點檢測:JIT優化的條件就是HotSpot的熱點檢測,熱點檢測是基于計數器的熱點探測,采用這方法的虛拟機會每個方法建立計數器統計方法的執行次數,超過門檻值,就是熱點方法
  • 計數器分類:
    • 1:方法調用計數器:統計方法被調用的次數,c1模式下是1500次,c2是10000次,可以使用-XX:CompileThreshold來設定,在分層編譯的情況下,-XX:CompileThreshold失效,根據待編譯的方法數和編譯線程數來動态調整,當方法計數器和回邊計數器和超過方法計數器門檻值時,會觸發JIT編譯器
    • 回邊計數器:用于統計一個方法中循環體代碼執行的次數,在位元組碼中遇到控制流向後跳轉的指令成為回邊,在不開啟分層的情況下,c1預設為13995,c2為10700,通過-XX:OnStackRelacePercentage=x來設定,在分層編譯中,門檻值失效,通過目前編譯的方法數,和編譯線程數來動态調整
    • 回邊計數器是為了觸發OSR編譯,棧上編譯,在循環周期較長的代碼段中,當循環 達到門檻值之後,JIT編譯器會将這段代碼編譯成機器語言緩存,在該循環中,會将執行代碼替代,執行緩存的機器語言

編譯優化

  • 編譯優化技術
    • 1:方法内聯:調用方法需要經曆壓棧和出棧,隻要執行前保護現場并記憶執行的位址,執行後恢複現場,會産生一定時間和空間上的開銷

      如果方法體代碼不是很大,有頻繁調用的方法來說,空間和時間消耗會很大,

      方法内聯的優化行為是吧目标方法的代碼指派到發起調用的方法之中,避免發生真是的方法調用

    • jvm會自動識别熱點方法,并使用方法内聯進行優化,通過-XX:CompileThreshold來設定熱點方法的門檻值,但是如果方法體太大,jvm也不會執行内聯操作,方法的大小門檻值可以設定參數來優化
    • 精華藏執行的方法,
      • 方法體小于325位元組都會進行内聯,通過

        -XX:MaxFreqlnlineSize=n來設定

      • 方法大小小于35位元組才會進行内聯,通過-XX:MaxInlineSize=n來設定
      • -XX:+PrintCompilation // 在控制台列印編譯過程資訊
      • -XX:+UnlockDiagnosticVMOptions // 解鎖對 JVM 進行診斷的選項參數。預設是關閉的,開啟後支援一些特定參數對 JVM 進行診斷
      • -XX:+PrintInlining // 将内聯方法列印出來
  • 總結:
    • 通過設定jvm參數減少熱點門檻值或增加方法體門檻值,以便更多的方法可以進行内聯,但是需要更多的記憶體
    • 避免在一個方法中寫大量的代碼,使用小方法體
    • 盡量使用final,privete,static關鍵字修飾方法,程式設計方法因為繼承,會需要額外的類型檢查

逃逸分析

  • 逃逸分析:判斷一個對象是否被外部方法引用或外部線程通路的分析技術,編譯器會根據逃逸分析的結果對代碼進行優化
  • 棧上配置設定,對象建立是在堆中配置設定記憶體的,如果逃逸分析發現一個對象隻在方法中使用,就會将對象配置設定到棧上(暫時未優化)
  • 鎖消除:如果加鎖的對象或變量是在局部方法中,處于隻能被目前線程通路,無法被其他線程通路的,是以JIT會對這個進行鎖消除

标量替換

  • 标量替換:
    • 如果一個對象不會被外部通路,且這個對象可以被拆分,當真正執行是,可能不建立這個對象,而是直接建立他的成員變量來代替,對象拆分後,配置設定成員變量在棧或寄存器上,原本的對象就無須配置設定記憶體空間了,這個編譯優化叫做标量替換

      -XX:+DoEscapeAnalysis 開啟逃逸分析(jdk1.8 預設開啟,其它版本未測試)

      -XX:-DoEscapeAnalysis 關閉逃逸分析

      -XX:+EliminateLocks 開啟鎖消除(jdk1.8 預設開啟,其它版本未測試)

      -XX:-EliminateLocks 關閉鎖消除

      -XX:+EliminateAllocations 開啟标量替換(jdk1.8 預設開啟,其它版本未測試)

      -XX:-EliminateAllocations 關閉就可以了

堆外記憶體

  • 為什麼使用堆外記憶體
    • 1、減少了垃圾回收

        使用堆外記憶體的話,堆外記憶體是直接受作業系統管理( 而不是虛拟機 )。這樣做的結果就是能保持一個較小的堆内記憶體,以減少垃圾收集對應用的影響。

    • 2、提升複制速度(io效率)

        堆内記憶體由JVM管理,屬于“使用者态”;而堆外記憶體由OS管理,屬于“核心态”。如果從堆内向磁盤寫資料時,資料會被先複制到堆外記憶體,即核心緩沖區,然後再由OS寫入磁盤,使用堆外記憶體避免了這個操作。

  • 堆外記憶體建立有兩種方式:
    • 1.使用ByteBuffer.allocateDirect()得到一個DirectByteBuffer對象,初始化堆外記憶體大小,裡面會建立Cleaner對象,綁定目前this.DirectByteBuffer的回收,通過put,get傳遞進去Byte數組,
    • 2.序列化對象,Cleaner對象實作一個虛引用(當記憶體被回收時,會受到一個系統通知)當Full GC的時候,如果DirectByteBuffer标記為垃圾被回收,則Cleaner會收到通知調用clean()方法,回收改堆外記憶體DirectByteBuffer

關于gc和回收器的選擇

假裝會優化之jvmjvm

繼續閱讀