判斷對象是否已死
1. 引用計數演算法
- 給對象中添加一個引用計數器,每當一個地方引用它時,計數器值就加1;當引用失敗時,計數器值就減1;任何時刻計數器為0的對象就是不能再被使用的。
- 主流的Java虛擬機裡面沒有選用引用計數演算法來管理內存,其中主要原因是它很難解決對象之間相互循環引用的問題。
2. 可達性分析演算法
- 在Java內主流實現都是通過可達性分析來判斷對象是否存活。
- 基本思路:通過一系列的稱為"GC Roots"的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明這個對象是不可用的。
- Java中可以作為GC Roots的對象:
- 虛擬機棧(棧幀中的本地變數表)中引用的對象。
- 方法區中類靜態屬於引用的對象。
- 方法區中常量引用的對象。
- 本地方法棧JNI(即一般說的Native方法)引用的對象。
回收方法區
- 主要回收兩個部分:廢棄變數和無用的類。
- "無用的類":
- 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
- 載入該類的ClassLoader已經被回收。
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法再任何地方通過反射訪問該類的方法。
- 虛擬機可以對滿足上述3個條件的無用類進行回收,但是僅僅是"可以",並不是和對象一樣,不使用了就必然會回收。
垃圾收集演算法
1. 標記-清除演算法(Mark-Sweep)
- 演算法分為標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。
- 不足之處:
- 效率問題:標記和清除兩個過程的效率都不高。
- 空間問題:標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集。
2. 複製演算法(Copying)
將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象複製到另外一塊上面,然後將已經使用過的內存空間一次清理掉。
代價:將內存縮小到原來的一半。
現代商業虛擬機都使用這種收集演算法來收集新生代:
- 將內存分為一塊較大的Eden和兩塊較小的Survivor,每次使用Eden和其中一塊Survivor。
- 回收時,將Eden和剛才用過的Survivor中還存活著的對象一次性複製到另一塊Survivor空間上,最後清除掉Eden和剛才用過的Survivor空間。
- HotSpot虛擬機默認Eden和Survivor的大小比例為8:1。
- 如果另外一塊Survivor空間沒有足夠的空間存放上一次新生代收集下來的存活對象,這些對象將直接通過分配擔保機制進入老年代。
3. 標記-整理演算法(Mark-Compact)
- 過程仍然與"標記-清除"演算法一樣,但是後序步驟不是直接對可回收的對象進行清除,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界外的內存。
4. 分代收集演算法
- 當前商業虛擬機都採用分代收集。
- 一般將Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。
- 在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製演算法,只需要付出少量存活對象的複製成本就可以完成收集。
- 在老年代中,因為對象存活率高、沒有額外的空間對它進行分配擔保,就必須使用"標記-清除"或者"標記-整理"演算法來進行回收。