异常堆栈信息丢失?到底是怎么回事?
01 即時編譯優(yōu)化
Java程序在運行初期是通過解釋器來執(zhí)行,當(dāng)發(fā)現(xiàn)某塊代碼運行特別頻繁,就會將之判定為熱點代碼(Hot Spot Code), 虛擬機會將這部分代碼編譯成本地機器碼,并對這些代碼進行優(yōu)化。這件事就是即時編譯(Just In Time, JIT)優(yōu)化, 做這件事的就是即時編譯器。
1.?解釋器與編譯器
目前主流虛擬機都采用解釋器、編譯器并存的架構(gòu)。
解釋器:程序執(zhí)行初期,解釋器執(zhí)行的方式可以省去編譯過程,節(jié)省時間
編譯器:在渡過初期后,編譯器把更多的代碼編譯成本地代碼,提升執(zhí)行效率,以空間換時間
因為編譯器存在過度優(yōu)化,基于假設(shè)優(yōu)化等可能失敗的優(yōu)化結(jié)果,通過逆優(yōu)化(Deoptimization)的方式,將程序的執(zhí)行主動權(quán)從編譯器交給解釋器執(zhí)行。可以把解釋器看成是一個保守派,編譯器是一個激進派,在JVM執(zhí)行體系里,兩者相輔相成,互相配合。
1.1 編譯器種類
一般虛擬機都內(nèi)置了兩個或三個即時編譯器,歷史比較久遠的C1, C2, 以及在JDK10才出現(xiàn)的Graal
C1:客戶端編譯器(Client Complier),執(zhí)行時間較短,啟動程序的時間較快。在一些物聯(lián)網(wǎng)小型設(shè)備上可指定這種編譯器,通過-client參數(shù)強制指定
C2:服務(wù)端編譯器(Server Complier),執(zhí)行時間較長,啟動時間較長但可編譯高度優(yōu)化的代碼,峰值性能更高。可通過-server參數(shù)強制指定
Graal:是一個實驗性質(zhì)的即時編譯器,其最大的特點是該編譯器用Java語言編寫,更加模塊化,也更容易開發(fā)與維護。充分預(yù)熱后Java代碼編譯成二進制碼后其執(zhí)行性能并不亞于由C++編寫的C2。可以通過參數(shù) -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 啟用,并替換 C2
1.2 分層編譯優(yōu)化
雖然可以通過-Xint參數(shù)強制虛擬機處于"解釋模式"此時編譯器不工作,可以通過-Xcomp參數(shù)強制虛擬機處于"編譯模式"此時解釋器不工作,可以通過-client參數(shù)使C2不工作,也可以通過-server參數(shù)使C1不工作,但是并不推薦這樣做,因為有分層編譯優(yōu)化這一特性。
編譯器在編譯代碼的時候會占用程序運行時間,優(yōu)化程度越高的代碼編譯時間會越長,甚至?xí)枰忉屍髫?fù)責(zé)收集程序運行監(jiān)控信息提供給編譯器來編譯優(yōu)化程度更高的代碼。所以為了在更短的時間內(nèi)編譯優(yōu)化程度更高的代碼,需要編譯器之間的配合,也就是所謂的分層編譯優(yōu)化。一共有五層,分別是:
純解釋執(zhí)行,解釋器不開啟收集程序運行監(jiān)控信息
使用C1編譯器進行簡單可靠的優(yōu)化,解釋器不開啟收集程序運行監(jiān)控信息
仍然使用C1編譯器優(yōu)化,但是會針對方法調(diào)用次數(shù)和回邊次數(shù)(循環(huán)代碼調(diào)用次數(shù))相關(guān)的統(tǒng)計
仍然使用C1編譯器優(yōu)化,統(tǒng)計信息才上一層的基礎(chǔ)上會加上分支跳轉(zhuǎn)、虛方法調(diào)用等全部統(tǒng)計信息,解釋器火力全開
使用C2編譯器優(yōu)化,相比C1,C2會開啟更多耗時更長的優(yōu)化,還會根據(jù)解釋器提供的程序運行信息進行一些更為激進的優(yōu)化
在開啟編譯優(yōu)化后,熱點代碼可能會被重復(fù)編譯,C1編譯器編譯得更快,C2編譯器編譯質(zhì)量更高,第0層模式解釋器執(zhí)行的時候也不用收集監(jiān)控信息,第4層模式C2在進行耗時較長的編譯較為忙碌時候,C1也能為C2承擔(dān)一部分編譯工作,交互關(guān)系如下圖
common是針對大部分代碼的編譯情況,trival method針對執(zhí)行次數(shù)較少的代碼
trival method很少被執(zhí)行所以沒有被C2編譯的必要,通過第4層模式的優(yōu)化就足夠了
在C1忙碌的時候,會直接由C2編譯;C2忙碌的時候,在C1編譯的路徑也會更長
2. 編譯觸發(fā)條件
上面提到即使編譯是針對熱點代碼進行編譯優(yōu)化,那么什么是熱點代碼?
被多次調(diào)用的方法
被多次執(zhí)行的循環(huán)代碼體
這里的多次如何知道具體有多少次?有兩種方法可以知道
基于采樣的熱點探測(Sample Based Hot Spot Code Detection): 虛擬機周期性地檢查各個線程的調(diào)用棧頂,如果發(fā)現(xiàn)某個方法經(jīng)常出現(xiàn)在棧頂,那么這個方法就是熱點方法,這種方法簡單高效但是精確度不高
基于計數(shù)器的熱點探測(Counter Based Hot Spot Code Dection): 虛擬機為每個方法建立計數(shù)器,計數(shù)器超過一定閾值就是熱點方法
目前HotSpot虛擬機使用的是第二種方法,虛擬機為每個方法都準(zhǔn)備了兩類計數(shù)器,方法調(diào)用計數(shù)器以及回邊計數(shù)器(回邊的意思是在循環(huán)的末尾邊界往回跳轉(zhuǎn),可以理解為循環(huán)代碼的一次執(zhí)行)
講到這里給大家舉一個工作中經(jīng)常見到的一個JIT優(yōu)化案例:異常堆棧丟失
02?異常堆棧丟失
1. 問題
總所周知在打印Java異常的時候,會將其堆棧信息一并輸出,這些堆棧信息非常重要,有助于我們排查問題,像這樣
20:10:50.491?[main]?ERROR?com.yangkw.ErrorTestjava.lang.NullPointerException: nullat com.yangkw.ErrorTest.error(ErrorTest.java:33)??at?com.yangkw.ErrorTest.main(ErrorTest.java:19)但是在最近在觀察系統(tǒng)的線上運行日志的時候,發(fā)現(xiàn)很多不帶堆棧的異常日志,讓人摸不著頭腦到底發(fā)生了什么,像這樣
20:10:50.491?[main]?ERROR?com.yangkw.ErrorTestjava.lang.NullPointerException: null2. 猜想
通過前面關(guān)于JIT編譯觸發(fā)條件的介紹,可以設(shè)想是拋出異常執(zhí)行太頻繁所以觸發(fā)了JIT優(yōu)化導(dǎo)致,于是我們可以寫一個Demo來驗證,堆棧完整的時候打印"full trace",堆棧丟失的時候打印"no trace"
public static void main(String[] args) throws InterruptedException {int count = 0;while (true) {try { count++; //統(tǒng)計調(diào)用次數(shù) error(); } catch (Exception e) {if (e.getStackTrace().length == 0) { LOG.error("no trace count:{}", count, e); Thread.sleep(1000); //方便觀察日志 } else { LOG.error("full trace count:{}", count, e); } } } }private static void error() { String nullMsg = null; nullMsg.toString(); }下面是執(zhí)行結(jié)果,可以看出程序是在執(zhí)行到8405次(每次執(zhí)行都會不同)的時候丟失了堆棧
3. 驗證
雖然8405次執(zhí)行的時候丟失了堆棧,但是并不能說明是因為JIT優(yōu)化導(dǎo)致的,于是我們可以加上參數(shù)-XX:+PrintCompilation 來打印即時編譯情況。
可以看到,在10388次執(zhí)行的時候是有堆棧信息的,在10389次執(zhí)行的時候就丟失了堆棧信息,在這中間就發(fā)生了即使編譯優(yōu)化,針對這一現(xiàn)象官方術(shù)語稱之為"fast throw"可以通過參數(shù)-XX:-OmitStackTraceInFastThrow關(guān)閉這一優(yōu)化
在ORACLE官方文檔有這么一段描述
The compiler in the server VM now provides correct stack backtraces for all "cold" built-in exceptions. For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag: -XX:-OmitStackTraceInFastThrow.
堆棧丟失只是表面現(xiàn)象,JIT還對其做了以下優(yōu)化:
創(chuàng)建需要拋出異常的實例
清空堆棧信息
將該實例緩存起來
之后再需要拋出的時候,將緩存實例拋出去
03 總結(jié)
解釋器、C1編譯器、C2編譯器各有優(yōu)劣,合理搭配,干活不累
-XX:-OmitStackTraceInFastThrow 謹(jǐn)慎使用,如果關(guān)閉fast throw的優(yōu)化應(yīng)預(yù)防"日志風(fēng)暴"使磁盤空間迅速被打滿
做好歷史日志的記錄以及備份,筆者通過回查歷史日志成功追回了異常的堆棧信息
日照充足的西瓜會更甜,擁有即時編譯優(yōu)化會讓Java程序程序更靈性
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的异常堆栈信息丢失?到底是怎么回事?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 当sql 没有足够的内存执行程序利用命令
- 下一篇: JPA的多表复杂查询