原理
JVM有三種記憶體,方法區(Method Area)、java棧(java stack)和java堆記憶體(java heap)。
方法區主要儲存常數池、命名常量、static常量等,為執行緒共享
方法呼叫,引數,變數地址走java stack,為執行緒私有
物件記憶體總是在heap中分配,執行緒共享。
主要講的是JVM的堆記憶體
JVM堆記憶體分為兩塊:Permanent Space和Heap Space。
- Permanent 即 持久代(Permanent Generation),主要存放的是Java類定義資訊,與垃圾收集器要收集的Java物件關係不大。
- Heap = { Old + NEW = {Eden, from, to} },Old 即 年老代(Old Generation),New 即 年輕代(Young Generation)。年老代和年輕代的劃分對垃圾收集影響比較大。
年輕代
所有新生成的物件首先都是放在年輕代。年輕代的目標就是儘可能快速的收集掉那些生命週期短的物件。年輕代一般分3個區,1個Eden區,2個Survivor區(from 和 to)。
大部分物件在Eden區中生成。當Eden區滿時,還存活的物件將被複制到Survivor區(兩個中的一個),當一個Survivor區滿時,此區的存活物件將被複制到另外一個Survivor區,當另一個Survivor區也滿了的時候,從前一個Survivor區複製過來的並且此時還存活的物件,將可能被複制到年老代。
2個Survivor區是對稱的,沒有先後關係,所以同一個Survivor區中可能同時存在從Eden區複製過來物件,和從另一個Survivor區複製過來的物件;而複製到年老區的只有從另一個Survivor區過來的物件。而且,因為需要交換的原因,Survivor區至少有一個是空的。特殊的情況下,根據程式需要,Survivor區是可以配置為多個的(多於2個),這樣可以增加物件在年輕代中的存在時間,減少被放到年老代的可能。
針對年輕代的垃圾回收即 Young GC。
年老代
在年輕代中經歷了N次(可配置)垃圾回收後仍然存活的物件,就會被複制到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。
針對年老代的垃圾回收即 Full GC
老年代和年輕代都是隸屬Heap Space。
持久代
用於存放靜態型別資料,如 Java Class, Method 等。持久代對垃圾回收沒有顯著影響。但是有些應用可能動態生成或呼叫一些Class,例如 Hibernate CGLib 等,在這種時候往往需要設定一個比較大的持久代空間來存放這些執行過程中動態增加的型別。
所以,當一組物件生成時,記憶體申請過程如下:
- JVM會試圖為相關Java物件在年輕代的Eden區中初始化一塊記憶體區域。
- 當Eden區空間足夠時,記憶體申請結束。否則執行下一步。
- JVM試圖釋放在Eden區中所有不活躍的物件(Young GC)。釋放後若Eden空間仍然不足以放入新物件,JVM則試圖將部分Eden區中活躍物件放入Survivor區。
- Survivor區被用來作為Eden區及年老代的中間交換區域。當年老代空間足夠時,Survivor區中存活了一定次數的物件會被移到年老代。
- 當年老代空間不夠時,JVM會在年老代進行完全的垃圾回收(Full GC)。
- Full GC後,若Survivor區及年老代仍然無法存放從Eden區複製過來的物件,則會導致JVM無法在Eden區為新生成的物件申請記憶體,即出現“Out of Memory”。
OOM(“Out of Memory”)異常一般主要有如下2種原因:
開發時遇到記憶體溢位時怎麼辦?
引起記憶體溢位的常見情況:
- 記憶體載入的資料良過大,如:從百萬~千萬級的資料庫中一次性獲取大量的資料
- 集合類有大量物件的引用,使用完成後未被清空,是的jvm不能回收
- 程式碼中存在死迴圈或大迴圈重複產生物件實體
- 使用的第三方軟體或者中介軟體存在記憶體溢位bug //情況很少
- 啟動引數記憶體設定的過小 / /針對持久代
前三個主要是年老代的情況,最後一個針對持久代,第三方軟體情況很少。
記憶體溢位的解決方案
- 修改jvm啟動引數,直接增加記憶體(使用-Xms -Xmx)
- 檢查錯誤日誌,檢視OutOfMemory錯誤之前是否有其他異常或錯誤
- 對程式碼進行走查和分析,找出可能出現記憶體溢位的錯誤或地方
- 使用記憶體檢視工具動態檢視記憶體使用情況
- 檢查對資料庫查詢中,是否存在一次性取出全部資料的sql。代之以分頁機制
- 檢查程式碼是否存在死迴圈或者遞迴呼叫
- 檢查是否有大量產生新物件實體的情況
- 檢查List、Map等集合,是否存在使用完後未被清理的情況