JVM學習筆記 JVM內存管理和JVM垃圾回收
JVM內存組成結構
JVM內存結構由堆、棧、本地方法棧、方法區等部分組成,結構圖如下所示:
1)堆
所有通過new創建的對象的內存都在堆中分配,其大小可以通過-Xmx和-Xms來控制。堆被划分為新生代和舊生代,新生代又被進一步划分為Eden和Survivor區,最后Survivor由FromSpace和ToSpace組成,結構圖如下所示:
新生代。新建的對象都是用新生代分配內存,Eden空間不足的時候,會把存活的對象轉移到Survivor中,新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制Eden和Survivor的比例舊生代。用於存放新生代中經過多次垃圾回收仍然存活的對象
2)棧
每個線程執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包括局部變量區和操作數棧,用於存放此次方法調用過程中的臨時變量、參數和中間結果
3)本地方法棧
用於支持native方法的執行,存儲了每個native方法調用的狀態
4)方法區
存放了要加載的類信息、靜態變量、final類型的常量、屬性和方法信息。JVM用持久代(PermanetGeneration)來存放方法區,可通過-XX:PermSize和-XX:MaxPermSize來指定最小值和最大值。介紹完了JVM內存組成結構,下面我們再來看一下JVM垃圾回收機制。
JVM垃圾回收機制
JVM分別對新生代和舊生代采用不同的垃圾回收機制
新生代的GC:
新生代通常存活時間較短,因此基於Copying算法來進行回收,所謂Copying算法就是掃描出存活的對象,並復制到一塊新的完全未使用的空間中,對應於新生代,就是在Eden和FromSpace或ToSpace之間copy。新生代采用空閑指針的方式來控制GC觸發,指針保持最后一個分配的對象在新生代區間的位置,當有新的對象要分配內存時,用於檢查空間是否足夠,不夠就觸發GC。當連續分配對象時,對象會逐漸從eden到survivor,最后到舊生代,
用javavisualVM來查看,能明顯觀察到新生代滿了后,會把對象轉移到舊生代,然后清空繼續裝載,當舊生代也滿了后,就會報outofmemory的異常,如下圖所示:
在執行機制上JVM提供了串行GC(SerialGC)、並行回收GC(ParallelScavenge)和並行GC(ParNew)
1)串行GC
在整個掃描和復制過程采用單線程的方式來進行,適用於單CPU、新生代空間較小及對暫停時間要求不是非常高的應用上,是client級別默認的GC方式,可以通過-XX:+UseSerialGC來強制指定
2)並行回收GC
在整個掃描和復制過程采用多線程的方式來進行,適用於多CPU、對暫停時間要求較短的應用上,是server級別默認采用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定線程數
3)並行GC
與舊生代的並發GC配合使用
舊生代的GC:
舊生代與新生代不同,對象存活的時間比較長,比較穩定,因此采用標記(Mark)算法來進行回收,所謂標記就是掃描出存活的對象,然后再進行回收未被標記的對象,回收后對用空出的空間要么進行合並,要么標記出來便於下次進行分配,總之就是要減少內存碎片帶來的效率損耗。在執行機制上JVM提供了串行GC(SerialMSC)、並行GC(parallelMSC)和並發GC(CMS),具體算法細節還有待進一步深入研究。
以上各種GC機制是需要組合使用的,指定方式由下表所示:

以下是新生代構成的再次說明:
新生代的構成
為了更好地理解GC,我們現在來學習新生代,新生代是用來保存那些第一次被創建的對象,他可以被分為三個空間
- 一個伊甸園空間(Eden )
- 兩個幸存者空間(Survivor )
一共有三個空間,其中包含兩個幸存者空間。每個空間的執行順序如下:
- 絕大多數剛剛被創建的對象會存放在伊甸園空間。
- 在伊甸園空間執行了第一次GC之后,存活的對象被移動到其中一個幸存者空間。
- 此后,在伊甸園空間執行GC之后,存活的對象會被堆積在同一個幸存者空間。
- 當一個幸存者空間飽和,還在存活的對象會被移動到另一個幸存者空間。之后會清空已經飽和的那個幸存者空間。
- 在以上的步驟中重復幾次依然存活的對象,就會被移動到老年代。
如果你仔細觀察這些步驟就會發現,其中一個幸存者空間必須保持是空的。如果兩個幸存者空間都有數據,或者兩個空間都是空的,那一定標志着你的系統出現了某種錯誤。
典型的垃圾收集算法
在確定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是開始進行垃圾回收,但是這里面涉及到一個問題是:如何高效地進行垃圾回收。由於Java虛擬機規范並沒有對如何實現垃圾收集器做出明確的規定,因此各個廠商的虛擬機可以采用不同的方式來實現垃圾收集器,所以在此只討論幾種常見的垃圾收集算法的核心思想。
1.Mark-Sweep(標記-清除)算法
這是最基礎的垃圾回收算法,之所以說它是最基礎的是因為它最容易實現,思想也是最簡單的。標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所占用的空間。具體過程如下圖所示:

從圖中可以很容易看出標記-清除算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多可能會導致后續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。
2.Copying(復制)算法
為了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象復制到另外一塊上面,然后再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。具體過程如下圖所示:

這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。
很顯然,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那么Copying算法的效率將會大大降低。
3.Mark-Compact(標記-整理)算法
為了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之后,它不是直接清理可回收對象,而是將存活對象都向一端移動,然后清理掉端邊界以外的內存。具體過程如下圖所示:

4.Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存划分為若干個不同的區域。一般情況下將堆區划分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據不同代的特點采取最適合的收集算法。
目前大部分垃圾收集器對於新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要復制的操作次數較少,但是實際中並不是按照1:1的比例來划分新生代的空間的,一般來說是將新生代划分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象復制到另一塊Survivor空間中,然后清理掉Eden和剛才使用過的Survivor空間。
而由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。
注意,在堆區之外還有一個代就是永久代(Permanet Generation),它用來存儲class類、常量、方法描述等。對永久代的回收主要回收兩部分內容:廢棄常量和無用的類。
檢查那些對象會被回收最重要的一個方法:
根搜索算法
由於引用計數算法的缺陷,所以JVM一般會采用一種新的算法,叫做根搜索算法。它的處理方式就是,
設立若干種根對象,當任何一個根對象到某一個對象均不可達時,則認為這個對象是可以被回收的。

就拿上圖來說,ObjectD和ObjectE是互相關聯的,但是由於GC roots到這兩個對象不可達,
所以最終D和E還是會被當做GC的對象,上圖若是采用引用計數法,則A-E五個對象都不會被回收。
說到GC roots(GC根),在JAVA語言中,可以當做GC roots的對象有以下幾種:
1、虛擬機棧中的引用的對象。
2、方法區中的類靜態屬性引用的對象。
3、方法區中的常量引用的對象。
4、本地方法棧中JNI的引用的對象。
第一和第四種都是指的方法的本地變量表,第二種表達的意思比較清晰,第三種主要指的是聲明為final的常量值
