1. 概述
在Java內(nèi)存區(qū)域里講了Java的內(nèi)存運行時數(shù)據(jù)區(qū)域分為如下5個部分
- 程序計數(shù)器(Program Counter)
- 虛擬機棧(Virtual Machine Stack)
- 本地方法棧(Native Method Stack)
- 堆(Heap)
- 方法區(qū)(Method Area)
其中前三個數(shù)據(jù)區(qū)域隨著線程的啟動而創(chuàng)建涮总,終止而銷毀颜及,這三個區(qū)域的內(nèi)存回收具有確定性宝磨,不需要過多考慮回收問題。所以JVM的垃圾回收機制的注意力就集中于堆和方法區(qū)闸婴,其中對堆的GC性價比是最高的,一般可以回收70%~95%的空間馍管。
2. GC過程
首先討論的是對堆的GC船殉,在這之前我們應(yīng)該知道要進行垃圾回收的步驟應(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梭域。所以也就有了第二種方法(可達性分析)來解決這個問題。
可達性分析算法(Reachability Analysis)
把堆中所有對象當(dāng)成一幅有向圖中的所有點搅轿,對象之間的引用構(gòu)成了點與點的之間的路徑病涨。接著從一系列被稱為GC Roots(一些被引用的對象)的點出發(fā)遍歷整個圖,圖中所有可以到達的點都是存活的對象璧坟,而那些不可到達的點則為死亡對象既穆,將被GC赎懦。
可充當(dāng)GC Roots的對象有下面幾種:
- 虛擬機棧中棧幀中本地變量表中變量引用的對象
- 本地方法棧中本地的方法引用的對象
- 方法區(qū)中類靜態(tài)變量引用的對象
- 方法區(qū)中常量引用的對象
垃圾回收算法
解決完第一個問題(判斷對象的存活)后,就可以去回收這些對象占用的內(nèi)存了幻工,至于怎么回收這些內(nèi)存励两,有下面幾種算法:
標(biāo)記-清除算法(Mark-Sweep)
標(biāo)記-清除算法如同它的名字一樣,有標(biāo)記和清除兩個階段囊颅。其中的標(biāo)記階段就是上面說到的確定對象的存活階段当悔,確定了要回收的對象后就回收死亡的對象,存活的對象留在原地踢代。標(biāo)記清除算法是最基礎(chǔ)的回收算法盲憎,它有兩個缺點:
- 標(biāo)記和清除階段效率都不高
-
清除之后內(nèi)存會產(chǎn)生大量不連續(xù)的碎片,導(dǎo)致分配大內(nèi)存對象困難
復(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)存空間括眠。這樣就使得空間的利用率達到90%彪标。但如果存活的對象超過10%的話,Survivor的空間就不夠用了掷豺,這時就需要依賴?yán)夏甏M行分配擔(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)歷垃圾回收達到一定次數(shù)還存活下來的話桌粉,這些對象將進入老年代,所以老年代里的對象每次GC存活率都很高衙四。因此針對于新生代和老年代對象的不同存活率铃肯,可以分別采取不同的垃圾回收算法,對于對象存活率低的新生代采用復(fù)制算法传蹈,而對于對象存活率高的老年代采用標(biāo)記清除或標(biāo)記整理算法押逼。
以上介紹的是關(guān)于堆中的GC,下面來說下方法區(qū)的GC惦界。
方法區(qū)的GC
方法區(qū)在HotSpot虛擬機中是永久代挑格,相比于堆中的新生代和老年代,永久代進行垃圾回收的性價比更低沾歪。
方法區(qū)的垃圾回收主要回收廢棄常量和無用的類漂彤,其中常量來自于方法區(qū)的常量池,包括字面值常量和符號引用灾搏〈焱回收常量跟回收堆中對象非常類似,以字面值常量為例狂窑,如果不存在其他對象引用該字面值常量媳板,如果發(fā)生GC且有必要的話,該字面值常量會被回收泉哈。對于無用的類的判斷比較苛刻蛉幸,必須同時滿足下列三個條件:
- 該類的所以實例都被回收
- 加載該類的類加載器已經(jīng)被回收
- 該類對應(yīng)的Class對象沒有在任何地方被引用
不過也可以滿足了上面的三個條件也不進行回收,可以通過設(shè)置虛擬機參數(shù)來控制回收丛晦。
3. 內(nèi)存分配策略
對象優(yōu)先在 Eden 分配
對象優(yōu)先在新生代的 Eden 區(qū)分配奕纫,當(dāng) Eden 區(qū)空間不夠時,執(zhí)行Minor GC大對象直接進入老年代
設(shè)置 -XX:PretenureSizeThreshold 參數(shù)烫沙,大于該參數(shù)的值的對象直接在老年代分配匹层,避免在 Eden 區(qū)和 Survivor 區(qū)之間的大量內(nèi)存復(fù)制長期存活的對象進入老年代
對象頭的Mark word擁有一個存儲分代年齡字段,每經(jīng)歷一次 Minor GC 存活下來該年齡字段加1斧吐,直到該年齡超過 XX:MaxTenuringThreshold 設(shè)置的值(默認(rèn)15)又固,則移動到老年代仲器。動態(tài)對象年齡判定
若 Survivor 區(qū)中同年齡所有對象大小總和大于 Survivor 空間一半煤率,則年齡大于等于該年齡的對象可以直接進入老年代。空間分配擔(dān)保
在發(fā)生 Minor GC 之前乏冀,JVM 先檢查老年代最大可用連續(xù)空間是否大于新生代所有對象大小蝶糯,成立的話 Minor GC 確認(rèn)是安全的,則進行Minor GC辆沦;否則如果 HandlePromotionFailure 設(shè)置的值為true并且老年代最大可用連續(xù)空間大于歷次晉升到老年代對象的平均大小昼捍,則進行 Minor GC识虚,否則進行 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 及以前的永久代空間不足