一、确定被回收的對象
1. 引用計數(Reference Counting)
給對象添加一個引用計數器,當該對象被其他對象引用時,計數器加一;引用失效時計數器減一;引用數為0的對象就是需要被回收的對象。
這樣的算法實作簡單,但也存在弊端,比如有兩個對象互相引用,但卻沒有其他任何地方引用它們,它們應當被視為“垃圾”,但他們的引用計數器并不為0;
2.可達性分析(Reachability Analysis)
選擇一系列對象作為起始點(GC Roots),從這些節點開始進行DFS,搜尋過的路徑被稱為引用鍊,若某個對象未處在任何引用鍊中,即該對象不可達,那麼就認為這個對象是可被回收的;
GC Root可以是:
- 虛拟機棧中引用的對象,即函數内局部變量所引用的對象;
- 方法區中類靜态屬性引用的對象;
- 方法區中常量引用的對象;
- 本地方法棧中引用的對象;
3. 對象的finalize方法
當一個對象在某一次可達性分析中,被标記為不可達,那麼它的finalize方法就可能被調用,前提是:
- 該對象的finalize方法被覆寫;
- 該對象的finalize方法為被執行過;
若滿足以上兩個條件,該對象會被加入到F-Queue中,由一個Finalizer線程執行;
虛拟機并不保證隊列中的對象的finalize方法一定會被執行,等到第二次标記到來時,仍然被标記為不可達的對象将會在不遠的将來被回收;
不過在項目代碼中,finalizer最好不要被重寫;
4.方法區的回收
方法區的垃圾回收主要包括:廢棄常量和無用的類;
廢棄常量顧名思義是指沒有被用到的常量,即目前程序中沒有任何一個對象引用了該常量;
判定一個類是垃圾就比較麻煩:
- 首先,堆上不存在任何該類的執行個體;
- 該類的classloader被回收;
- 類對應的對象沒有被引用,無法通過反射調用該類的方法;
二、垃圾回收算法
1. 标記-清除(Mark-Sweep)
首先标記出所有需要被回收的對象,然後統一回收;
它的不足在于效率較低且會産生大量的記憶體碎片;
2. 複制算法(Copying)
将記憶體分為兩塊,每次隻使用其中一塊,進行回收時,将仍存活的對象複制到另半塊記憶體中,然後清理已使用的半塊記憶體;
優點是效率高且沒有碎片,缺點是浪費記憶體;
複制算法常被用于新生代的記憶體回收。由于在實際場景中,新生代的對象大多會在GC中被回收,因而備用記憶體可以小一些;實際的做法是将記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和一塊Survivor,GC時将存活對象複制到另一塊Survivor空間中;Eden與Survivor的比例為8:1,浪費的空間為10%,尚可接受;當Survivor空間不夠用時,可以使用老年代空間進行配置設定擔保;
3. 标記-整理(Mark-Compact)
将所有存活對象移到空間的一端,然後将區域外的空間清理;
4.分代收集
新生代對象“朝生夕死”,使用複制算法;老年代對象存活率高,使用标記-整理或标記清除;
三、保證GC的效率
1.使用OopMap找到GC Root
OopMap是一個記錄了對象引用在Java堆棧中的位置的資料結構;
在GC時,GC線程就不需要掃描整個方法區,可以友善的知道每個對象的位置,進而快速完成根節點枚舉;
2.安全點(Safe Point)
程式運作期間導緻引用變化的指令非常多,每次變化都生成新的OopMap顯然是不現實的;事實上,隻有當線程運作到安全點(Safepoint)時才停下來開始生成OopMap、進行GC;
安全點的線程同步,即讓所有線程同時停在最近的安全點:
- 搶先式中斷:GC發生時,讓所有線程暫停,如果暫停的地方不在安全點上,就恢複線程,讓它停到最近的安全點上;
- 主動式中斷:設定一個中斷标志,線程執行時不斷地輪詢這個标志,若發現标志為真則把自己中斷挂起,标志設定在安全點上;
實際一般用主動式中斷;
3.安全區域(Safe Region)
安全點的不足在于,若線程處于Blocked或Waiting狀态,那麼該線程就無法響應JVM的中斷請求,停到安全點上;
安全區域(Safe Region)被用來解決這個問題。安全區域是引用關系不會發生變化的一段代碼片段,在這個區域的任意位置開始GC都是安全的。當線程離開Safe Region之前,需要檢查系統是否已經完成了GC,若否,則需要等待完成。Safe Region相當于拓寬了安全點,如果說線程在安全區域内Blocked那麼GC還是能正常進行的。
參考
- 《深入了解Java虛拟機(第2版)》 周志明
- What does Oop Maps means in Hotspot VM exactly