垃圾收集(Garbage Collection ,GC),是一個長久以來就被思考的問題,當(dāng)考慮GC的時候,我們必須思考3件事情:
- 哪些內(nèi)存需要回收?
- 什么時候回收?
- 如何回收?
那么在JAVA中,我們要怎么來考慮GC呢?首先回想以下內(nèi)存區(qū)域的劃分,其中程序計數(shù)器、本地方法棧、虛擬機(jī)棧三個區(qū)域隨線程而生,隨線程釋放,棧中的棧幀隨著方法的進(jìn)入和退出執(zhí)行著出棧和入棧的操作,每一個棧幀分配多少內(nèi)存基本是在類結(jié)構(gòu)確定時就已經(jīng)固定的(可能會進(jìn)行一些優(yōu)化,但是大體上已知),因此這幾個區(qū)域就不需要考慮回收的問題,因為方法結(jié)束或者線程結(jié)束時,內(nèi)存自然都被回收。不需要額外的GC算法等。
然而Java堆和方法區(qū)則不一樣,一個接口所對應(yīng)的多個實(shí)現(xiàn)類所需要的內(nèi)存可能不一樣,一個方法中的多個分支所需要的內(nèi)存也可能不一樣,我們只有在程序處于運(yùn)行期間才能知道程序需要創(chuàng)建那些對象,這部分的內(nèi)存的分配和回收是動態(tài)的,因此,垃圾收集器關(guān)注的是這方面的內(nèi)存。
一. 如何確定對象可以回收
1.引用計數(shù)算法
最容易想到與理解的算法,即對于每一個對象,每當(dāng)該對象被引用時,計數(shù)器值就+1,引用失效時,計數(shù)器就-1。因此,當(dāng)對象的引用計數(shù)為0時,即為不可再被使用的。該算法也在一些領(lǐng)域被使用來進(jìn)行內(nèi)存管理,但是JAVA虛擬機(jī)中并沒有選用該算法。主要是因為不能很好的解決循環(huán)引用的問題。
舉個簡單的例子來說明循環(huán)引用:
class Container{ public Object obj ;
}public class ReferTest { public static void main(String[] args){
Container c1 =new Container();
Container c2 =new Container();
c1.obj = c2 ;
c2.obj = c1 ;
c1 = null ;
c2 = null ; //此時c1 c1會被判定為死亡對象么? }
}
事實(shí)上會被判定為死亡對象,因為JAVA虛擬機(jī)不是采用引用計數(shù)來進(jìn)行判斷的,因此如果發(fā)生垃圾回收,c1,c2 都會被回收內(nèi)存。
2.可達(dá)性分析
Java、C#的主流實(shí)現(xiàn)都是采用該種方式,來判斷對象是否存活。
這個算法的基本思路就是一系列“GC Roots”作為起始點(diǎn),從這些節(jié)點(diǎn)向下搜索,搜索到的所有引用鏈中的對象都是可達(dá)的,其余的對象都是不可達(dá)的,如上例,即使c1,c2互相引用,但是c1,c2都不屬于GC Roots對象,因此都不可達(dá)。
Java中,以下幾種對象可以作為GC Roots:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象。
- 本地方法棧JNI方法引用的對象。
- 方法區(qū)類的靜態(tài)屬性引用的對象。
- 方法區(qū)常量引用的對象。
3.引用的分類
了解了GC Roots之后,我們可能會希望存在這么一種對象,內(nèi)存夠的時候不進(jìn)行回收,當(dāng)需要內(nèi)存時再將其回收。JDK 1.2 中對引用進(jìn)行了擴(kuò)充。將引用分為了4種,從強(qiáng)到弱依次為;
強(qiáng)引用(Strong Reference)
我們一般情況下使用的都是強(qiáng)引用,如Object o = new Object(),之類的代碼。只要強(qiáng)引用還在,垃圾收集器就永遠(yuǎn)不會回收被引用的對象。
軟引用(Soft Reference)
SoftReference類來實(shí)現(xiàn),用來描述一些還有用但是不必須的對象,在系統(tǒng)如果不回收就會發(fā)生OOM時才會對軟引用進(jìn)行內(nèi)存回收。
弱引用(Weak Reference)
WeakReference類來實(shí)現(xiàn),描述非必需的對象,強(qiáng)度弱,只能活到下一次發(fā)生垃圾回收前,無論那時內(nèi)存是否短缺,都會對軟引用對象進(jìn)行內(nèi)存回收
虛引用(Phantom Reference)
PhantomReference類實(shí)現(xiàn),不會對生存時間發(fā)生任何影響,唯一目的時能在這個對象被收集器回收時得到一個通知。
4.其他
及其不建議使用finalize()方法,雖然可以在回收時被調(diào)用,但是finalize()方法的執(zhí)行代價高昂,不確定性大,無法保證各個對象的調(diào)用順序。使用finalize()能做的工作,使用try()finally()或其他方式可以執(zhí)行的更好。大家可以忘記JAVA中有這個方法的存在。本身就是在JAVA剛誕生時向C/C++程序員做的妥協(xié),但是未得到優(yōu)化。
方法區(qū)(永久代)進(jìn)行GC的效率極低,花費(fèi)較大,但是在大量使用反射、動態(tài)代理等場景都需要虛擬機(jī)具備類卸載的功能,以保證永生代的空間。
二.垃圾收集算法
1.標(biāo)記清除算法(Mark-Sweep)
算法分為兩個階段,標(biāo)記與清除。
標(biāo)記階段:標(biāo)記出所有需要回收的對象。回收階段:將所有標(biāo)記區(qū)域回收。由于該算法不對空間進(jìn)行整理,因此會產(chǎn)生大量的內(nèi)存碎片,內(nèi)存空間碎片過多會導(dǎo)致在分配較大的對象時,因為沒有連續(xù)的內(nèi)存而不得不提前觸發(fā)一個GC。另外,標(biāo)記與清除的過程效率都不高。這也是最基礎(chǔ)的GC算法。
2.復(fù)制算法(Copying)
將內(nèi)存的總?cè)萘糠譃閮蓧K,每次只使用其中的一塊,當(dāng)這一塊用完了,觸發(fā)GC,此時將還存活的對象轉(zhuǎn)移到另一塊內(nèi)存中,之前使用的那一塊內(nèi)存完全清理掉。這樣每次對一個半?yún)^(qū)進(jìn)行回收,也不會存在內(nèi)存碎片,實(shí)現(xiàn)簡單,運(yùn)行高效,但是一次只能使用半塊內(nèi)存可能會造成浪費(fèi)。
在新生代中,絕大部分的對象時“朝生夕死”的,因此,不需要按照1:1來劃分空間。而是將內(nèi)存分為一塊較大的Eden區(qū)以及兩個Survivor區(qū),HotSpot虛擬機(jī)中,Eden:Survivor=8:1 ,每次使用一個Eden區(qū)以及一個Survivor區(qū),90%的空間,觸發(fā)GC后,將剩余的對象轉(zhuǎn)移到未使用的Survivor中,然后清理Eden區(qū)和用過的Survivor區(qū),空間不夠時,會擔(dān)保分配到老年代。這樣一次可以使用90%的內(nèi)存空間,極大的提高了內(nèi)存的使用率。因此,新生代一般采用這種算法來回收。
3.標(biāo)記整理算法(Mark-Compact)
如果回收時空間內(nèi)的對象存活率較高,那么使用復(fù)制算法一次只能使用50%的空間(以應(yīng)對所有對象都存活的情況),因此老年代采用標(biāo)記整理算法。先對需要清理的對象進(jìn)行標(biāo)記,然后將存活的對象都向一端移動,直接清理掉端邊界以外的內(nèi)存。這種方式也不會留下內(nèi)存碎片。
標(biāo)記整理算法沒有復(fù)制算法快。
三. Java垃圾收集器
(了解即可,需要時可以網(wǎng)上細(xì)查)
新生代收集器:Serial收集器、ParNew收集器(Serial的多線程版本)、Parallel Scanvenge收集器(控制吞吐量,提高相應(yīng)速度)
老年代收集器:Serial Old收集器、Parallel Old收集器、CMS收集器(最短停頓)、G1(新生代、老年代都可回收)
四. 內(nèi)存的分配與回收
新生代:即復(fù)制算法中提到的Eden區(qū)以及2個Survivor區(qū)。
老年代:新生代存活足夠長時間后進(jìn)入老年代。堆上的另一塊區(qū)域。
Minor GC:發(fā)生在新生代的垃圾收集動作。因為Java對象存活時間一般較短,故Minor GC非常頻繁,一般回收速度也較快。
Full GC:發(fā)生在老年代的垃圾收集動作,伴隨著最少一次的Minor GC,且速度較慢(比Minor GC慢10倍以上)
1.空間的分配
1)對象優(yōu)先在新生代Eden區(qū)分配。當(dāng)Eden區(qū)沒有足夠空間時,將發(fā)動一次Minor GC.
2)較大對象需要連續(xù)的空間,如長字符串或數(shù)組,如果放在新生代會提前觸發(fā)GC。故大對象直接進(jìn)入老年代區(qū)域,避免頻繁的GC。
3)長期存活的對象進(jìn)入老年代,每個對象有一個年齡,在對象頭Mark word中記錄,剛被創(chuàng)建時年齡為0,當(dāng)它活過一次Minor GC,并且轉(zhuǎn)移到Survivor中,年齡變?yōu)?,此后,在Survivor區(qū)中每活過一個Minor GC,年齡就會+1,當(dāng)年齡達(dá)到某個程度(默認(rèn)為15),就會晉升到老年代。
4)此外,為了適應(yīng)內(nèi)存的復(fù)雜情況,年齡不一定達(dá)到規(guī)定值才能進(jìn)入老年代。當(dāng)Survivor區(qū)的相同年齡所有對象大小大于Survivor區(qū)大小的一半時,此年齡就會被作為判定標(biāo)準(zhǔn),大于等于該年齡的都會進(jìn)入老年代。
2.空間的回收--GC
這里我用一張圖來徹底解釋清除:

需要解釋的地方有:擔(dān)保失敗,這個的作用在圖上已經(jīng)解釋的很清楚了,可以在JVM參數(shù)設(shè)置。
另外一個地方就是平均大小來作比較,因為有多少對象晉升到老年代是無法知道的,所以只好取之前每一次晉升到老年代的對象的容量的平均值大小來作為經(jīng)驗值,來決定是否進(jìn)行Full GC來讓老年代騰出更多空間。如果仍然失敗,那么只能進(jìn)行一次Full GC。在我個人開來,之所以使用擔(dān)保,經(jīng)驗值來盡可能的只進(jìn)行MinorGC,所有的一切,都是為了盡可能不執(zhí)行Full GC的情況下將需要申請的內(nèi)存空間搞定。