垃圾回收與記憶體分配——總結篇
阿新 • • 發佈:2020-07-08
# 垃圾回收與記憶體分配
- [垃圾回收與記憶體分配](#垃圾回收與記憶體分配)
- [一些基礎](#一些基礎)
- [垃圾回收演算法](#垃圾回收演算法)
- [垃圾回收器](#垃圾回收器)
- [常見問題](#常見問題)
## 一些基礎
- 物件的四種引用型別
- 強引用,記憶體不足時報錯oom,但不會該類物件
- 弱引用,當記憶體不足時才會回收
- 軟引用,不管記憶體是否充足,在gc都會回收
- 虛引用,任何時候都可以被回收
- 怎麼判斷物件是否仍在使用?
- 引用計數法
每個物件有一個引用計數屬性,當被引用時計數+1,當引用釋放時-1。兩個物件互相迴圈使用時會導致物件無法回收
- 可達性分析——Java預設
若從GC Roots向下搜尋時,走過的路徑稱為引用鏈。當物件沒有任何引用鏈時代表已不可用。
- 可作為GC Roots的物件有哪幾類?
- 虛擬機器棧中引用的物件
- 方法區類靜態屬性引用的物件
- 方法區常量引用的物件
- 本地方法中引用的物件
- 方法區回收的必要條件
> 主要回收兩部分內容:廢棄的常量和不再使用的型別。價效比很低
- 該類所有例項都已被回收
- 該類對應的類載入器已被回收(很難)
- 無法通過反射訪問該類(該類對應的java.lang,Class沒有被引用)
- 安全點和安全區域
> HotSpot沒有為每一條指令生成OopMap,只在“特定位置”記錄了這些資訊(OopMap可理解成附加資訊,對棧內的資料進行說明),這些位置稱為安全點,執行緒在安全點可以被確定,從而確定GC Roots資訊。
- 安全點特定位置
1. 迴圈的末尾
2. 方法返回前
3. 呼叫方法的Call之後
4. 丟擲異常的位置
- 安全區域
> 安全區域是指一段程式碼片中,引用關係不會發生變化,在這個區域任何地方 GC 都是安全的,安全區域可以看做是安全點的一個擴充套件
- 執行緒中斷的方式
1. 主動式:不對執行緒操作,簡單的設定一個標記位,執行緒不斷主動輪詢這個標誌位狀態,為真時,執行緒在最近的安全點掛起
2. 被動式:系統直接把所有執行緒中斷,如發現有的執行緒不在安全點時,就恢復執行到最近的安全點(不採用)
- 併發情況下的可達性變動解決演算法(G1和cms)
- 增量空間演算法——CMS
當出現新的引用物件時,則記錄下該物件,待掃描結束後再以此為根重新掃描一次
- 原始快照演算法——G1
當出現刪除引用時,將要刪除的引用記錄下來,待掃描結束後再將該物件為根重新掃描
## 垃圾回收演算法
- 標記-清除演算法
標記需要回收的物件或不需要回收的物件,再統一清除。
缺點:
1. 效率不穩定,無論標記還是清除,效率都會隨著物件增多而降低
2. 記憶體空間碎片化
- 標記-複製演算法
把記憶體空間分為兩塊,每次只使用其中一塊,當該塊記憶體不足後,把存活的物件複製到另一塊,再把原空間一次性清除。
優點:沒有記憶體碎片。
缺點:記憶體空間利用率低;當存活物件較多時,效率變低
- 標記-整理演算法
標記完成後,將存活的物件向一端移動,清除邊界以外的內容
優點:記憶體規整,物件賦值/建立速度快
缺點:標記、清理效率不高
- 分代收集
> 分代收集是將物件按照存活時間進行分代(新生代和老年代),根據不同區域的特點結合前三種演算法進行收集
- 新生代,每次垃圾收集都有大量物件死去,選用標記-複製演算法
>**新生代複製演算法的改進**:許多新生代的物件存活時間較短,不需要按照1:1的比例進行復制演算法記憶體分配,可將記憶體分為較大的Eden區和兩塊較小的Survivor,回收時將Eden 和 Survivor 中還存活著的物件一次性地複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛才用過的 Survivor 空間
- 老年代,物件存活率較高,沒有其餘區域可以進行分配擔保,使用標記-清除、標記-整理演算法
## 垃圾回收器
- 新生代
- Serial
序列,標記複製演算法。客戶端預設新生代收集器
- ParNew
並行,標記複製演算法;只有ParNew和serial可以配合CMS使用
- Parallel Scavenge
並行,標記複製演算法;吞吐量優先收集器,可控制吞吐量(使用者程式碼執行時間/總時間)
1. 最大垃圾收集停頓時間引數[絕對值]
2. 垃圾回收時間佔總時間比率引數[相對值]
3. 自適應調節空間引數[布林值],開啟後可自動調節Eden、Survivor區域大小
- 老年代
- Serial Old
標記整理演算法,主要供客戶端模式,若用於服務端:與Parallel Scavenge搭配使用;或者作為CMS發生失敗後的與預案
- Parallel Old
標記整理演算法,Parallel Scavenge的老年版
- CMS
- CMS
> 響應時間優先,標記清除演算法。多用於B/S架構的服務端上
- 四個步驟:
1. 初始標記:STW,只標記GC Roots直接關聯到的物件
2. 併發標記:與使用者執行緒一起遍歷整個物件圖
3. 重新標記:STW,修整併發標記期間,因使用者執行緒導致引用變化的部分(增量更新演算法實現)
4. 併發清除:與使用者執行緒並行,清除已死亡的物件
- 三個缺點:
1. 對處理器資源敏感,預設啟動執行緒數(處理器核心數+3)/4
2. 產生浮動垃圾。併發標記和清除階段會與使用者執行緒一起執行,所以需要預留一部分空間供使用者執行緒使用,當空間不足時,會臨時啟用預案:使用serial old進行標記整理
3. 標記清除演算法導致記憶體碎片。大物件不夠分配時,觸發full gc耗時。可設定引數,多次清楚後,在下一次full gc前進行一次整理
- G1
> 面向整個堆空間的Region佈局形式,每個Region都可以扮演Eden、Survivor或老年代空間。允許設定收集停頓時間
- 四個步驟:
1. 初始標記:在minor gc階段只標記gc Roots引用的物件,所以不存在停頓
2. 併發標記:與使用者執行緒併發執行,標記完成後處理SATB(原始快照演算法)記錄在併發時有變動的物件
3. 最終標記:STW(短暫),處理併發結束後仍遺留的少量SATB記錄
4. 篩選回收:STW,對各Region的回收價值與成本進行排序,根據期望停頓時間指定回收計劃。將存活物件複製到空閒Region後,清除原Region
## 常見問題
- finalize() 方法什麼時候被呼叫?它的目的是什麼?
物件被釋放前被垃圾回收器呼叫,目的是釋放該物件所持有的資源(對外記憶體、長連線等),且該方法只會被呼叫一次。(不建議使用)
- 為什麼要有不同的引用型別?
由於gc回收時機不可控,且有時候需要適當的控制物件被回收的時機。比如新建 Person類,每次查詢資訊都需要重新構建例項,物件生命週期過短,引起巨大的消耗。聽過軟引用和HashMap的結合可以構建快取記憶體,提升