1. 概述
在Java內(nèi)存區(qū)域里講了Java的內(nèi)存運(yùn)行時數(shù)據(jù)區(qū)域分為如下5個部分
- 程序計數(shù)器(Program Counter)
- 虛擬機(jī)棧(Virtual Machine Stack)
- 本地方法棧(Native Method Stack)
- 堆(Heap)
- 方法區(qū)(Method Area)
其中前三個數(shù)據(jù)區(qū)域隨著線程的啟動而創(chuàng)建,終止而銷毀,這三個區(qū)域的內(nèi)存回收具有確定性,不需要過多考慮回收問題。所以JVM的垃圾回收機(jī)制的注意力就集中于堆和方法區(qū),其中對堆的GC性價比是最高的,一般可以回收70%~95%的空間。
2. GC過程
首先討論的是對堆的GC,在這之前我們應(yīng)該知道要進(jìn)行垃圾回收的步驟應(yīng)該是
- 知道哪些對象需要回收?
- 用什么方式去回收?
判斷對象的存活
針對第一個問題我們得確定堆中對象的“存活”,一個對象的“存活”其實就是能否通過任何途徑使用該對象,下面通過一段Code看下就明白:
public class Main{
public static void main(String[] args){
A a = new A();
a = null;
}
}
在這段Code里面,一開始創(chuàng)建一個A類型的對象,變量a持有這個對象的引用,接著a被賦值為null后。從此就無法通過任何變量來使用這個對象了,那么這個對象也就是所謂的“死亡”了,而GC的 就是這些對象。接下來有兩種方法可以找出堆中存活和死亡的對象。
引用計數(shù)法(Reference Counting)
給每一個對象添加一個引用計數(shù)器,每當(dāng)對象被引用,就對該對象的引用計數(shù)器加一,當(dāng)引用失效時引用計數(shù)器就減一。直到對象的引用計數(shù)器為0時該對象就是已死亡,可被GC。這種方法看起來簡單高效,但JVM卻沒有使用它來判斷對象的存活,原因是它很難解決對象之間相互引用的問題。還是來一段Code看下:
public class Main{
public static void main(String[] args){
A a = new A();
B b = new B();
a.ref = b;
b.ref = a;
a = null;
b = null;
}
}
在這段Code中,a和b兩個引用最后都null,也就是無法通過它們來使用一開始創(chuàng)建的兩個對象,雖然這樣它們卻無法回收,原因是創(chuàng)建的兩個對象相互引用導(dǎo)致兩個對象的引用計數(shù)器都不為0。所以也就有了第二種方法(可達(dá)性分析)來解決這個問題。
可達(dá)性分析算法(Reachability Analysis)
把堆中所有對象當(dāng)成一幅有向圖中的所有點(diǎn),對象之間的引用構(gòu)成了點(diǎn)與點(diǎn)的之間的路徑。接著從一系列被稱為GC Roots(一些被引用的對象)的點(diǎn)出發(fā)遍歷整個圖,圖中所有可以到達(dá)的點(diǎn)都是存活的對象,而那些不可到達(dá)的點(diǎn)則為死亡對象,將被GC。
可充當(dāng)GC Roots的對象有下面幾種:
- 虛擬機(jī)棧中棧幀中本地變量表中變量引用的對象
- 本地方法棧中本地的方法引用的對象
- 方法區(qū)中類靜態(tài)變量引用的對象
- 方法區(qū)中常量引用的對象
垃圾回收算法
解決完第一個問題(判斷對象的存活)后,就可以去回收這些對象占用的內(nèi)存了,至于怎么回收這些內(nèi)存,有下面幾種算法:
標(biāo)記-清除算法(Mark-Sweep)
標(biāo)記-清除算法如同它的名字一樣,有標(biāo)記和清除兩個階段。其中的標(biāo)記階段就是上面說到的確定對象的存活階段,確定了要回收的對象后就回收死亡的對象,存活的對象留在原地。標(biāo)記清除算法是最基礎(chǔ)的回收算法,它有兩個缺點(diǎn):
- 標(biāo)記和清除階段效率都不高
-
清除之后內(nèi)存會產(chǎn)生大量不連續(xù)的碎片,導(dǎo)致分配大內(nèi)存對象困難
標(biāo)記清除算法
復(fù)制算法(Copying)
復(fù)制算法將內(nèi)存分為大小相等的兩塊,每次只使用一塊,待這塊內(nèi)存用完,將這一塊上存活的對象復(fù)制到另一塊上,再把存在垃圾對象的那一塊占用的內(nèi)存一次清掉。這樣做效率高的原因是存活的對象遠(yuǎn)遠(yuǎn)少于死亡的對象,從而只需復(fù)制少量的存活對象。

復(fù)制算法解決了標(biāo)記-清除算法的清除階段效率低的問題和碎片問題但卻使可用內(nèi)存減少一半。其實有個辦法可以解決這個問題:
IBM公司的專業(yè)研究表明新生代中的對象98%是“朝生夕死”的,所以并不用按照1:1來劃分空間,而是將內(nèi)存分為3塊。一塊80%大小的Eden空間和兩塊10%大小的Survivor空間,每次使用一塊Eden和一塊Survivor,當(dāng)需要回收時,將使用中的Eden和Survivor上的存活對象復(fù)制到另一塊Survivor上,最后直接清理使用過的Eden和Survivor的內(nèi)存空間。這樣就使得空間的利用率達(dá)到90%。但如果存活的對象超過10%的話,Survivor的空間就不夠用了,這時就需要依賴?yán)夏甏M(jìn)行分配擔(dān)保。
標(biāo)記整理算法(Mark-Compact)
相比于復(fù)制算法,標(biāo)記整理算法使用與適用于老年代這種對象存活率高的區(qū)域。標(biāo)記整理和標(biāo)記清除很相似,前面的標(biāo)記步驟都一樣,不一樣在標(biāo)記整理在清除前多做了整理步驟讓存活的對象向一端移動,最后在清除掉端邊界以外的內(nèi)存。

分代收集算法(Generational Collection)
因為現(xiàn)在的商用JVM的垃圾回收都采用分代收集算法,所以一般把堆內(nèi)存劃分為新生代和老年代。剛創(chuàng)建的對象存在于新生代中,當(dāng)有一些對象經(jīng)歷垃圾回收達(dá)到一定次數(shù)還存活下來的話,這些對象將進(jìn)入老年代,所以老年代里的對象每次GC存活率都很高。因此針對于新生代和老年代對象的不同存活率,可以分別采取不同的垃圾回收算法,對于對象存活率低的新生代采用復(fù)制算法,而對于對象存活率高的老年代采用標(biāo)記清除或標(biāo)記整理算法。
以上介紹的是關(guān)于堆中的GC,下面來說下方法區(qū)的GC。
方法區(qū)的GC
方法區(qū)在HotSpot虛擬機(jī)中是永久代,相比于堆中的新生代和老年代,永久代進(jìn)行垃圾回收的性價比更低。
方法區(qū)的垃圾回收主要回收廢棄常量和無用的類,其中常量來自于方法區(qū)的常量池,包括字面值常量和符號引用?;厥粘A扛厥斩阎袑ο蠓浅n愃?,以字面值常量為例,如果不存在其他對象引用該字面值常量,如果發(fā)生GC且有必要的話,該字面值常量會被回收。對于無用的類的判斷比較苛刻,必須同時滿足下列三個條件:
- 該類的所以實例都被回收
- 加載該類的類加載器已經(jīng)被回收
- 該類對應(yīng)的Class對象沒有在任何地方被引用
不過也可以滿足了上面的三個條件也不進(jìn)行回收,可以通過設(shè)置虛擬機(jī)參數(shù)來控制回收。
3. 內(nèi)存分配策略
對象優(yōu)先在 Eden 分配
對象優(yōu)先在新生代的 Eden 區(qū)分配,當(dāng) Eden 區(qū)空間不夠時,執(zhí)行Minor GC大對象直接進(jìn)入老年代
設(shè)置 -XX:PretenureSizeThreshold 參數(shù),大于該參數(shù)的值的對象直接在老年代分配,避免在 Eden 區(qū)和 Survivor 區(qū)之間的大量內(nèi)存復(fù)制長期存活的對象進(jìn)入老年代
對象頭的Mark word擁有一個存儲分代年齡字段,每經(jīng)歷一次 Minor GC 存活下來該年齡字段加1,直到該年齡超過 XX:MaxTenuringThreshold 設(shè)置的值(默認(rèn)15),則移動到老年代。動態(tài)對象年齡判定
若 Survivor 區(qū)中同年齡所有對象大小總和大于 Survivor 空間一半,則年齡大于等于該年齡的對象可以直接進(jìn)入老年代。空間分配擔(dān)保
在發(fā)生 Minor GC 之前,JVM 先檢查老年代最大可用連續(xù)空間是否大于新生代所有對象大小,成立的話 Minor GC 確認(rèn)是安全的,則進(jìn)行Minor GC;否則如果 HandlePromotionFailure 設(shè)置的值為true并且老年代最大可用連續(xù)空間大于歷次晉升到老年代對象的平均大小,則進(jìn)行 Minor GC,否則進(jìn)行 Full GC。
4. Minor GC 與 Full GC
觸發(fā)條件
- Minor GC:當(dāng) Eden 區(qū)空間滿時,就將觸發(fā) Minor GC
- Full GC:
- 調(diào)用 System.gc() 大多情況下回觸發(fā)Full GC,通過 -XX:+ DisableExplicitGC 來禁止 RMI 調(diào)用System.gc()。
- 老年代空間不足
- 空間分配擔(dān)保失敗
- JDK 1.7 及以前的永久代空間不足
