見到有人讨論java裡的異常性能是好是壞,在業務代碼裡要不要用異常處理.
例如一些請求參數,到底是應該人工用if/else判斷,還是通過異常統一處理,還是通過注解校驗,抑或是其他方式呢?
這些方式對系統的性能會有什麼實際影響呢?
一般認為,java異常之是以慢,是因為需要擷取目前的運作棧資訊,而異常機制本身是正常性能消耗.
進而想到,一些通用的日志架構,比如log4j,logback,都是通過運作棧擷取抛出異常的代碼方法和運作行數的,官方文檔也标注此類資訊的列印會有較大的性能消耗.
而且我們也是通過類似的方式對jws的日志進行了增強.
那麼性能到底會有有多差?
運作環境
jdk8 + idea 2016
mac mini 2014 中配(硬碟非常慢,如有其它運作結果請聯系)
非空機運作,是以結果不是非常準确
jvm配置<code>-client -xcomp -xmx1024m -xms1024m</code>,每次調用完後主動通知gc一次
編譯模式跟解析模式的結果相差非常大
一般都說,使用異常之是以慢,是因為擷取運作棧慢,那到底有多慢,慢在哪裡,如果不擷取異常棧的話速度又如何?
通過<code>throwable</code>的源碼可以得知,大部分的夠着函數裡都會調用<code>java.lang.throwable#fillinstacktrace()</code>方法設定目前運作的異常棧,而這個方法慢的原因有:
方法本身是<code>synchronized</code>,也就是對象級的同步
這個方法需要有一個native調用
需要擷取目前運作棧的所有資訊
另外,throwable的很多方法也是<code>synchronized</code>的.
(不過線程棧資訊本身有緩存,理論上第二次調用會快一些,而且異常一般不會在多個線程中處理)
是以理論上來說,隻要把異常棧填充這一步去掉,異常對象應該是跟普通對象是差不多的.
剛好throwalbe及各種exception都有一個特殊的構造函數,可以跳過異常棧的擷取.
普通對象
hashmap
省略異常棧
普通異常
1000000個對象
35
38
48
1444
10000000個對象
30
62
99
10525
100000000個對象
269
593
936
112262
可以看到,關閉異常棧擷取後,異常對象的建立跟普通對象基本一緻,而擷取異常棧則多了不止一個數量級的耗時.
10個線程,每個線程10000000個對象
223
728
534
73946
100個線程,每個線程1000000個對象
251
605
533
85106
500個線程,每個線程200000個對象
264
780
793
68589
在并行模式下的結果跟串行模式幾乎一緻.
如果異常對象的建立的性能沒有問題,那麼try/catch塊呢?
沒有try/catch
循環外try/catch
循環内try/catch
循環内3次try/catch
運作10000000次
10
12
9
13
運作100000000次
43
49
53
45
運作500000000次
221
222
229
可以看到,當沒有發生異常時,try/catch并不消耗性能.
那麼,對異常的throw/catch需要消耗多少性能呢?
沒有throw/catch
throw/catch
11
36
51
113
252
519
而當抛出異常時,try/catch本身的性能消耗也隻是普通的代碼性能消耗.
通用的日志架構裡都是通過運作棧去擷取日志調用代碼的方法名/行數等資訊的,而且通過上述測試可以知道,這種方式會對性能代碼比較大的影響.
通過往某個檔案列印日志對運作資訊擷取的性能做測試.(使用非緩存式列印,一般日志架構預設都沒有使用緩存列印)
普通日志列印
運作資訊日志列印
列印10000行
297
列印100000行
535
2132
列印500000行
2576
8598
在串行模式下,通過運作棧擷取調用資訊确實會比普通的日志列印耗時多,可是可能因為磁盤io關系,性能差異沒有異常對象與普通對象的差異大.
10個線程,每個線程列印100000行
7199
19511
20個線程,每個線程列印50000行
7600
20053
50個線程,每個線程列印20000行
7832
19031
在并發環境下,由于性能進一步被檔案io限制,性能差異進一步縮小.
以上所有的測試都隻是驗證在一個非常純粹的環境下的性能表現,其中甚至會有jvm的一些優化措施.
而實際業務進行中,會有架構,網絡,業務處理等多種因素會影響系統的性能.
是以通過在本機搭建的一個基于jws的web工程來模拟實際的業務服務,通過ab測試相關的場景性能.
建立一個有3個參數的請求,通過ab工具測試if/else和try/catch兩種參數校驗方式對性能的影響.
(資料機關:qps)
if/else參數校驗
try/catch優化異常參數校驗
try/catch普通異常參數校驗
串行,5000請求
166.140
163.773
164.636
10并發,5000請求
206.59
206.22
203.06
50并發,5000請求
205.74
205.64
202.80
根據統計平台,生産環境80%請求的耗時基本都在10ms附近,測試用例通過<code>sleep</code>的方式模拟每次請求有10ms的業務處理.
(考慮到機器問題,在本機器上10ms的業務處理應該算是一個很小的值了)
每次請求列印10行日志
每次請求列印20行日志
每次請求列印50行日志
普通日志
56.67
54.85
49.76
運作上線文日志
51.39
47.44
37.10
59.54
57.27
52.78
55.81
48.96
38.57
雖然即使經過優化後的異常性能也有一定的消耗,可是在異常校驗這種場景下,一來因為發生頻率小,二來因為執行次數少(一個請求隻會出現一次try/catch),對業務請求的性能幾乎沒有影響.
而通過運作棧實作的日志增強,因為日志列印本身的高頻率,是以對業務系統性能有一定的影響.
(此用例基于jws,非jws環境無法運作)