公司公眾號(hào)去年上的一篇文章,搬運(yùn)過來(lái)
背景
java8的正式版本已經(jīng)發(fā)布了2年多了,我們都知道java8更加高效,比如更加高效的G1、更加高效的jit、默認(rèn)開啟TieredCompilation更加高效的工作模式以及和“java.lang.OutOfMemoryError: PermGen space” 撒油啦啦 等等高大上的性能優(yōu)化;當(dāng)然了,這都不是最主要的:
主要原因是因?yàn)榍耙欢螘r(shí)間線上tomcat很詭異的就突然掛掉了,經(jīng)查失敗log,是命中了一個(gè)jvm crash的bug,bug詳見:http://bugs.java.com/view_bug.do?bug_id=8021898,jdk的1.7.0_60后修復(fù)了這個(gè)問題,正好看到j(luò)ava8那么多高大上的優(yōu)化后,就準(zhǔn)備一步到位直接升級(jí)到j(luò)ava8;
但是理想是美好的,現(xiàn)實(shí)是骨感的;我們很榮幸的掉進(jìn)了坑里了,下面主要從spring4.0的坑和新metaspace問題 這2個(gè)方面來(lái)描述。
spring4.0的問題
前言
上面說(shuō)了,遇到了jvm的crash的bug,必須要升級(jí)jdk的版本;找了套測(cè)試環(huán)境做了回歸,發(fā)現(xiàn)沒啥問題,非常高興,準(zhǔn)備在線上開搞。
現(xiàn)象
最開始的找了2組流量相對(duì)較小的機(jī)器,升級(jí)后重啟服務(wù)沒有發(fā)現(xiàn)異常現(xiàn)象,以為沒啥問題了,就準(zhǔn)備開始搞一組qps比較高的機(jī)器,替換jdk重啟之后沒啥問題,然后觀察了會(huì),發(fā)現(xiàn)load在緩慢上升,緊接著服務(wù)的接口開始大量的超時(shí),見下圖:

排查過程
-
stack排查
首先,是抓了幾個(gè)tomcat棧log下來(lái),研究棧發(fā)現(xiàn)有大量的lock,見下圖:
往下繼續(xù)找正在運(yùn)行的那個(gè)線程,如下圖:
通過上面的現(xiàn)象看到,不管是lock的,還是runnable的,都是卡在的spring的代碼棧下,這里就有點(diǎn)奇怪了,spring的版本沒有升級(jí)啊,先否決了,因?yàn)閖ava8針對(duì)gc和jit有過相關(guān)的,首先來(lái)開始排查這2者,繼續(xù)往下; - 排查gc
首先排查了gc的問題,發(fā)現(xiàn)除了比之前更頻繁一點(diǎn)之外,其它沒啥區(qū)別,也沒有觸發(fā)full gc,基本可以排除 - jit排查
java8針對(duì)jit有一些默認(rèn)的優(yōu)化,所以發(fā)現(xiàn)gc沒有問題的情況下,首先想到的就是jit的設(shè)置問題
首先懷疑的是jit的cache的內(nèi)存大小問題,調(diào)大后重啟,發(fā)現(xiàn)只能延緩一點(diǎn)load升高的時(shí)間,沒有根本解決問題
然后是調(diào)整jit的默認(rèn)線程數(shù),之前我們是設(shè)置了2個(gè)線程,使用默認(rèn)的3個(gè)線程,有一定的效果,抗住了20分鐘;然后繼續(xù)調(diào)大4、5,發(fā)現(xiàn)反而比之前掛了還要快,重啟之后基本上就掛了,也基本可以排除
spring排查
最容易出現(xiàn)問題的方面排查過了,沒有發(fā)現(xiàn)問題,那就回歸棧日志來(lái)排查,找不到明顯問題,問下Google吧。
還真有發(fā)現(xiàn):spring官網(wǎng)有人匯報(bào)過一個(gè)bug,SerializableTypeWrapper從jdk的1.7.0_51這個(gè)版本開始出現(xiàn)了性能瓶頸,bug地址詳見:https://jira.spring.io/browse/SPR-11335
我們之前的是用的jdk1.7.0_45這個(gè)版本,沒有發(fā)現(xiàn)這個(gè)問題;具體原因在后面來(lái)描述。
解決方案
升級(jí)spring的版本就可以解決這個(gè)問題;
從4.0.3開始,spring針對(duì)jdk8有較大的改善,具體可參見官網(wǎng)說(shuō)明:https://spring.io/blog/2014/03/27/spring-framework-4-0-3-released-with-java-8-support-now-production-ready
問題分析
ResolvableType
首先我們來(lái)看下ResolvableType這個(gè)類的相關(guān)代碼,如下圖:



可以看到1018-1022行之間,先是從cache中取,如果沒有取到,就會(huì)往cache中put一個(gè),通過上面的??梢灾?,equals比較耗時(shí)(會(huì)通過SerializableTypeWrapper生成相關(guān)代理),這里get和put之間沒有任何攔截措施,get不到直接put,put內(nèi)部是有l(wèi)ock的,就是我們看到的上面的lock的圖,這樣就有可能導(dǎo)致大量的線程等待;
按理說(shuō)即使會(huì)慢,也應(yīng)該只是在剛開始啟動(dòng)的時(shí)候,等都初始化完全后,cache中就有了,后面就好了啊,咱們繼續(xù)往下看;
ConcurrentReferenceHashMap的問題
ConcurrentReferenceHashMap是一個(gè)soft cache, 過期的時(shí)候會(huì)走到put里,加鎖等待,導(dǎo)致更多的線程block, 線程block導(dǎo)致tomcat更多的線程同時(shí)運(yùn)行導(dǎo)致gc壓力,而soft cache是一個(gè)軟cache,壓力過大時(shí)會(huì)被gc回收掉,這樣會(huì)導(dǎo)致soft cache 接連失效;這也就驗(yàn)證了上面開始的gc比之間頻繁的問題,這種情況在qps高的服務(wù)下會(huì)比較容易出現(xiàn);
spring的修復(fù)方案
ResolvableType

可以看到在get之前加了一個(gè)check操作;大體意思就是checker一下cache的有效性,如果cache已過期,把過期的信息強(qiáng)制剔除以提高效率
SerializableTypeWrapper

可以看到針對(duì)equals有優(yōu)化操作;大體意思是在執(zhí)行equals方法的時(shí)候,直接執(zhí)行原始的equals,不用再經(jīng)過多層代理的過濾;尤其是針對(duì)ResolvableType這種高執(zhí)行頻率的操作效果較好
Metaspace的問題
前言
經(jīng)過上面的折騰,終于算是把java8用上了,運(yùn)行了幾天之后很正常(其實(shí)是我想多了);
現(xiàn)象
在大概一個(gè)星期之后的凌晨,突然收到報(bào)警,線上服務(wù)swap有報(bào)警或者服務(wù)掛掉了。。。
問題排查
查了下原因,問題出現(xiàn)在java8新搞得MetaSpace身上
概念
首先來(lái)介紹2個(gè)概念;
- PermGen
PermGen space的全稱是Permanent Generation space,是指內(nèi)存的永久保存區(qū)域,說(shuō)說(shuō)為什么會(huì)內(nèi)存益出:這一部分用于存放Class和Meta的信息,Class在被 Load的時(shí)候被放入PermGen space區(qū)域,它和和存放Instance的Heap區(qū)域不同,所以如果你的APP會(huì)LOAD很多CLASS的話,就很可能出現(xiàn)PermGen space錯(cuò)誤。這種錯(cuò)誤常見在web服務(wù)器對(duì)JSP進(jìn)行pre compile的時(shí)候。 -
MetaSpace
Java8將移除永久區(qū),使用本地內(nèi)存來(lái)存儲(chǔ)類元數(shù)據(jù)信息并稱之為:元空間(Metaspace);
Java8的啟動(dòng)參數(shù):PermSize 和 MaxPermSize 會(huì)被忽略并給出警告(如果在啟用時(shí)設(shè)置了這兩個(gè)參數(shù))
這意味著不會(huì)再有java.lang.OutOfMemoryError: PermGen問題,也不再需要你進(jìn)行調(diào)優(yōu)及監(jiān)控內(nèi)存空間的使用……但請(qǐng)等等,這么說(shuō)還為時(shí)過早。在默認(rèn)情況下,這些改變是透明的,接下來(lái)我們的展示將使你知道仍然要關(guān)注類元數(shù)據(jù)內(nèi)存的占用。請(qǐng)一定要牢記,這個(gè)新特性也不能神奇地消除類和類加載器導(dǎo)致的內(nèi)存泄漏。
Metaspace 容量
默認(rèn)情況下,類元數(shù)據(jù)只受可用的本地內(nèi)存限制(容量取決于是32位或是64位操作系統(tǒng)的可用虛擬內(nèi)存大?。?。
新參數(shù)(MaxMetaspaceSize)用于限制本地內(nèi)存分配給類元數(shù)據(jù)的大小。如果沒有指定這個(gè)參數(shù),元空間會(huì)在運(yùn)行時(shí)根據(jù)需要?jiǎng)討B(tài)調(diào)整。
原因分析
在java8里,由于PermSize 和 MaxPermSize已經(jīng)失效,而你又正好沒有設(shè)置MetaspaceSize和MaxMetaspaceSize這2個(gè)參數(shù),那么就有可能會(huì)導(dǎo)致 metaspace的空間在不停的擴(kuò)展,會(huì)導(dǎo)致機(jī)器的內(nèi)存不足;進(jìn)而可能出現(xiàn)swap內(nèi)存被吃;嚴(yán)重可能導(dǎo)致進(jìn)程直接被系統(tǒng)直接kill掉
總結(jié)
升級(jí)java8應(yīng)該注意以下2點(diǎn):
- 需配合Spring 4.0.3以上版本使用
- 需要配置Metaspace大小
暫時(shí)為止,升級(jí)java8的問題暫時(shí)告一段落;
如果你近期有升級(jí)java8的計(jì)劃,正好你也看到了這篇文章,那么恭喜你,你可以少踩2個(gè)坑;java8很美好,升級(jí)需謹(jǐn)慎。
還遺漏一個(gè)問題,為什么ResolvableType的equals在java8下性能會(huì)明顯下降,大家可以自己找找源碼,后面有機(jī)會(huì)可以再單獨(dú)出一篇文章來(lái)解釋下。


