從一次線上故障思考 Java 問題定位思路

問題出現(xiàn):現(xiàn)網(wǎng)CPU飆高贤徒,F(xiàn)ull GC告警

CGI 服務(wù)發(fā)布到現(xiàn)網(wǎng)后醉蚁,現(xiàn)網(wǎng)機(jī)器出現(xiàn)了Full GC告警睛琳,同時(shí)CPU飆高99%盒蟆。在優(yōu)先恢復(fù)現(xiàn)網(wǎng)服務(wù)正常后,開始著手定位Full GC的問題师骗。在現(xiàn)場(chǎng)只能夠抓到四個(gè)GC線程占用了很高的CPU茁影,無法抓到引發(fā)Full GC的線程。查看了服務(wù)故障期間的錯(cuò)誤日志丧凤,發(fā)現(xiàn)更多的是由于Full GC引起的問題服務(wù)異常日志募闲,無法確定Full GC的根源。為了查找問題的根源愿待,只能從發(fā)布本身入手去查問題浩螺,發(fā)現(xiàn)一次bugfix的提交靴患,有可能觸發(fā)一個(gè)死循環(huán)邏輯:

for(inti =1;i <= totalPage ;i++) {

String path = path_prefix +"?cmd=txt_preview&page="+ String.valueOf(i) +"&sign="+ fileSignature;

url_list.add(path);

}

循環(huán)中的參數(shù) totalPage 為 long 類型,由一個(gè)外部參數(shù)進(jìn)行賦值要出。當(dāng)外部參數(shù)非常大鸳君,超過 int 的最大值時(shí),i遞增到int的最大值后患蹂,i++ 會(huì)發(fā)生翻轉(zhuǎn)或颊,變成一個(gè)負(fù)數(shù),從而使 for 會(huì)進(jìn)入死循環(huán)传于。利用下面這段代碼可以試驗(yàn):

publicstaticvoidmain(String[] args){

longtotalPage = Long.MAX_VALUE;

for(inti =0;i

if(i<0){

System.out.println(i);

}

}

}

通過日志囱挑,發(fā)現(xiàn)外部確實(shí)傳遞了一個(gè)非常大的參數(shù):

確認(rèn)了當(dāng)命中邏輯的時(shí)候,會(huì)進(jìn)入一個(gè)死循環(huán)沼溜。在循環(huán)中不斷進(jìn)行字符串的拼接與list的Add操作平挑,很快就會(huì)耗盡JVM堆內(nèi)存導(dǎo)致Full GC。經(jīng)過測(cè)算系草,實(shí)際上并不需要死循環(huán)通熄,只要是一個(gè)比較大的循環(huán),就能夠引發(fā)Full GC找都。對(duì)totlePage的大小做了限定后唇辨,發(fā)布了新版本,沒有再出現(xiàn)Full GC的問題能耻。

現(xiàn)場(chǎng)還原:重現(xiàn)問題赏枚,探索定位思路

回顧排查問題的過程并不高效,最開始懷疑過是否是打包有問題或使用的jdk版本不對(duì)嚎京,花了較多的時(shí)間確認(rèn)打包問題嗡贺。另一方面,發(fā)布帶出的代碼較多鞍帝,通過重復(fù)review代碼無法很快鎖定問題诫睬。為了探索一種更有效的問題定位方法,我將有問題的代碼重新部署到機(jī)器上帕涌,手動(dòng)構(gòu)造請(qǐng)求觸發(fā)bug摄凡,探索定位此類問題的通用思路。

如何確定bug可以導(dǎo)致CPU飆升蚓曼?為何會(huì)引發(fā)OOM亲澡?

1) 在 Java 服務(wù)上開啟 JMX,在本地使用 VisualVm 來查看 Java 服務(wù)在運(yùn)行過程中的內(nèi)存纫版、GC床绪、線程等信息。VisualVM 是 Sun 的一個(gè) OpenJDK 項(xiàng)目,它是集成了多個(gè) JDK 命令工具的一個(gè)可視化工具癞己,它主要用來監(jiān)控 JVM 的運(yùn)行情況膀斋,可以用它來查看和瀏覽 Heap Dump、Thread Dump痹雅、內(nèi)存對(duì)象實(shí)例情況仰担、GC 執(zhí)行情況、CPU 消耗以及類的裝載情況绩社,也可以使用它來創(chuàng)建必要信息的日志摔蓝。

可以看到邏輯被命中的時(shí)候,CPU確實(shí)是升到100%的愉耙,此時(shí)也發(fā)生了Full GC告警贮尉。嘗試著多發(fā)了幾次請(qǐng)求,服務(wù)直接就掛掉了劲阎。這里有個(gè)問題是:不是已經(jīng)Full GC了嗎绘盟,為什么還會(huì)發(fā)生OOM鸠真?實(shí)際上悯仙,雖然JVM已經(jīng)開始回收內(nèi)存,但是由于對(duì)象被引用吠卷,這些內(nèi)存是回收不掉的锡垄。從GC日志可以看到回收的情況:

從GC日志中可以看到,新生代的Eden區(qū)域與老年代都已經(jīng)被占滿祭隔。如果新生代放不下對(duì)象的時(shí)候货岭,object會(huì)直接被放到老年代中。除了GC日志疾渴,也可以使用jstat命令來堆Java堆內(nèi)存的使用情況進(jìn)行統(tǒng)計(jì)展示:

jstat-gcutil12309100010

1000為統(tǒng)計(jì)的間隔千贯,單位為毫秒,10為統(tǒng)計(jì)的次數(shù)搞坝,輸出如下:

從輸出中同樣可以看到E(Eden)區(qū)與O(Old)區(qū)都已經(jīng)被占滿了搔谴。其他幾個(gè)輸出項(xiàng)的含義如下:

YGC: 從啟動(dòng)到采樣時(shí)Young Generation GC的次數(shù)

YGCT: 從啟動(dòng)到采樣時(shí)Young Generation GC所用的時(shí)間 (s).

FGC: 從啟動(dòng)到采樣時(shí)Old Generation GC的次數(shù).

FGCT: 從啟動(dòng)到采樣時(shí)Old Generation GC所用的時(shí)間 (s).

GCT: 從啟動(dòng)到采樣時(shí)GC所用的總時(shí)間 (s).

可以看到JVM一直在嘗試回收老年代姊扔,但是一直沒能將內(nèi)存回收回來绑榴。

如何獲取占用CPU最高的線程id忘嫉?

2)可以登上機(jī)器映挂,確認(rèn)下是什么線程使 CPU 飆高绵载。先ps查看 Java 進(jìn)程的 PID:

拿到進(jìn)程 pid 后宦言,可以使用 top 命令蔽氨,來看是什么線程占用了 CPU玉雾。

top-p12309 -H

-p 用于指定進(jìn)程融师,-H 用于獲取每個(gè)線程的信息右钾,從 top 輸出的內(nèi)容,可以看到有四個(gè)線程占用了非常高的 CPU:

到這里可以拿到12313、12312舀射、12311灭将、12314這四個(gè)線程id。為了確定這些是什么線程后控,需要使用 jstack 命令來查看這幾個(gè)是什么線程庙曙。

高占用CPU的是什么線程?

3) jstack 是 Java 虛擬機(jī)自帶的一種堆棧跟蹤工具浩淘,用于打印出給定的 Java 進(jìn)程 ID 或 core file 或遠(yuǎn)程調(diào)試服務(wù)的 Java 堆棧信息捌朴。使用下面命令,將 Java 進(jìn)程的堆棧信息打印到文件中:

jstack -l12309>stack.log

在線程堆棧信息中张抄,線程 id 是使用十六進(jìn)制來表示的砂蔽。將上面四個(gè)四個(gè)線程 id 轉(zhuǎn)換為16進(jìn)制,分別是0X3019署惯、0X3018左驾、0×3017、0x301A极谊。在 stack.log 中可以找到這幾個(gè)線程:

到這里可以確定的是诡右,死循環(huán)引發(fā)了Full GC,四個(gè)GC線程一直嘗試著回收內(nèi)存,這四個(gè)線程將CPU占滿轻猖。

是哪些對(duì)象占用了內(nèi)存帆吻?

4)Full GC、OOM咙边、CPU 被占滿的問題都得到了解答猜煮。那么再次遇到類似的線上問題時(shí),如何確定或者縮小問題范圍败许,找到導(dǎo)致問題的代碼呢王带?這時(shí)候需要進(jìn)一步觀察的是 Java 堆內(nèi)存的信息,查看是什么對(duì)象占用了內(nèi)存市殷°底可以使用上文提到的 VisualVM 來生成 headdump 文件:

也可以在機(jī)器上使用 jmap 命令來生成 head dump 文件。

jmap -dump:live,format=b,file=headInfo.hprof12309

live 這個(gè)參數(shù)表示我們需要抓取的是目前在生命周期內(nèi)的內(nèi)存對(duì)象被丧,也就是說 GC 收不走的對(duì)象盟戏,在這種場(chǎng)景下,我們需要的就是這些內(nèi)存的信息甥桂。生成了 hprof 文件后柿究,可以拉回到本地,使用 VisualVM 來打開它進(jìn)行分析黄选。打開后可以看到:

從信息中可以看到蝇摸,字符串 char[] 在占了內(nèi)存的73%婶肩,因此可以確定的是內(nèi)存泄漏與字符串有關(guān)。通常生成的 headdump 文件會(huì)很大貌夕,也可以使用下面的命令律歼,來查看占用內(nèi)存最多的類型:

jmap -histo12309> heap.log

輸出內(nèi)容如下:

能否對(duì)堆內(nèi)對(duì)象進(jìn)行查詢?

5) 到這里突然有個(gè)想法啡专,如果能夠分析出相似度高的字符串险毁,那么有比較大的可能是這些字符串存在泄漏,從而可以縮小問題代碼的范圍们童。確實(shí)是有這么一種工具來對(duì)堆內(nèi)的對(duì)象進(jìn)行分析畔况,也就是OQL(Object Query Language),在VisualVM中可以對(duì)headdump文件執(zhí)行對(duì)象查詢,下面是一個(gè)示例慧库,查找包含內(nèi)容最多的List:

selectmap(top(heap.objects('java.util.ArrayList'),'rhs.size - lhs.size',5),"toHtml(it)+'='+it.size")

查詢結(jié)果如下:

如何查找到相似度最高的字符串跷跪,還在繼續(xù)學(xué)習(xí)研究中。

一些疑問與總結(jié)

1)為什么無法抓到引發(fā) Full GC 的線程齐板?一個(gè)猜測(cè)是線程拋出 OOM 異常之后就被終止了吵瞻,線程只存活了很短的時(shí)間。

2)為什么對(duì) Eden 區(qū)回收后存活的對(duì)象甘磨,不會(huì)被拷貝到 survivor 區(qū)橡羞?從上面的 GC 日志可以看到,BeforeGC 與 AfterGC宽档,新生代中的兩個(gè)survivor 區(qū)(也就是from\to)一直都是0%尉姨,這里猜想可能是 survivor 區(qū)太小庵朝,沒有足夠的空間存放從 Eden 區(qū)拷貝拷貝過來的對(duì)象吗冤。同時(shí)老年代也沒有足夠的空間(已經(jīng)99%了),因此 JVM 的 GC 基本沒有什么有效的回收操作九府。

3)重現(xiàn)問題時(shí)椎瘟,在日志里發(fā)現(xiàn)了一個(gè) OOM 的錯(cuò)誤信息:

java.lang.OutOfMemoryError:GCoverheadlimitexceeded

這種情況發(fā)生的原因是, 程序基本上耗盡了所有的可用內(nèi)存, GC 也清理不了。JVM 執(zhí)行垃圾收集的時(shí)間比例太大, 有效的運(yùn)算量太小侄旬。默認(rèn)情況下, 如果GC花費(fèi)的時(shí)間超過 98%, 并且 GC 回收的內(nèi)存少于 2%, JVM 就會(huì)拋出這個(gè)錯(cuò)誤肺蔚。從這里也可以看到 GC 線程一直在嘗試回收內(nèi)存,但是回收效果實(shí)在太差儡羔,也就是第二點(diǎn)提到的宣羊。

4)當(dāng)時(shí)在線上環(huán)境出現(xiàn)問題時(shí),看到很多 log4 j的錯(cuò)誤日志信息汰蜘,是什么原因仇冯?猜測(cè)大概是寫日志的?I/O 操作要經(jīng)過內(nèi)存,而內(nèi)存已經(jīng)被使用光族操,無法進(jìn)行寫操作所導(dǎo)致苛坚。這些問題都可以進(jìn)一步研究。

對(duì)于一般的 OOM 問題,通過這幾個(gè)方面的思考泼舱,大致可以鎖定問題所在等缀,或是縮小問題可能發(fā)生的范圍。例如對(duì)某些特定類型的內(nèi)存泄漏來說娇昙,到這一步已經(jīng)可以分析出是什么類型導(dǎo)致內(nèi)存泄漏尺迂。而對(duì)本案例來說,根據(jù)排查結(jié)果可以優(yōu)先考慮的是字符串的泄露冒掌,代碼 review 中查看是否有操作字符串的地方枪狂,而不會(huì)將問題的優(yōu)先級(jí)鎖定在打包問題上。

擴(kuò)展閱讀

Java面試題66:如果查詢和定位慢查詢

排查Java線上服務(wù)故障的方法和實(shí)例分析

Spring4+Spring MVC+MyBatis整合思路

來源:https://www.cnblogs.com/QG-whz/p/9647614.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宋渔,一起剝皮案震驚了整個(gè)濱河市州疾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌皇拣,老刑警劉巖严蓖,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異氧急,居然都是意外死亡颗胡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門吩坝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來毒姨,“玉大人,你說我怎么就攤上這事钉寝』∧牛” “怎么了?”我有些...
    開封第一講書人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵嵌纲,是天一觀的道長(zhǎng)俘枫。 經(jīng)常有香客問我,道長(zhǎng)逮走,這世上最難降的妖魔是什么鸠蚪? 我笑而不...
    開封第一講書人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮师溅,結(jié)果婚禮上茅信,老公的妹妹穿的比我還像新娘。我一直安慰自己墓臭,他們只是感情好蘸鲸,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著起便,像睡著了一般棚贾。 火紅的嫁衣襯著肌膚如雪窖维。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,837評(píng)論 1 290
  • 那天妙痹,我揣著相機(jī)與錄音铸史,去河邊找鬼。 笑死怯伊,一個(gè)胖子當(dāng)著我的面吹牛琳轿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播耿芹,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼崭篡,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了吧秕?” 一聲冷哼從身側(cè)響起琉闪,我...
    開封第一講書人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎砸彬,沒想到半個(gè)月后颠毙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡砂碉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年蛀蜜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片增蹭。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡滴某,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滋迈,到底是詐尸還是另有隱情霎奢,我是刑警寧澤,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布杀怠,位于F島的核電站椰憋,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏赔退。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一证舟、第九天 我趴在偏房一處隱蔽的房頂上張望硕旗。 院中可真熱鬧,春花似錦女责、人聲如沸漆枚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墙基。三九已至软族,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間残制,已是汗流浹背立砸。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留初茶,地道東北人颗祝。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像恼布,于是被迫代替她去往敵國(guó)和親螺戳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容