深入理解 Java 虛擬機:GC垃圾收集器
判斷哪些對象需要回收
Java 堆裏存放着幾乎所有的對象實例,因此在回收前需要判斷哪些對象是 “存活” 的,這些對象不需要回收,只回收已經 “死去” 的對象(即不可能再被任何途徑使用的對象)。
引用計數器算法
算法原理:
給對象添加一個引用計數器,每當有一個地方引用它時,計算器 +1;當引用失效時,計數器 -1;任何時刻計數器爲 0 的對象就是不可能再被使用的,可以被回收。
優點: 實現簡單,判斷效率高
缺點: 無法解決對象間相互引用的問題
應用: Python 語言,遊戲腳本領域使用的 Squirrel 等
可達性分析算法
算法原理:
從一系列稱爲 “GC Roots
” 的對象爲起點,沿着引用鏈
向下搜索,當一個對象到 GC Roots 沒有任何引用鏈相連,
則證明此對象可以被回收(即 GC Roots 無法到達此對象)。
GC Roots 的對象:
-
虛擬機棧(棧幀中的本地變量表)中引用的對象。
對象
在創建時候,會在堆上開闢一個空間用於分配實例,之後把堆的地址
作爲引用
存放在棧
中,在對象生命週期結束
後,引用就會從虛擬機棧中出棧
-
方法區(永久代)中類靜態屬性引用的對象。
即被static
修飾的靜態對象
,存放在方法區
中 -
方法區(永久代)中常量引用的對象。
即被static
與final
修飾的對象,存放在方法區
中 -
本地方法棧中 JNI(即一般說的 Native 方法)引用的對象。
JNI
即Java Native Interface
,主要用於幫助 Java 與 別的語言進行通信(主要 C,C++)提供接口。
缺點: GC停頓
,爲了保證一致性
,導致GC進行時必須停頓
所有的 Java 執行線程
。可以想象下,系統運行一半,突然像被人按了暫停鍵一樣突然卡住了,然後 GC 結束才繼續。
引用還有分類(瞭解)
目的: 我們希望描述這樣的一類對象,當內存空間足夠
時,則保留
在內存中;如果空間在回收後還非常的緊張
,則可以拋棄
這些對象,很多系統的緩存功能
都符合這種場景。
強引用: 程序代碼中普遍存在,類似 “Object obj = new Object()” 這類的引用,只要強引用
還在,垃圾收集器就永遠不可能回收這個對象
。
軟引用: 用來描述一些還有用但非必須的對象
。這種對象在系統將要發生內存溢出
錢,會把這些對象列進回收範圍進行第二次回收
,依然沒有足夠內存,纔會拋出內存溢出異常
。提供了 SoftReference 類
來實現軟引用
。
弱引用: 用來描述非必須對象
,強度比軟引用更弱
一些,被引用關聯的對象只能生存到下次垃圾收集之前
。提供了 WeakReference 類
來實現弱引用
。
虛引用: 也成爲幽靈引用或幻影引用,最弱的一張引用關係。這個引用不會對對象的生存時間產生影響
,也無法通過虛引用獲取一個對象實例,這個引用的唯一目的,就是被收集器回收時收到一條系統通知
。提供了 PhantomReference 類
來實現虛引用
。
“緩刑” finalize(瞭解)
即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷再次標記過程。
第一次標記並進行一次篩選。
篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋 finalize 方法
,或者 finzlize 方法已經被虛擬機調用
過,虛擬機將這兩種情況都視爲“沒有必要執行”,對象被回收。
如果對象要在 finalize() 中成功拯救自己,只要重新與引用鏈上的任何的一個對象建立關聯
即可,譬如把自己賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出“即將回收”的集合。
第二次標記,基本就是被回收了,因爲 finalize() 只會執行一次
。
不建議使用 finalize()
,原因是運行代價高昂,不確定性大,無法保證調用順序
,比如使用 try-finally
之類的方式都可以做的更好,更及時
。
開始垃圾收集
標記 - 清除算法
最基礎的收集算法(後面都是這個基礎的改進算法)
,算法分爲 “標記” 和 “清除” 兩個階段。
標記階段: Java
算法就是上面的 可達性分析算法
。
回收階段: 沒什麼特別的,就是直接把標記的回收了。
缺點:
效率問題
,標記和清楚兩個過程效率都不高。空間問題
,由於時標記後直接就回收了,導致空間會有大量不連續的內存碎片
,導致如果分配大內存
對象,無法找到足夠的連續內存而不得不提前 GC。
複製算法
爲了解決效率問題
,因此出現了稱爲 “複製”
的收集算法。
原理: 將內存分爲大小相等
的兩塊,每次只用其中一塊。當內存用完
了,把存活的對象
複製到另一塊上,然後把之前已使用的空間清理掉。
優點:
- 只對半區的內存進行回收
- 不需要考慮內存碎片情況,因爲移到另一半空閒的上面,按順序分就行了
- 實現簡單,運行高效
缺點: 內存變成原來的一半
改進:
現在商用虛擬機都採用這種算法回收新生代
,因爲 98% 的新生代都死得快(Java 裏一個方法跑完,裏面的對象基本就都沒用了)。
內存劃分:一塊較大的 Eden 空間
和 兩塊較小的 Survivor 空間
,Eden 比 Survivor 大小比例是 8 比 1。
每次使用 1 塊 Eden,1 塊 Survivor
,進行回收時候,會把 Eden 和 Survivor 上存活的對象
都複製到另一個 Survivor
上。當 另一個 Survivor
內存不足時,需要依賴其他內存(老年代)
進行分配擔保
。
缺點: 老年代都是存活時間較長的對象,因此這種算法不適合於老年代。
標記 - 整理算法
原理: 標記過程與 “標記 - 清除” 算法一樣,但是後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存
。
分代收集算法
把 Java 堆分爲新生代
和老年代
,然後依照各自的特點採用最適當的收集算法。
規則:
在新生代
中如果發現有大批量
的對象死去,只有少量存活,就採用複製算法
。
在老年代
中因爲對象存活率高且沒有額外的空間對它進行分配擔保,就必須使用 “標記 - 清理”
或 “標記 - 整理”
算法來進行回收。
HotSpot 算法
雖然標記算法用的是 可達性分析算法
,但是缺點存在 GC 停頓
問題,因此 HotSpot
在實現方面做了修改。
枚舉根節點
即尋找所有可達性分析
中的 GC Roots
。
爲了節省枚舉根節點而採用的解決方案:
爲了讓虛擬機知道哪裏存放着對象引用,即 GC Roots
,而不是把整個執行上下文和全局引用檢查一遍,HotSpot
中使用了一組稱爲 OopMap
的數據結構來達到目的。
類加載完成的時候,把對象什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用,這樣,GC在掃描時就可以直接得知這些信息了。
安全點
在 OopMap 的協助下,HotSpot 可以快速且準確
地完成 GC Roots 枚舉
,但有另一個問題,OopMap 內容變化的指令非常多,如果爲每一個條指令都生成對應的OopMap,將需要大量的額外空間
。
解決方法:
在 特定的位置
記錄信息(即 OopMap 的信息),這些位置被稱爲安全點
。
特點:
只在遇到 安全點
才能停頓
進行 GC
。
安全點既不能太少(內容就多了,GC 停頓就會變長)
,也不能過於頻繁
以至於增大運行時的負荷(頻繁 GC)
,標準就是 “是否據有讓程序長時間執行的特徵”
。
因此最明顯的特徵就是指令序列複用
,例如:方法調用、循環跳轉、異常跳轉等,纔會產生安全點
。
如何讓GC發生時,所有線程(除執行JNI調用的線程)都到最近的安全點停頓下來?
-
搶先式中斷
,不需要線程的執行代碼主動配合,在GC發生時,首先把所有線程中斷
,如果有線程中斷的地方不在安全點,就恢復線程,讓它執行到安全點
。 -
主動式中斷
,需要中斷線程時,不直接對線程操作,而是設置一個標誌
,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就中斷掛起
。輪詢標誌的地方和安全點是重合的
。
安全區域
由於線程處於 Sleep
或 Blocked
的時候,線程無法響應 JVM
的中斷請求,這時候就需要 安全區域
來解決問題。
特點:
在一段代碼片段中,引用關係不會發生變化
。在這個區域的任何地方 GC 都是安全
的,可以把安全區域
看作是安全點
的擴展。
描述:
在線程執行到安全區域代碼時,首先標識自己進入安全區域,當這段時間裏 JVM 發起 GC,不用管標識爲安全區域的線程了。在線程要離開安全區域時,要檢查系統是否已經完成了根節點枚舉,如果完成,線程繼續執行,否則等待直到收到可以安全離開安全區域的信號爲止。