本文將分析JVM的垃圾回收策略,哪些內存需要回收和如何回收的問題。
哪些內存需要回收
垃圾收集器在對堆進行回收前,第一件事情就是要確定這些對象之中哪些還存活著,哪些已經(jīng)死去。
引用計數(shù)算法
引用計數(shù)算法是指,在對象中添加一個引用計數(shù)器,每當有一個地方引用它時,計數(shù)器就加1;當引用失效時,計數(shù)器減1;任何時刻計數(shù)器為0的對象就是不可能再被使用的。
引用計數(shù)算法的缺陷是不能解決對象之間互相引用的問題,因此不被主流虛擬機選用。
可達性分析算法
通過一系列的稱為GC Roots的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所經(jīng)過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,這個對象就是不可用的。

在java語言中,可作為GC Roots的對象包括:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI(即一般說的Native方法)引用的對象
引用的分類
java的引用可以分為強引用、軟引用、弱引用、虛引用:
- 強引用:是指在程序代碼中直接存在的引用,類似“Object obj = new Object()”這類的引用。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。
- 軟引用:還有用但是并非必需的引用,在系統(tǒng)將要發(fā)生內存溢出異常之前會把這些對象列進回收范圍中進行二次回收,若還是沒有足夠的內存,才會拋出內存溢出異常。
- 弱引用:非必需的對象,只能生存到下一次垃圾收集發(fā)生之前。當垃圾收集器工作時,無論內存是否夠用都將回收這些對象。
-
虛引用:最弱的一種引用關系。一個對象是否有虛引用的存在完全不會對他的生存時間構成影響,也無法通過虛引用來取得一個對象實例。
垃圾收集算法
標記-清除算法
最基礎的收集算法是“標記-清除”(Mark-Sweep)算法,如同它的名字一樣,算法分為標記和清除兩個階段。
標記:首先標記所有需要回收的對象
清除:在標記完成后統(tǒng)一回收所有被標記的對象

缺點:
- 效率問題,標記和清除兩個過程的效率都不高。
- 空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內存,而不得不提前觸發(fā)另一次垃圾收集動作。
復制算法(新生代算法)
它將可用內存按容量劃分為大小相等的兩塊,每次只用其中的一塊。當這一塊內存用完之后,將還存活的對象復制到另一塊去,然后再把已使用過的內存空間一次清理掉。
優(yōu)點:每次都是對整個半?yún)^(qū)進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現(xiàn)簡單,運行高效。

缺點:代價是將內存縮小為了原來的一半,未免太高了一點。
現(xiàn)在的商用虛擬機都采用這種手機算法來回收新生代,IBM公司的專門研究表明,新生代中的對象98%是“朝生夕死”,所以并不需要按照1:1的比例來劃分內存空間。
解決方法:將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的對象一次性復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。
標記-整理算法(老年代算法)
復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。所以在老年代一般不能直接選用這種算法。根據(jù)老年代的特點,提出了“標記-整理”(Mark-Compact)算法。標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。

分代收集算法
當前商用虛擬機都采用了這種算法,根據(jù)對象的存活周期將內存劃分為幾塊,一般是把Java堆分為新生代和老生代,根據(jù)各個年代采用適當?shù)氖占惴ā?/p>
- 新生代一般采用復制算法(Copying)。
- 老生代一般采用 標記-清理(Mark-Sweep) 或者標記-整理(Mark-Compact) 進行回收。
