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

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