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