JVM記憶體管理調優
執行時資料區是JVM把自己管理的記憶體部分抽象出來的模型,抽象出來的不同的資料區域,以便於管理,具體有程式計數器、堆、棧、本地方法棧和方法區這幾個區域。這幾個區域有的會產生記憶體溢位的問題,在實際生產中會導致服務不可用,所以確保機器的魯棒性,JVM調優是不可忽視的一環。
物件回收判斷
在進行JVM調優之前,我們要先對物件回收判斷和垃圾回收方式有所瞭解,才能針對他們的特點考慮如何進行回收。如何判斷物件是否應該被回收呢?有兩種方式引用計數法和可達性分析,通常我們採用可達性分析來進行回收操作。
引用計數法
引用計數法非常簡單,如果有引用指向了這個物件則把這個物件計數加一,如果一個物件的計數為0說明這個物件沒有引用可以回收。但是這種方法存在明顯的問題,1.無法判斷四種不同型別的引用情況;2.如果發生物件之間的迴圈引用無法清理,導致記憶體洩漏。
可達性分析
可達性分析就是從一系列的ROOT物件出發,能夠達到的物件我們就認為是不應該被回收的,其他物件無論是成環還是其他形狀的物件我們都會進行回收操作。ROOT包括所有的棧上的物件以及在方法區中的靜態變數能夠聯絡到的堆中的物件。
垃圾回收演算法
在實際應用中,新生代的虛擬機器運用複製演算法,把空間分成8比1比1的三個部分,每次用其中的9塊,把剩餘的存貨物件放入1塊中,因為新生代的物件普遍壽命短。在老年代中因為壽命比較長久,使用標記整理演算法。新生代和老年代的回收相比,新生代比老年代快10倍,兩者觸發回收都是在空間(新生代是Eden)滿了的情況下。
標記-清除
標記清除指現現記憶體堆區域標記一遍,發現所有需要回收的資料,將其打上標記,然後從頭開始清理垃圾。這樣的問題在於,有可能清理之後空間中有很多不連續的可用空間,碎片化的情況比較嚴重。這種演算法的優勢是簡單。
複製演算法
將堆空間分成兩邊,每次只用其中的一部分,需要進行清理的時候,將所有存活下來的資料放到另外一塊區域上,之後統一把這半邊的資料全部刷掉。這種方法的好處在於清理簡單,劣勢在於會造成空間的浪費,每次只用一半。
標記-整理
標記整理在標記清除的基礎上進行了優化,先把標記後存活的物件都放在堆空間的一側,之後從結尾刷掉其他所有資料。
OOM異常與調優
OOM堆溢位
如果我們在堆上建立了一些不是很大的物件,但是建立數量很多就有可能造成堆溢位。這個時候我們通常可以設定引數HeapDumpOnOutOfMemory來列印溢位瞬間的堆空間快照。一般的情況下堆溢位分為兩類,一類是堆空間溢位,空間不夠大,我們可以設定引數增大的大小和自動擴充套件範圍。還有一種是記憶體洩漏導致空間比實際的空間小,比如ThreadLocal配合執行緒池的情況,這種情況下我們通過工具進行可達性分析,分析出有問題的部分想辦法進行回收即可。
可能因為程式會產生非常大量的小物件,比如hashmap百萬個,這些資料都不能回收,這樣會出發很多次GC,但是效果都不好,而且預設的情況下我們使用複製演算法進行操作,這樣就會導致大量的複製操作,既然物件不符合“朝生夕死”的特點,我們也沒必要使用複製演算法了,這裡推薦去掉兩個小空間,然後直接將新生代的放到老年代裡面去。
如果老年代空間滿了可能是有很多大物件,比如DOM分析出來的大檔案全部載入到記憶體的時候就會放在老年代,這個時候我們可以預先對老年代擴容,免得老年代會不斷的自動擴充套件。
SOF棧溢位
棧溢位是我們在一個執行緒之中呼叫的方法過多導致的,這種情況一般是遞迴的邏輯有問題造成的,我們在測試的時候就能測試出這種錯誤。除此之外一般不會發生StackOverflow的問題。
OOM棧溢位
棧還可能因為擴充套件的時候記憶體不夠而溢位,這種情況比較特殊,是執行緒過多,而且每個執行緒中的呼叫鏈特別長的時候可能出現。此時為了解決這個問題,我們可以採用降低棧深度的方法,因為每個棧的深度降低了,總體可用的棧個數就能增加了。
OOM方法區溢位
方法區在1.8中已經不存在了,因為隨著CGLib,groovy,JSP熱部署等技術的出現,動態修改位元組碼生成類和類載入器替換生成類的操作越來越多,如果我們將方法區放在堆中已經難以滿足現狀的類數量,把方法區放本地記憶體中是一個不錯的選擇。新特性也不能神奇地消除類和類載入器導致的記憶體洩漏。
OOM直接記憶體溢位
直接記憶體溢位是我們在使用NIO的時候聲明瞭一塊區域,但這塊區域的大小使得整個直接記憶體部分放不下,從而OOM。一般Thread Dump檔案不大,但是其中有NIO操作的話可能是直接記憶體溢位導致的。直接記憶體溢位是NIO的底層通過Unsafe類來分配空間,分配的大小超過限額導致的。