Java jvm內存溢出是指應用程序在運行的過程中,由於有不斷的數據寫入到內存,到導致內存不足,進程被系統內核殺死。所在在服務程序運行的時候,要觀察一段時間的程序內存使用和分配情況。
故事原因
在一次遊戲合服的操作之後,幾個服的玩家被合併到同一個服,這個時候,玩家的數據量會猛增。突然就收到客服反應有些玩家登陸不進去了,一些在遊戲中的玩家明顯感覺到遊戲卡頓。基於這些原因,首先就是查看系統的CPU和內存使用情況:
1. 使用top命令,查看cpu和內存的使用率
2. 使用ps -ef|grep 進程名,獲取此程序的消息號
發現cpu使用率一直處於100%,內存也接近枯竭
使用命令:top -H -p 進程號
查看了一下此Java進程的所有線程,發現是gc線程一直在執行,好像進入了一個死循環:
內存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。
業務緩存優化
這個就要根據大家的業務來自行查找和優化了,主要的思路有:
- 是否有緩存一直在加數據而沒有移除策略
- 併發量是否過高,同時有大量的大對象序列化
- 是否有緩存穿透,大量數據緩存中沒有,直接從數據庫加載,比如我們新合服過來的玩家數據原來都沒有緩存,都必須從數據庫加載。而玩家的數據對象又非常大,需要反序列化爲對象實例。
- 是否有大量線程創建
總結
這次OOM異常主要是合服之後玩家數據量猛增導致的,以前沒有發現的原因是後期的功能未再做壓力測試,玩家的數據隨着遊戲時間越來越長,量會越來越大。優化的方向主要有兩個:一是JVM配置的優化,因爲有大量的緩存,所以應該給老年代分配較多的內存,二是業務的優化,除了緩存之後,儘量減少大對象的創建,如果大部分業務必須要使用到這些大對象,可以把大對象中的這些數據拆分出來,變成小對象使用。