業(yè)務(wù)敘述
技術(shù)是為了業(yè)務(wù)服務(wù)的上煤,所以討論技術(shù)的時候離不開業(yè)務(wù)場景板辽。故此先進(jìn)行簡述:
最近在做一個從數(shù)據(jù)庫讀取pdf文件并且解析成文字進(jìn)行分析的功能僵刮。由于本人對垃圾回收也是略有涉獵攒霹,因此代碼設(shè)計上基本不存在本應(yīng)該回收但是卻無法回收的情況厘托。然而,這個程序用springboot啟動后一段時間就會出現(xiàn)oom錯誤流炕,錯誤日志如下:
java.lang.OutOfMemoryError: Java heap space
at oracle.sql.BLOB.getBytes(BLOB.java:217)
at org.apache.ibatis.type.BlobTypeHandler.getNullableResult(BlobTypeHandler.java:43)
at org.apache.ibatis.type.BlobTypeHandler.getNullableResult(BlobTypeHandler.java:28)
at org.apache.ibatis.type.BaseTypeHandler.getResult(BaseTypeHandler.java:81)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyAutomaticMappings(DefaultResultSetHandler.java:521)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:402)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:354)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:328)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:301)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:194)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:65)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
at com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor.doQuery(MybatisSimpleExecutor.java:67)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:83)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
at sun.reflect.GeneratedMethodAccessor34.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:426)
at com.sun.proxy.$Proxy60.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:223)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.executeForMany(MybatisMapperMethod.java:158)
at com.baomidou.mybatisplus.core.override.MybatisMapperMethod.execute(MybatisMapperMethod.java:76)
at com.baomidou.mybatisplus.core.override.MybatisMapperProxy.invoke(MybatisMapperProxy.java:61)
at com.sun.proxy.$Proxy65.findAttachments(Unknown Source)
at sun.reflect.GeneratedMethodAccessor35.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
從日志中可以看出澎现,在獲取數(shù)據(jù)庫中的Blob數(shù)據(jù)時,出現(xiàn)了Java heap space不足每辟,也就是堆滿了剑辫。在此科普一下幾種OOM的情況:
PermGen space
發(fā)生這種問題的原意是程序中使用了大量的jar或class,使java虛擬機(jī)裝載類的空間不夠渠欺,與Permanent Generation space有關(guān)妹蔽。常用的解決方法是調(diào)整XX:PermSize和XX:MaxPermSize參數(shù)的大小。
unable to create new native thread
顧名思義就是不能創(chuàng)建native的線程。遇到這種情況還是比較復(fù)雜的胳岂,具體遇到了再分析编整。
Java heap space
發(fā)生這種問題的原因是java虛擬機(jī)創(chuàng)建的對象太多,在進(jìn)行垃圾回收之間乳丰,虛擬機(jī)分配的到堆內(nèi)存空間已經(jīng)用滿了闹击,與Heap space有關(guān)。這是比較常見的情況成艘。
分析錯誤
jvisualvm
jvisualvm是一個性能分析工具赏半,自從 JDK 6 Update 7 以后已經(jīng)作為 Oracle JDK 的一部分,位于 JDK 根目錄的 bin 文件夾下淆两。jvisualvm自身要在 JDK6 以上的版本上運(yùn)行断箫,但是它能夠監(jiān)控 JDK1.4 以上版本的應(yīng)用程序。
雖然jdk9之后這個工具被移除了秋冰,但是恰好當(dāng)前項目是java 8的仲义,jvisualvm依然可以使用。
具體用法:在jdk的bin目錄下啟動剑勾,或者如果環(huán)境變量已經(jīng)配好埃撵,直接在控制臺下輸入jvisualvm也可以啟動
當(dāng)然我這里不是寫怎么用這個工具,具體操作不細(xì)述虽另。有了工具就可以開工了暂刘。以下是整個分析的過程。
懷疑1:byte數(shù)組占用內(nèi)存捂刺,未及時清理
這個懷疑很簡單谣拣,因此日志報的是在讀取blob格式數(shù)據(jù)時內(nèi)存溢出。因此我第一反應(yīng)就是在當(dāng)字節(jié)數(shù)組使用完后進(jìn)行一次gc族展。
啟動應(yīng)用后java堆如下圖:
圖上可以明顯看出來成過山車式的效果森缠,運(yùn)行一段時間后再次出現(xiàn)OOM。利用jvisualvm的dump功能進(jìn)行分析仪缸,在山頂和山崖下分別dump堆信息進(jìn)行比對(忘記截圖了)發(fā)現(xiàn)大字節(jié)數(shù)組差異不明顯贵涵,顯然問題不是字節(jié)數(shù)組造成的。
懷疑2:奇技淫巧stream會不會有問題
由于喜歡用stream寫程序恰画,恰好分析這段代碼又是在forEach塊中宾茂,因此懷疑是不是這個寫法有坑。于是改成普通的循環(huán)锣尉,再次查看堆:
從統(tǒng)計學(xué)上看刻炒,這次的結(jié)果和上次沒有很大區(qū)別,出現(xiàn)了一段400M維持的情況自沧。
懷疑3:正則解析產(chǎn)生大量臨時對象
比對heap圖中波峰段和日志情況,基本上每個波峰段都是在解析文件時出現(xiàn)∧聪幔看過老夫github page的小伙伴應(yīng)該知道爱谁,老夫號稱正則小王子,所以解析文字必然使用正則孝偎。到這時候我是有點擔(dān)心的访敌,雖然我理解中正則引擎是計算密集型,但是畢竟沒讀過源碼衣盾,萬一恰好是坑咋辦寺旺?但是再細(xì)讀代碼和分析dump文件,我感覺事情沒那么簡單(當(dāng)然也有不敢相信就是這么一回事)因為所有的pattern對象我都是預(yù)編譯好的势决,在解析的時候了不起生成一些matcher對象阻塑,再新建一些String或者char數(shù)組。如果是這樣果复,那么波峰應(yīng)該是一段明顯的斜線上升陈莽,而從圖上又可以看出在解析的時候波峰呈平緩上升。故此我懷疑是在解析文件之前讀取環(huán)節(jié)出的問題虽抄。
懷疑4:pdfBox解析pdf時產(chǎn)生大量冗余
由于文件讀取速度非匙吒椋快,而jvisualvm的時間軸是秒為單位迈窟,在以毫秒為單位的程序中很難看出端倪私植,因此在文件讀取后添加Thread.sleep(2000)
方法。
結(jié)論
最終通過比對日志時間和heap時間軸發(fā)現(xiàn)是pdfBox解析文件時產(chǎn)生大量臨時對象(由于pdfBox封裝的很厲害车酣,整個解析代碼非常少兵琳,因此不會是我人為的操作導(dǎo)致的),因此把gc標(biāo)志放到文件讀取完成后骇径。再次查看heap躯肌,效果如下圖:
至此問題算是解決了。由于是上班時間解決的破衔,因此沒有截很多圖清女,中間略去通過日志和heap圖形化界面對比找到罪魁禍?zhǔn)椎牟襟E∥福看不懂的小伙伴多思考一下了哈~
整體來說這是本人第一次遇到oom并解決的過程嫡丙,還是蠻爽的,畢竟很多時候我們學(xué)習(xí)時都是只聞其聲不見其廬山真面目读第。但是真的遇到了也不需要驚慌曙博,但凡別人可以,自己不可以怜瞒,那都是細(xì)節(jié)小問題父泳;如果別人不可以般哼,自己可以,那是別人沒拷貝全(想到今天組里小姑娘CV老夫代碼時的少初始化了一個變量惠窄,折騰一下午)蒸眠;別人不行,自己也不行杆融,折騰折騰楞卡,也許就行了。