本文將分析JVM的垃圾回收策略,哪些內存需要回收和如何回收的問題。
哪些內存需要回收
垃圾收集器在對堆進行回收前,第一件事情就是要確定這些對象之中哪些還存活着,哪些已經死去。
引用計數算法
引用計數算法是指,在對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器減1;任何時刻計數器爲0的對象就是不可能再被使用的。
引用計數算法的缺陷是不能解決對象之間互相引用的問題,因此不被主流虛擬機選用。
可達性分析算法
通過一系列的稱爲GC Roots的對象作爲起始點,從這些節點開始向下搜索,搜索所經過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,這個對象就是不可用的。
在java語言中,可作爲GC Roots的對象包括:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(即一般說的Native方法)引用的對象
引用的分類
java的引用可以分爲強引用、軟引用、弱引用、虛引用:
- 強引用:是指在程序代碼中直接存在的引用,類似“Object obj = new Object()”這類的引用。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。
- 軟引用:還有用但是並非必需的引用,在系統將要發生內存溢出異常之前會把這些對象列進回收範圍中進行二次回收,若還是沒有足夠的內存,纔會拋出內存溢出異常。
- 弱引用:非必需的對象,只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論內存是否夠用都將回收這些對象。
-
虛引用:最弱的一種引用關係。一個對象是否有虛引用的存在完全不會對他的生存時間構成影響,也無法通過虛引用來取得一個對象實例。
垃圾收集算法
標記-清除算法
最基礎的收集算法是“標記-清除”(Mark-Sweep)算法,如同它的名字一樣,算法分爲標記和清除兩個階段。
標記:首先標記所有需要回收的對象
清除:在標記完成後統一回收所有被標記的對象
缺點:
- 效率問題,標記和清除兩個過程的效率都不高。
- 空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存,而不得不提前觸發另一次垃圾收集動作。
複製算法(新生代算法)
它將可用內存按容量劃分爲大小相等的兩塊,每次只用其中的一塊。當這一塊內存用完之後,將還存活的對象複製到另一塊去,然後再把已使用過的內存空間一次清理掉。
優點:每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
缺點:代價是將內存縮小爲了原來的一半,未免太高了一點。
現在的商用虛擬機都採用這種手機算法來回收新生代,IBM公司的專門研究表明,新生代中的對象98%是“朝生夕死”,所以並不需要按照1:1的比例來劃分內存空間。
解決方法:將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的對象一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。
標記-整理算法(老年代算法)
複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。所以在老年代一般不能直接選用這種算法。根據老年代的特點,提出了“標記-整理”(Mark-Compact)算法。標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。
分代收集算法
當前商用虛擬機都採用了這種算法,根據對象的存活週期將內存劃分爲幾塊,一般是把Java堆分爲新生代和老生代,根據各個年代採用適當的收集算法。
- 新生代一般採用複製算法(Copying)。
- 老生代一般採用 標記-清理(Mark-Sweep) 或者標記-整理(Mark-Compact) 進行回收。