java jvm 內存溢出(OOM) 定位與優化

Java jvm內存溢出是指應用程序在運行的過程中,由於有不斷的數據寫入到內存,到導致內存不足,進程被系統內核殺死。所在在服務程序運行的時候,要觀察一段時間的程序內存使用和分配情況。

故事原因

在一次遊戲合服的操作之後,幾個服的玩家被合併到同一個服,這個時候,玩家的數據量會猛增。突然就收到客服反應有些玩家登陸不進去了,一些在遊戲中的玩家明顯感覺到遊戲卡頓。基於這些原因,首先就是查看系統的CPU和內存使用情況:

1. 使用top命令,查看cpu和內存的使用率
2. 使用ps -ef|grep 進程名,獲取此程序的消息號

發現cpu使用率一直處於100%,內存也接近枯竭

使用命令:top -H -p 進程號

查看了一下此Java進程的所有線程,發現是gc線程一直在執行,好像進入了一個死循環:
gc線程佔滿CPU

內存RES使用了7G,而我們給JVM運行時設置的最大堆同存是6G,所以從這裏可以確定是因爲內存不足的原因導致的服務卡死或崩潰。

查找內存不足的原因

現在已經明確是因爲內存不足導致的服務器異常,那麼最主要的就是要查出是什麼數據佔用了大量的內存。
首先使用jmap命令查看當前內存的對象快照

 jmap -histo:live 進程號 > d.jmap

大於號是把查出的結果放到d.jmap文件中,文件名可以自己定義。
使用 less d.jmap命令打開統計文件,可以發現一些定義的對象實例數量很大:
大對象實例統計

從這些統計信息中可以獲得哪些對象的實例數量比較大,爲什麼這麼大這就和自己的業務有關係了,比如緩存內容太多,緩存清理速度沒有增加速度快,再比如並高比較高,有大對象的序列化,總之是在同一時刻內存中出現了衆多的大對象實例。根據分析之後,如果增加內存之後,還是不能解決問題,可以進行一波優化,然後再更新到線上服務。

觀察內存的變化

當優化好之後,更新上線上,需要觀察一些新的進程的變化。

jhsdb jmap --heap --pid 進程號
(對於jdk8之後的版本,不能再使用jmap -heap pid的命令了,需要使用上面的命令)。

可以查看到整個JVM內存的分配和使用情況

using thread-local object allocation.
Garbage-First (G1) GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 12884901888 (12288.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 2575302656 (2456.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 4194304 (4.0MB)

Heap Usage:
G1 Heap:      //整個JVM堆棧的使用情況
   regions  = 3072
   capacity = 12884901888 (12288.0MB)
   used     = 7882316288 (7517.16259765625MB)
   free     = 5002585600 (4770.83740234375MB)
   61.17482582728068% used
G1 Young Generation:   //年輕代的分配使用情況
Eden Space:
   regions  = 533
   capacity = 2541748224 (2424.0MB)
   used     = 2235564032 (2132.0MB)
   free     = 306184192 (292.0MB)
   87.95379537953795% used
Survivor Space:
   regions  = 39
   capacity = 163577856 (156.0MB)
   used     = 163577856 (156.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:    //老年代的使用情況
   regions  = 1308
   capacity = 10179575808 (9708.0MB)
   used     = 5483174400 (5229.16259765625MB)
   free     = 4696401408 (4478.83740234375MB)
   53.86446845546199% used

優化步驟一之內存分配

我們第一次是採取的增加內存的方式,把JVM的最大堆內存從6G增加到了12G,但是過段時間之後,程序又OOM崩潰了。程序重啓之後,觀察內存又快照被佔用了。查看JVM的gc情況

jstat -gc 進程號 取樣時間,例如:jstat -gc - 3333 5000 每5秒統計一個這個進程3333的gc情況。

發現很快執行了一次Full GC(FGC 的值爲1),再觀察JVM的內存使用情況,發現老年代內存分配比例比較小,因爲我們業務有很多緩存,這些對象會長期留在內存中,最終被移到老年代之中。所以老年代應該被分配更多有內存。控制年輕代和老年代的分配比較的參數是NewRatio,(jvm啓動中這樣配置: -XX:NewRatio=n)JDK10中,如果不配置的話,默認是2,表示年經代和老年代的比值是1:2,年輕代佔1/3,老年代佔2/3。我們修改爲了4,即 -XX:NewRatio=4。

業務緩存優化

這個就要根據大家的業務來自行查找和優化了,主要的思路有:

  1. 是否有緩存一直在加數據而沒有移除策略
  2. 併發量是否過高,同時有大量的大對象序列化
  3. 是否有緩存穿透,大量數據緩存中沒有,直接從數據庫加載,比如我們新合服過來的玩家數據原來都沒有緩存,都必須從數據庫加載。而玩家的數據對象又非常大,需要反序列化爲對象實例。
  4. 是否有大量線程創建

總結

這次OOM異常主要是合服之後玩家數據量猛增導致的,以前沒有發現的原因是後期的功能未再做壓力測試,玩家的數據隨着遊戲時間越來越長,量會越來越大。優化的方向主要有兩個:一是JVM配置的優化,因爲有大量的緩存,所以應該給老年代分配較多的內存,二是業務的優化,除了緩存之後,儘量減少大對象的創建,如果大部分業務必須要使用到這些大對象,可以把大對象中的這些數據拆分出來,變成小對象使用。
歡迎關注,獲取更多文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章