一、項(xiàng)目背景
????????本項(xiàng)目是基于spring boot + electron + vue 的桌面應(yīng)用程序,由于涉及USB設(shè)備通訊,因此采用c++封裝了一個(gè)USB通訊的動(dòng)態(tài)庫,由jna去調(diào)用該庫。由于是桌面應(yīng)用程序,spring boot部署在客戶機(jī)上,因此客戶機(jī)也相當(dāng)于一臺小型服務(wù)器。
二、問題出現(xiàn)
? ? ? ? 在程序第一次商用過程中,首次出現(xiàn)spring boot莫名崩潰的情況,同時(shí)伴隨著jvm報(bào)錯(cuò)文件(類似于hs_err_pid8644.log文件)的輸出。
三、分析
? ? ? ? 1、第一次分析
? ? ? ? 有報(bào)錯(cuò)文件當(dāng)然先從報(bào)錯(cuò)文件入手,但這種jvm的報(bào)錯(cuò)文件實(shí)在不知道從何看起,查閱資料后發(fā)現(xiàn),最有用的信息就在這個(gè)地方:

? ? ? ? J或j開頭的輸出標(biāo)識java層面的,c開頭表示動(dòng)態(tài)庫的。首先看java層面,確定了是從調(diào)用動(dòng)態(tài)庫的發(fā)指令接口之后,jvm崩潰,之后全是c層面的輸出。
? ? ? ? 由于在其他項(xiàng)目中經(jīng)常調(diào)用dll動(dòng)態(tài)庫,因此看到這樣的崩潰信息第一反應(yīng)當(dāng)然是調(diào)用dll過程中,c++發(fā)生了不可被java捕獲的異常(例如c++的指針越界,導(dǎo)致內(nèi)存泄漏),進(jìn)而導(dǎo)致jvm的異常退出。
? ? ? ? 帶著這樣的猜想,首先查看了代碼中會(huì)調(diào)用到dll接口的地方。
? ? ? ? 要建立與USB設(shè)備的通訊,首先需要dll初始化該設(shè)備并打開與該設(shè)備通訊的接口通道,同時(shí)獲取該通道句柄,在后續(xù)發(fā)送指令的過程中,需要帶上該句柄,當(dāng)結(jié)束與設(shè)備的通訊,則需要釋放接口并關(guān)閉設(shè)備。
? ? ? ? 在spring boot中,與設(shè)備的通訊的正常流程基本是:init(識別設(shè)備,并打開通訊接口) -> getInfo(發(fā)送指令獲取設(shè)備信息) -> syncTime(發(fā)送指令同步設(shè)備時(shí)間) -> writeInfo(發(fā)送指令,修改設(shè)備信息) -> changeMode(發(fā)送指令將設(shè)備轉(zhuǎn)為USB模式)。
? ? ? ? 本以為是否是頻繁開關(guān)設(shè)備導(dǎo)致dll的崩潰,但正常流程來看并不會(huì),反倒是沒有去close設(shè)備,于是乎在流程最后加上close。這反而增大了崩潰的幾率。因此開始懷疑是c++庫的問題。
? ? ? ? 2、第二次分析
? ? ? ? 開始閱讀c++庫代碼。重點(diǎn)關(guān)注um_usbapi_close這個(gè)接口。

? ??????usb_release_interface方法用于釋放已打開的設(shè)備通訊接口,usb_close方法關(guān)閉設(shè)備,umapi_list_del從鏈表中刪除該設(shè)備,free釋放節(jié)點(diǎn)
? ? ? ? 通過以上代碼發(fā)現(xiàn),在嘗試釋放通訊接口之后,如果釋放失敗則立刻返回結(jié)果,沒有去鏈表中刪除設(shè)備。設(shè)備接入后基本只有兩個(gè)結(jié)果,一個(gè)是設(shè)備拔出,一個(gè)是轉(zhuǎn)U盤模式,這都會(huì)使得設(shè)備斷開通訊連接,所以usb_release_interface必定是返回失敗的。usb_release_interface失敗,則不會(huì)再鏈表中刪除設(shè)備。
? ? ? ? 所以分析原因,由于鏈表沒有刪除設(shè)備信息,導(dǎo)致鏈表不斷累積,容量變大超過限制,導(dǎo)致內(nèi)存溢出。
? ? ? ? 于是將umapi_list_del等方法放在return之前,無論成功失敗都去刪除鏈表中的設(shè)備。
? ? ? ? 結(jié)果是崩潰的幾率更大了。。。。推翻重來!
? ? ? ? 3、第三次分析
? ? ? ? 開始從jvm的內(nèi)存泄漏這個(gè)方向分析。
? ? ? ? 在調(diào)試過程遇到過一個(gè)報(bào)錯(cuò):
java.lang.OutOfMemoryError: unable to create new native thread
? ? ? ? 顯而易見是OOM,且是創(chuàng)建線程時(shí)候報(bào)的OOM。這讓我很疑惑,我的jvm參數(shù)已經(jīng)給到1G了,且正在運(yùn)行的線程并不是很多,怎么就不夠內(nèi)存了?查找資料,發(fā)現(xiàn)創(chuàng)建線程不僅僅是需要jvm內(nèi)存,還需要系統(tǒng)的空余內(nèi)存。項(xiàng)目應(yīng)用場景,運(yùn)行內(nèi)存只有4G,win10系統(tǒng)已經(jīng)占了一半,開了兩個(gè)spring boot服務(wù)外加一個(gè)electron,剩余內(nèi)存空間已經(jīng)少之又少。
? ? ? ? 于是調(diào)整jvm大小,為512M。
? ? ? ? 調(diào)整后崩潰幾率小了,但依然會(huì)偶現(xiàn)。此次修改保留。
? ? ? ? 4、第四次分析
? ? ? ? 值得一提的是,之前一直以為只能但設(shè)備通訊,但經(jīng)過前面對c++代碼的研究,發(fā)現(xiàn)代碼漏洞,在open設(shè)備成功后并沒有返回,而是繼續(xù)遍歷設(shè)備并Open,這導(dǎo)致后面open到已經(jīng)打開過通訊接口的設(shè)備,于是ret又被設(shè)置為失敗了。修改后支持根據(jù)句柄與設(shè)備通訊,實(shí)現(xiàn)多設(shè)備的同時(shí)通訊,這解決了一大堆java層面的已知問題,也不再需要鎖去限制通訊了。
? ? ? ? 繼續(xù)分析,開始從jvm層面去查看。
? ? ? ? 這里用到一個(gè)jdk自帶的jvm內(nèi)存查看工具:jvisualvm,使用方式查看
http://www.itdecent.cn/p/7958eead8cc8
? ? ? ? 下載插件Visual GC,使用方式查看
https://www.cnblogs.com/reycg-blog/p/7805075.html
? ? ? ? 通過對內(nèi)存的監(jiān)控,發(fā)現(xiàn)metaspace(永久代)幾乎占滿,同時(shí)byte[]創(chuàng)建速度及數(shù)量非常的多,導(dǎo)致young gc頻繁,而當(dāng)metaspace到達(dá)一定閾值,則觸發(fā)full GC,full GC對這種桌面的實(shí)時(shí)通訊程序來說是致命的。于是查看代碼中哪里使用了大量的byte數(shù)組。
? ? ? ? 第一反應(yīng)當(dāng)然是設(shè)備通訊這一塊,設(shè)備通訊都是以byte數(shù)組進(jìn)行數(shù)據(jù)交互的,于是發(fā)現(xiàn)Usb通訊庫里有類似這樣的代碼:

? ? ? ? cmdBuf是一個(gè)byte數(shù)組,這里根據(jù)cmdBuf的傳值,來new出一個(gè)固定大小的Memory區(qū)域。Memory是jna包下的一個(gè)類,主要用于創(chuàng)建一塊內(nèi)存區(qū)域,接收c++的返回信息。
????????于是發(fā)問:Memory創(chuàng)建的內(nèi)存存放在哪里?帶著這個(gè)疑問查詢了資料,發(fā)現(xiàn):java在調(diào)用c++動(dòng)態(tài)庫的時(shí)候,動(dòng)態(tài)庫的方法接口都是存放在本地方法棧中,因此jna創(chuàng)建的Memory空間也是在本地方法棧,而本地方法棧不屬于GC范圍。
? ? ? ? 人一下子就精神起來了。每次發(fā)送一個(gè)數(shù)據(jù)包給設(shè)備,都會(huì)建立至少1KB的Memory空間,直到通訊結(jié)束都沒有去釋放,于是本地方法棧內(nèi)存不斷累積,最終觸發(fā)full GC(初步分析)。于是,處理方式是在每次完成通訊后,都將建立的Memory釋放。
? ? ? ? 到此,崩潰問題還未復(fù)現(xiàn)。
四、總結(jié)
????????這是第一次深入到j(luò)vm內(nèi)存,從內(nèi)存信息來分析問題代碼。經(jīng)過這次調(diào)試,后續(xù)在對這些內(nèi)存空間的處理上更加謹(jǐn)慎了,包括stream流的close,也要引起關(guān)注。
? ? ? ? 但還有一個(gè)問題未能解決,metaspace依然幾乎占滿。這個(gè)問題還需要繼續(xù)研究。? ? ? ?