記一次OutOfMemoryError: Metaspace診斷

起因

  • 線上新運行的一個微服務,啓動過程到穩定之後,服務一致沒有問題,但是在運行一小段時間之後,經常服務狀態不可用,訪問改服務的Restful接口處於503狀態

初步分析

  • 剛開始的時候並沒有意識到這個問題的嚴重性,以爲是服務器突發的問題 ,採取的手段 就是暴力重啓,可是重啓之後一小段時間內確實沒問題,但是之後又服務不可用了
  • 通常 情況下,HTTP 503的狀態碼標示當前的服務響應有問題,難道是總線的通信 有問題?觀察 其他在該節點上的服務狀態之後發現,他們都響應的很及時,這個服務是怎麼了,撲街了?
  • 這個時候的直覺告訴我,這個服務肯定是運行一段時間之後崩掉了,但是log裏邊爲什麼沒有報OOM的 錯誤呢?查看了下服務的啓動參數,發現沒有顯示的打印內存溢出日誌,二話不說在啓動參數裏邊配置下-XX:+HeapDumpOnOutOfMemoryError,別說,還真發現了異常日誌:Java OutOfMemoryError: Metaspace

Metaspace異常

  • 拿到了異常堆棧用MAT分析工具統計了一下,發現報告中分析內存 可能泄露的地方都是在調用URLClassLoader和Sysytem的ClassLoader,不太像是程序引起的內存泄露呀
    【異常情況下的內存快照】
    異常堆棧
    【服務啓動什麼都不做情況下的內存快照】
    正常啓動
  • 通過Jmap dump 服務正常啓動之後的內存快照發現,異常場景下的快照文件中的內存並沒有增加多少呀,爲什麼會引起內存溢出呢?
  • 雖然還是不清楚問題產生的原因,爲了分析此問題,決定把啓動參數的metaspace空間調大一些,再來看看正常執行結束之後dump的內存快照到底是什麼樣的
    【XX:MaxMetaspaceSize擴大一倍之後的快照】
    調大之後dump的 快照
  • 拿到Metaspace空間擴容之後的內存快照文件之後瞬間變得不淡定了,納尼,怎麼分析的結論還是ClassLoader的問題?而且前後感覺只增加了10M呀,啓動參數XX:MaxMetaspaceSize設置的是64M,遠還沒有到達這個峯值呀?瞬間不淡定了。
  • 冷靜下來之後分析了下這個現象,可以肯定的是,元空間(MetaspaceSize)的數據肯定發生過增加,肯定有一部分的class或者常量老化到這裏,但是爲什麼這個空間增長的不多,卻還是內存溢出了呢?莫非是發生過GC,回收掉了?但是存在矛盾呀,MetaspaceSize的數據都基本是class一些數據,還有靜態常量文件,不可能會有顯著的GC效果呀?

Jconsole監控內存變化

  • 爲了搞定這個問題,決定要監控一下JVM的內存使用情況,這讓我想到了用Jconsle來看一下整個運行過程中的元空間內存變化過程,服務啓動添加如下配置參數,Jconsole連接對應的IP和端口號進行連接
JAVA_OPTS="$JAVA_OPTS -Djava.rmi.server.hostname=10.10.10.111.1 -Dcom.sun.management.jmxremote"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=12345"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.local.only=false"
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.ssl=false"
  • 這一連上,不得了,我的個乖乖,服務啓動正常之後的內存就跑到快60M了,而且執行執行代碼代碼邏輯之後,元空間數據又
    老化了15M左右  在這裏插入圖片描述
  • 從內存監控來看 ,看來我們內存溢出確實是因爲原來分配的最大元數據空間內存64M是不夠用的,因爲啓動穩定之後都接近60M了,程序在執行過程中再老化一些數據,肯定會導致內存溢出的,這也就說明了開頭的那個問題。
  • 如果規避這個問題,我們之前調大這個參數就解決了這個問題,但是現實生產環境對硬件資源的要求肯定是有限度的呀,爲什麼啓動就需要這麼大的內存?執行任務過程中爲什麼會老化一些數據?對於這個問題還是要刨根問底、。
  • 首先,在服務運行過程中,我們看到上圖對應的Metaspace空間增長是線性的,並沒有短時間內急劇增加內存,說明這個可能是正常現象,而且我們通過MAT分析了啓動穩定之後和執行程序之後的對象佔用,並沒有發現有某一種類型的對象發生過陡峭增長。
  • 【啓動穩定後Class分佈】
    在這裏插入圖片描述
    *【執行程序之後的Class分佈】
    在這裏插入圖片描述

優化手段

  • 在查閱資料過程中發現,JVM 有這麼一個參數,-XX:MaxMetasaceFreeRatio=N ,當進行過Metaspace GC之後, 會計算當前Metaspace的空閒空間比,如果空閒比大於這個參數,那麼虛擬機會釋放Metaspace的部分空間,我設置的參數爲50,爲了驗證 他的GC情況,在啓動參數中添加了-XX:+PrintGCDetails -Xloggc:…/logs/gc.log -XX:+PrintGCTimeStamps 日誌,但是GC日誌並沒有正常數據,MaxMetasace的空間也沒有看到明顯的減少
  • 由於這部分空間主要放的是Class的信息,標準的甲骨文/ Sun VM旁觀世界是:類是永遠的。所以一旦加載,即使沒有人在意,他們也會留在記憶中。我們瞭解到JVM 還有個配置參數-XX:+CMSClassUnloadingEnabled,他可以掃描MaxMetasace區,卸載掉不在使用的類,配置這個參數之後並沒有發現較大的提升,但是可以防止重複創建class而導致的內存泄露

降低啓動時MaxMetasace內存

  • 就JVM 類的加載機制而言,在加載階段,虛擬機需要完成以下三件事情:
  • 1. 通過一個類的全限定名來獲取其定義的二進制字節流。
    
    2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
    
    3.  在Java堆中生成一個代表這個類的java.lang.Class對象,作爲對方法區中這些數據的訪問入口。
    
  • 就第一條而言,二進制字節流並不只是單純地從Class文件中獲取,比如它還可以從Jar包中獲取、從網絡中獲取(最典型的應用便是Applet)、由其他文件生成(JSP應用)等。
  • 因此我們懷疑除了服務包上的class正常佔用MaxMetasace空間外,很有 可能是我們引用的jar包加載了很多的class
  • 於是排查服務中引用比較大的jar包,出掉多餘的加載項,對於那些不能去掉的,按照最小粒度去加載,經過調試之後發現服務啓動之後的Matspace空間成功的減少了近20M!

遺留問題

  • 這裏這個問題 雖然解決了,但是還有一個遺留問題,爲什麼 明明Metaspace佔用的空間都超過60M了,dump的 快照只有30多M?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章