纳尼???我JVM优化过头了,直接把异常信息优化没了?怎么办
你好呀,我是why。
你猜這次我又要寫個啥沒有卵用的知識點呢?
不好意思,問的稍微有點早了,啥提示都沒給,咋猜呢,對吧?
先給你上個代碼:
public class ExceptionTest {public static void main(String[] args) {String msg = null;for (int i = 0; i < 500000; i++) {try {msg.toString();} catch (Exception e) {e.printStackTrace();}}} }來,就這代碼,你猜猜寫出個什么花兒來?
當然了,有猜到的朋友,也有沒猜到的朋友。
很好,那么請猜出來了的同學迅速拉到文末,完成一鍵三連的任務后,就可以出去了。
沒有猜出來的同學,我把代碼一跑起來,你就知道我要說啥了:
一瞬間的事兒,瞅見了嗎?神奇嗎?產生疑問了嗎?
沒關系,你要沒看清楚,我還能給你截個圖:
在拋出一定次數的空指針異常后,異常堆棧沒了。
這就是我標題說的:太扯了吧?異常信息突然就沒了。
你說為啥?
為啥?
這事就得從 2004 年講起了。
那一年,SUN 公司于 9 月 30 日 18 點發布了 JDK 5。
在其 release-notes 中有這樣一段話:
https://www.oracle.com/java/technologies/javase/release-notes-introduction.html
主要是框起來的這句話,看不明白沒關系,我用我八級半的英語給你翻譯一下。
我們一句句的來:
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.
如果要禁止使用預分配的異常,請使用這個新參數:-XX:-OmitStackTraceInFastThrow。
這幾句話先不管理解沒有。但是至少知道它這里描述的場景不就是剛剛代碼演示的場景嗎?
它最后提到了一個參數?-XX:-OmitStackTraceInFastThrow,二話不說,先拿來用了,看看效果再說:
同樣的代碼,加入該啟動參數后,異常堆棧確實會從頭到尾一直打印。
不知道你感覺到沒有,加入該啟動參數后,程序運行時間明顯慢了很多。
在我的機器上沒加該參數,程序運行時間是 2826 ms,加上該參數運行時間是 5885 ms。
說明確實是有提升性能的功能。
到底是咋提升的,下一節說。
先說個其他的。
這里都提到 JVM 參數了,我順便再分享一個網站:
https://club.perfma.com/topic/OmitStackTraceInFastThrow
該網站提供了很多功能,這是其中的幾個功能:
JVM 參數查詢功能那必須得有:
很好用的,你以后遇到不知道是干啥用的 JVM 參數,可以在這個網站上查詢一下。
到底為啥?
前面講了是出于性能原因,從 JDK 5 開始會出現異常堆棧丟失的現象。
那么性能問題到底在哪?
來,我們一起看一下最常見的空指針異常。
以本文為例,看一下異常拋出的時候調用路徑:
最終會走到這個 native 方法:
java.lang.Throwable#fillInStackTrace(int)
fill In Stack Trace,顧名思義,填入堆棧跟蹤。
這個方法會去爬堆棧,而這個過程就是一個相對比較消耗性能的過程。
為啥比較耗時呢?
給你看個比較直觀的:
這類的異常堆棧才是我們比較常見的,這么長的堆棧信息,可不消耗性能嗎。
現在,我們現在再回去看這句話:
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.
出于性能的考慮,當一個異常被拋出若干次后,該方法可能會被重新編譯。在重新編譯之后,編譯器可能會選擇一種更快的策略,即不提供異常堆棧跟蹤的預分配異常。
所以,你能明白,這個“出于性能的考慮”這句話,具體指的就是節約 fillInStackTrace(爬堆棧)的這個性能消耗。
更加深入一點的研究對比,你可以看看這個鏈接:
http://java-performance.info/throwing-an-exception-in-java-is-very-slow
我這里貼一下結論:
關于消除異常的性能消耗,他提出了三個解決方案:
重構你的代碼不使用它們。
緩存異常實例。
重寫 fillInStackTrace 方法。
通過小日…小日子過的還不錯的日本的站點,輸入關鍵信息后,知乎的這個鏈接排在第二個:
https://www.zhihu.com/question/21405047
這個問題下面,有一個R大的回答,粘貼給你看看:
大家都不約而同的提到了重寫 fillInStackTrace 方法,這個性能優化小技巧,也就是我們可以這樣去自定義異常:
用一個不嚴謹的方式測試一下,你就看這個意思就行:
重寫了 fillInStackTrace 方法,直接返回 this 的對象,比調用了爬棧方法的原始方法,快了不是一星半點兒。
其實除了重寫 fillInStackTrace 方法之外,JDK 7 之后還提供了這樣的一個方法:
java.lang.Throwable#Throwable(java.lang.String, java.lang.Throwable, boolean, boolean)
可以通過 writableStackTrace 入參來控制是否需要去爬棧。
那么到底什么時候才應該去用這樣的一個性能優化手段呢?
其實R大的回答里面說的很清楚了:
其實我們寫業務代碼的,異常信息打印還是非常有必要的。
但是對于一些追求性能的框架,就可以利用這個優勢。
比如我在 disruptor 和 kafka 的源碼里面都找到了這樣的優化落地源碼。
先看 disruptor 的:
com.lmax.disruptor.AlertException
- Overridden so the stack trace is not filled in for this exception for performance reasons.
- 由于性能的原因,重載后的堆棧跟蹤不會被填入這個異常。
再看 kafka 的:
org.apache.kafka.common.errors.ApiException
- avoid the expensive and useless stack trace for api exceptions
- 避免對api異常進行昂貴而無用的堆棧跟蹤
而且你注意到了嗎,上面著兩個框架中,直接把 synchronized 都干掉了。如果你也打算重寫,那么也可以分析一下你的場景中是否可以去掉 synchronized,性能又可以來一點提升。
另外,R大的回答里面還提到了這個優化是 C2 的優化。
我們可以簡單的證明一下。
分層編譯
前面提到的 C2,其實還有一個對應的 C1。這里說的 C1、C2 都是即時編譯器。
你要是不熟悉 C1、C2,那我換個說法。
C1 其實就是 Client Compiler,即客戶端編譯器,特點是編譯時間較短但輸出代碼優化程度較低。
C2 其實就是 Server Compiler,即服務端編譯器,特點是編譯耗時長但輸出代碼優化質量也更高。
大家常常提到的 JVM 幫我們做的很多“激進”的為了提升性能的優化,比如內聯、快慢速路徑分析、窺孔優化,包括本文說的“不顯示異常堆棧”,都是 C2 搞的事情。
多說一句,在 JDK 10 的時候呢,又推出了 Graal 編譯器,其目的是為了替代 C2。
至于為什么要替換 C2,額,原因之一是這樣的…
http://icyfenix.cn/tricks/2020/graalvm/graal-compiler.html
C2 的歷史已經非常長了,可以追溯到 Cliff Click 大神讀博士期間的作品,這個由 C++ 寫成的編譯器盡管目前依然效果拔群,但已經復雜到連 Cliff Click 本人都不愿意繼續維護的程度。
你看前面我說的 C1、C1 的特點,剛好是互補的。
所以為了在程序啟動、響應速度和程序運行效率之間找到一個平衡點,在 JDK 6 之后,JVM 又支持了一種叫做分層編譯的模式。
也是為什么大家會說:“Java 代碼運行起來會越來越快、Java 代碼需要預熱”的根本原因和理論支撐。
在這里,我引用《深入理解Java虛擬機HotSpot》一書中 7.2.1 小節[分層編譯]的內容,讓大家簡單了解一下這是個啥玩意。
首先,我們可以使用?-XX:+TieredCompilation?開啟分層編譯,它額外引入了四個編譯層級。
- 第 0 級:解釋執行。
- 第 1 級:C1 編譯,開啟所有優化(不帶 Profiling)。Profiling 即剖析。
- 第 2 級:C1 編譯,帶調用計數和回邊計數的 Profiling 信息(受限 Profiling).
- 第 3 級:C1 編譯,帶所有Profiling信息(完全Profiling).
- 第 4 級:C2 編譯。
常見的分層編譯層級轉換路徑如下圖所示:
- 0→3→4:常見層級轉換。用 C1 完全編譯,如果后續方法執行足夠頻繁再轉入 4 級。
- 0→2→3→4:C2 編譯器繁忙。先以 2 級快速編譯,等收集到足夠的 Profiling 信息后再轉為3級,最終當 C2 不再繁忙時再轉到 4 級。
- 0→3→1/0→2→1:2/3級編譯后因為方法不太重要轉為 1 級。如果 C2 無法編譯也會轉到 1 級。
- 0→(3→2)→4:C1 編譯器繁忙,編譯任務既可以等待 C1 也可以快速轉到 2 級,然后由 2 級轉向 4 級。
如果你之前不知道分層編譯這回事,沒關系,現在有這樣的一個概念就行了。面試不會考的,放心。
接下來,就要提到一個參數了:
-XX:TieredStopAtLevel=___
看名字你也知道了,這個參數的作用是讓分層編譯停在某一層,默認值為 4,也就是到 C2 編譯。
那我把該值修改為 3,豈不是就只能用 C1 了,那就不能利用 C2 幫我優化異常啦?
實驗一波:
果然如此,R大誠不欺我。
總結
以上是生活随笔為你收集整理的纳尼???我JVM优化过头了,直接把异常信息优化没了?怎么办的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .Net性能调优-垃圾回收!!!最全垃圾
- 下一篇: CSDN《某一程序员竟然吃过shi?让我