眾說周知,Java與C++同為支持面向?qū)ο蟮恼Z言,但他們對內(nèi)存的管理方式卻有很大的不同。C++開發(fā)者往往需要手動調(diào)用內(nèi)存分配函數(shù)對內(nèi)存進行分配,并在使用完這塊內(nèi)存之后,手動進行釋放。而Java開發(fā)者一般來說不需要關(guān)心內(nèi)存是如何進行分配的,只需要將精力放在業(yè)務(wù)開發(fā)上。究其原因,是因為Java擁有垃圾回收機制,旨在自動化的對內(nèi)存進行管理。筆者在這里并不想就這兩門語言的內(nèi)存管理機制進行比較,兩者的內(nèi)存管理方式各有優(yōu)劣。這里,筆者想介紹一下Java的內(nèi)存回收機制,以及Java的垃圾收集器。
在詳細(xì)介紹Java的垃圾回收機制之前,首先我們需要明確3三件事情,即:
- 哪些內(nèi)存需要回收?
- 什么時候回收?
- 如何回收?
哪些內(nèi)存需要回收?
在前面我們介紹過了Java內(nèi)存區(qū)域,在里面有簡單的涉及到一點兒內(nèi)存回收的知識,強調(diào)了對方法區(qū)和Java堆的內(nèi)存回收的必要。那么其他的內(nèi)存需要進行內(nèi)存回收嗎?準(zhǔn)確來說,對所有Java內(nèi)存區(qū)域都需要進行內(nèi)存回收。只不過,程序計數(shù)器、虛擬機棧、本地方法棧這3個區(qū)域隨線程而生,隨線程而滅,線程銷毀的時候,與之對應(yīng)的這三個區(qū)域的內(nèi)存就會被回收,整個過程都是確定的行為,其所需的內(nèi)存基本上在類結(jié)構(gòu)確定下來時就已知了。而方法區(qū)和Java堆不一樣,一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個分支需要的內(nèi)存可能不一樣,這部分內(nèi)存的大小只能在運行時才能知道,對這部分內(nèi)存的分配與回收是動態(tài)的過程,是不確定的。因此,垃圾回收器主要是關(guān)注這部分的內(nèi)存。
什么時候回收?
在上一個問題中,我們明白了Java的垃圾回收主要是針對Java堆和方法區(qū)進行回收,因此,針對這兩個區(qū)域,來討論什么時候進行內(nèi)存回收。
Java堆
判斷對象是否需要回收的方式一般有兩種:引用計數(shù)算法和可達性分析算法。
引用計數(shù)算法,故名思義,它主要根據(jù)對象被引用的情況來判斷對象是否需要回收。如果一個對象沒有被任何一個對象引用的話,則判定這個對象需要被回收。具體來說,就是在一個對象實例被創(chuàng)建的時候,對這個對象實例被引用的情況進行計數(shù),每次對象被引用,就將引用計數(shù)器加1,當(dāng)引用結(jié)束的時候,將引用計數(shù)器減1,直到引用計數(shù)器歸零,就進行回收。
引用計數(shù)法是一個很簡單的算法,易于實現(xiàn),效率也高。但是卻有一個弊端,當(dāng)出現(xiàn)兩個對象互相引用的時候,引用計數(shù)器永遠無法歸零,那么就永遠無法被回收,所以主流的Java虛擬機都沒有選擇這個算法。目前來說,主流的實現(xiàn)方式是可達性分析算法。
可達性分析算法是通過可達性分析來判定對象是否需要被回收的。這個算法的基本思想路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI引用的對象
無論是引用計數(shù)算法還是可達性分析算法,其算法都與“引用”有關(guān)。在JDK1.2之后,Java將引用分為了強引用、軟引用、弱引用和虛引用4種,這4種引用強度依次減弱:
- 強引用簡單來說就是“Object obj=new Object()”這類引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
- 軟引用是用來描述一些還有用但并非必需的對象。對于這樣的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。在JDK1.2之后,提供了SoftReference類來實現(xiàn)軟引用。
- 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。在JDK1.2之后,提供了WeakReference類來實現(xiàn)弱引用。
- 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知。在JDK1.2之后,提供了PhantomReference類來實現(xiàn)虛引用。
方法區(qū)
方法區(qū)也被稱為永久代,很多人認(rèn)為永久代是不需要被回收的,Java虛擬機規(guī)范也沒有明確要求永久代需要被回收,但就實際情況而言,對永久代的回收是一個必須的過程,盡管永久代的回收效率相對來說很低。
永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類。
回收廢棄常量的過程和回收J(rèn)ava堆中的對象非常類似。當(dāng)常量池中的常量沒有被引用的時候,該常量就被判定為廢棄常量,可以進行回收。
判定一個常量是否是“廢棄常量”比較簡單,判定一個類是否是“無用的類”就比較困難了。類需要同時滿足下面3個條件才能算是“無用的類”:
- 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例。
- 加載該類的ClassLoader已經(jīng)被回收。
- 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足上述3個條件的無用類進行回收,但是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數(shù)進行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數(shù)需要FastDebug版的虛擬機支持。
在大量使用反射、動態(tài)代理、CGLib等ByteCode框架、動態(tài)生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。
如何回收?
垃圾收集算法,主要有以下幾種:標(biāo)記-清除算法、復(fù)制算法、標(biāo)記-整理算法。
標(biāo)記-清除算法
標(biāo)記-清除算法是最基礎(chǔ)的收集算法,算法分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。之所以說它是最基礎(chǔ)的收集算法,是因為其他算法都是基于其上對其不足之處的改進。它的主要不足有兩個:一個效率問題,標(biāo)記和清除兩個過程的效率都不高;另一個是空間問題,標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。
復(fù)制算法
復(fù)制算法是用來解決標(biāo)記-清除算法的效率問題,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只能使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另一塊上面,然后再把已使用過的空間一次清理到。這樣使得每次都是對這個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況。只是這種算法的代價是將內(nèi)存縮小為原來的一半,未免有點高。
現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代,因為實際上98%的對象都是“朝生夕死”,所以根本不需要按照1:1的比例來劃分內(nèi)存空間。因此,在內(nèi)存劃分的時候,將內(nèi)存劃分一塊Eden區(qū)域和兩塊Survivor區(qū)域,Eden區(qū)域和Survivor區(qū)域的比例默認(rèn)是8:1,也就是說,每次新生代中可用內(nèi)存空間為整個新生代容量的90%(80%+10%),只有10%的內(nèi)存會被浪費,這是可以接受的。
標(biāo)記-整理算法
標(biāo)記-整理算法是為了解決復(fù)制算法在對象存活率較高時就要進行較多的復(fù)制操作,效率變低以及大量空間浪費,所以在老年代一般不能直接選用這種算法。標(biāo)記-整理算法的標(biāo)記過程與標(biāo)記-清除算法一樣,但并不馬上對內(nèi)存進行回收,而是先讓所有存活的對象都向一端移動,然后直接清理掉邊界以外的內(nèi)存,也就是把存活的對象進行了整理,減少了內(nèi)存碎片。
分代收集算法
分代收集算法說白了就是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊,比如說老年代和新生代,然后根據(jù)不同區(qū)域的特點采用不同的收集算法。一般對新生代采用復(fù)制算法,對老年代采用標(biāo)記-清理算法或者標(biāo)記-整理算法。