轉載請註明出處 http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html
上週在線上系統發現了兩個bug,值得記錄下查找的過程和原因。以後如果還有查找bug比較有價值的經歷,我也會繼續分享。
第一個bug的起始,是在線上日誌發現一個頻繁打印的異常——java.lang.ArrayIndexOutOfBoundsException。但是卻沒有堆棧,只有一行一行的ArrayIndexOutOfBoundsException。沒有堆棧,不知道異常是從什麼地方拋出來的,也就不能找到問題的根源,更談不上解決。題外,工程師在用log4j記錄錯誤異常的時候,我看到很多人這樣用(假設e是異常對象):
這樣才能在日誌中完整地輸出異常堆棧來。如何寫好日誌是另一個話題,這裏不展開。繼續我們的找bug經歷。剛纔提到,我們線上日誌一直出現一行錯誤信息ArrayIndexOutOfBoundsException卻沒有堆棧,是我們沒有正確地寫日誌嗎?檢查代碼不是的,這個問題其實是跟JDK5引入的一個新特性有關,對於一些頻繁拋出的異常,JDK爲了性能會做一個優化,在JIT重新編譯後會拋出沒有堆棧的異常。在使用server模式的時候,這個優化是開啓的,我們的服務器跑在server模式下並且jdk版本是6,因此在頻繁拋出ArrayIndexOutOfBoundsException異常一段時間後優化開始起作用,只拋出沒有堆棧的異常信息了。
那麼怎麼解決這個問題呢?因爲這個優化是在JIT重新編譯後才起作用,因此一開始拋出異常的時候還是有堆棧的,所以可以查看較舊的日誌,尋找有完整堆棧的異常信息。但是由於我們的日誌太大,會定期刪除,我們的服務器也啓動了很長時間,因此查找日誌不是很靠譜的方法。
另一個解決辦法是暫時禁用這個優化,強制要求每次都要拋出有堆棧的異常。幸好JDK提供了選項來關閉這個優化,配置JVM參數
我們找了臺機器,配置了這個參數並重啓一下。過了一會就找到問題所在,堆棧類似這樣
at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:436)
at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2081)
at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:1996)
at java.util.Calendar.setTimeInMillis(Calendar.java:1109)
at java.util.Calendar.setTime(Calendar.java:1075)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:876)
at java.text.SimpleDateFormat.format(SimpleDateFormat.java:869)
at java.text.DateFormat.format(DateFormat.java:316)
讀者肯定猜到了,這個問題是由於SimpleDateFormat的誤用引起的。SimpleDateFormat的javadoc中有這麼句話:
If multiple threads access a format concurrently, it must be synchronized externally.
出問題的代碼就是由於工程師認爲SimpleDateFormat的創建代價很高,然後搞了個map做緩存,所有線程共用這個instance做format,同時沒有做同步。悲劇就誕生了。
這裏就引出我想提到的第二點問題,在使用一個類或者方法的時候,最好能詳細地看下該類的javadoc,JDK的javadoc是做的非常好的,javadoc除了做說明之外,通常還會給示例,並且會點出一些關鍵問題,如線程安全性和平臺移植性。
最後,我將做個測試,到底在使用SimpleDateFormat怎麼做纔是最好的方式?假設我們要實現一個formatDate方法將日期格式化成"yyyy-MM-dd"的格式。
第一個方法是每次使用都創建一個instance,並調用format方法:
SimpleDateFormat format =new SimpleDateFormat("yyyy-MM-dd");
return format.format(date);
}
第二個方法是隻創建一個instance,但是在調用方法的時候做同步:
publicstaticsynchronized String formatDate2(Date date) {
return formatter.format(date);
}
第三個方法比較特殊,我們爲每個線程都緩存一個instance,存放在ThreadLocal裏,使用的時候從ThreadLocal裏取就可以了:
@Override
protected SimpleDateFormat initialValue() {
returnnew SimpleDateFormat("yyyy-MM-dd");
}
};
publicstatic String formatDate3(Date date) {
SimpleDateFormat format = formatCache.get();
return format.format(date);
}
然後我們測試下三個方法併發調用下的性能並做一個比較,併發100個線程循環調用1000萬次,記錄耗時。我們設置了JVM參數:
| 第1次測試 | 第2次測試 | 第3次測試 |
formatDate1 | 50545 | 49365 | 53532 |
formatDate2 | 10895 | 10761 | 10673 |
formatDate3 | 10386 | 9919 | 9527 |
從結果來看,方法1最慢,方法3最快,但是就算是最慢的方法1也可以達到每秒鐘
總結下本文找bug經歷想表達的幾點想法:
(1)正確地打印錯誤日誌
(2)在server模式下,最好都設置-XX:-OmitStackTraceInFastThrow
(3)使用類或者方法的時候,最好能詳細閱讀下javadoc,很多問題都能找到答案
(4)使用SimpleDateFormat的時候要注意線程安全性,要麼每次new,要麼做同步,兩者的性能有差距,但是這個差距很難成爲你的性能瓶頸。
下篇文章我再分享另一個bug的查找經歷,也是比較有趣,可以看到一些工具的使用。