前言:建議先了解JVM的記憶體結構才能對垃圾回收有更深的理解,可以移步JVM記憶體結構
我們都知道:java最大的特點就是實作自動記憶體管理(自動分配物件,自動垃圾回收),接下來我們就看看它是怎么回收垃圾的,
一.垃圾回收相關演算法垃圾回收主要有兩個階段: 標記階段 清除階段
標記階段:該階段主要為了判斷物件是否存活
- 物件存活:有指標指向物件(物件還有可用的價值)
- 物件銷毀:沒有指標指向物件(物件沒有可用的價值)
1.參考計數演算法
- 對每一個物件內部保存一個整數的參考屬性,記錄物件被參考的次數情況, 當物件被任何一個變數參考,次數就+1,當參考失效,次數-1,當次數為0,就表示該物件可以被回收
- 優點: 實作簡單,判斷效率高,回收沒有延遲性
- 缺點:
- 需要給物件增加額外的空間開銷
- 無法處理回圈參考(致命的缺點,導致java沒有使用該演算法)

- 但是python使用了該演算法,看看它是如何解決這個缺點
- 手動解除參考,在合適的時候,程式員自己手動處理回收
- 使用弱參考,weakref是python專門提供用來解決回圈參考的
2.可達性分析演算法(根搜索演算法,追蹤性演算法)
- 它是從GC Roots開始,從上到下根據參考判斷是否能鏈接到目標物件, 可以達到目標物件,就不是垃圾;達不到的物件就是垃圾,等待回收
- 相對于參考計數演算法,執行效率就沒那么高,但是主要可以解決回圈參考的問題
哪些元素可以當作為"GC Roots"? (面試題)
- 虛擬機堆疊中的參考:區域變數,方法引數等
- 本地方法堆疊中的參考
- 靜態屬性的參考:static
- 常量的參考:static final
- 同步監視器synchronized 持有的鎖物件
- 臨時性加入的參考: 比如分代收集中,只針對于java堆中某一個區域進行回收,該區域的物件也有可能被別的區域的物件的屬性參考,對于該區域來說,別的區域的物件的參考也可以作為"GC Roots", (比如只對新生代進行回收,但是新生代的一些物件被老年代參考,那么老年代的物件也可以作為GC Roots)

清除階段:
1.標記-清除(Mark-Sweep)演算法
- 對堆記憶體從頭到尾進行線性的遍歷,發現某個物件在其Header中沒有標記為可達物件,進行回收
- 優點: 常見,基礎,容易想到
- 缺點: 執行效率不高 會產生記憶體碎片,需要維護一個空閑串列
- 不是真的置空,就是把物件的地址放在一個空閑串列中,這時物件實際還在記憶體中, 只有下次有新的物件進來占用空間時,從空閑串列中找到空閑的地址,直接覆寫原來的資料,
2.復制演算法
- 背景:就是為了解決標記-清除演算法效率低的問題
- 將堆記憶體分為兩塊,每次只使用一塊,在垃圾回收時,將存活的物件復制到未被使用的記憶體塊中,,并進行整理(放到一端),然后將使用中記憶體塊中的所有物件都進行清除,重復此程序,完成回收
- 優點:執行高效,保證復制過去之后空間的連續性,不會出現記憶體碎片
- 缺點:需要兩倍的空間
- 特別的: 如果系統的垃圾物件很多,復制演算法很理想,因為復制演算法需要復制的存活物件不多,效率就快,它適合于存活物件少,垃圾物件多的前提下,所以適用于新生代
- 應用場景: 新生代的survivor0區和survivor1

3.標記-壓縮(Mark-Compact)演算法
- 背景:就是對標記-清除演算法的改進,主要為了解決記憶體碎片的問題,適用于老年代
- 將堆空間中所有物件壓縮到堆記憶體的一端,按順序排放,之后,清除邊界外所有的空間
- 優點: (解決了其他兩個演算法的缺陷)
- 對比標記-清除演算法,不會產生記憶體碎片
- 對比復制演算法,消除了記憶體減半的高額代價
- 缺點: 從效率上看,低于其他兩大演算法 (對比標記-清除演算法,還得增加整理階段)

- 無,沒有最好的演算法,只有最適合的演算法
綜合性演算法
1.分代收集演算法
- 讓不同生命周期的物件采用不同的收集方式,以便提高回收效率
- 比如:根據新生代和老年代的特點,分別采用不同的演算法
- 年輕代:生命周期短,存活率低,回收頻繁,就采用復制演算法
- 老年代:生命周期長,存活率高,回收不頻繁,就采用標記-清除或者標記-整理
2.增量收集演算法
- 垃圾回識訓產生STW,增量收集演算法就是每次讓垃圾回收只回收一小片區域的記憶體空間,那么就讓應用執行緒和垃圾回收執行緒交替執行,盡可能減少暫停時間,
- 缺點:因為執行緒來回切換,會使得垃圾回收的總成本上升,造成吞吐量下降
3.磁區演算法
- 也是為了減少暫停時間,將堆空間分割為多個小塊(region),每個region單獨回收,單獨使用, 主要是說G1回收器
二.垃圾回收相關概念
物件的finalization機制
- 垃圾回收器發現物件沒有參考指向的時候,就準備回收,但是回收之前,會呼叫物件的finalize()
- 所有物件都有該方法,它允許在子類中進行重寫,一般用于物件回收之前的資源釋放, 比如:關閉檔案,套接字和資料庫鏈接等
注意:
- 永遠不要主動呼叫該方法,交給垃圾回收器去呼叫
- 該方法只能被執行一次
- 可觸及:物件是可達的
- 可恢復:沒有參考指向該物件,準備被回收,但是也有可能在finalize()中被復活 (可救)
- 不可觸及:物件的finalize()被呼叫過,沒有恢復的可能,直接回收 (不可救)
- 經歷兩次標記
- 物件不是可達的,進行第一次標記
- 進行篩選,判斷物件是否有必要執行finalize()方法
- 如果物件的finalize()已經被呼叫過,或者根本沒有重寫finalze(),沒有必要執行,直接判定為不可觸及
- 如果物件重寫了finalize()并且沒有執行過,就會將該物件放入一個佇列中,虛擬機自動創建的,優先級低的Finalizer執行緒就會執行該方法,
- 稍后GC會對上述佇列進行第二次標記,如果發現又有參考指向物件,就被移出回收集合中,如果還是沒有參考指向,直接判定為不可觸及
System.gc()的理解
- 通過代碼呼叫System.gc()會"顯示觸發Full GC"
- 然而System.gc()還附帶一個免責宣告,無法保證每一次呼叫都一定會觸發Full GC, 可能就是性能測驗的時候用一用
記憶體溢位與記憶體泄露 記憶體溢位:
- 就是記憶體空間不夠,報OOM,但是隨著GC的一直發展,一般情況下不會出現OOM,除非是應用程式占用的記憶體增長速度非常快,垃圾回收的已經跟不上記憶體消耗的速度,
- 造成OOM的原因:
- java虛擬機的堆記憶體設定不夠
- 代碼中創建了大量的大物件,并且長時間不能被垃圾收集器收集(存在被參考)
- 特別的:一般在報OOM之前就會觸發Full GC ,但是有一些情況下也可能不觸發, 比如一些超大物件.類似一個超大陣列超過堆的最大值,JVM可以判斷垃圾收集也不能解決這個問題,就直接報OOM
記憶體泄露:
- 嚴格意義上來說,只有物件不會被參考,但是GC又不能回收他們的情況,才叫記憶體泄露,但是寬泛意義上:雖然經過可達性演算法驗證后,該物件還是被連著的,但是該物件已經不需要了,或者說沒有存在的意義了,也成為記憶體泄露
- 記憶體泄露是可能導致OOM,不是一定會導致OOM
- 記憶體泄露的例子:
- 單例模式
- 單例的生命周期和應用程式是一樣長的,所以在單例程式中,如果持有對外部物件的參考的話,,那么這個外部物件是不能被回收的,則會導致記憶體泄露
- 一些提供close的資源未關閉
- 資料庫連接(connecion),網路連接(socket),io連接等,這些都需要手動關閉,否則不能回收
- 單例模式
- 注意:列舉回圈參考不合適,因為回圈參考是在參考計數演算法中才會出現, 而java是采用可達性演算法,根本不會出現回圈參考
圖示記憶體泄漏:

Stop The World
- 當垃圾開始收集的時候,用戶執行緒暫停
- 具體就是當GC進行可達性演算法分析的時候,用戶執行緒暫停
- 任何的GC,都會發生STW,只能說盡可能縮短暫停時間
垃圾回收器中的并行與并發
- 串行:暫停其他的執行緒(主要說其他的垃圾回收執行緒),只執行它自己的執行緒.
- 并行:在自己的執行緒執行的程序中,其他的垃圾回收執行緒也執行 (以上都是說在垃圾執行緒進行的時候,用戶執行緒處于STW)
- 并發:在一段時間內,自己的垃圾回收執行緒執行的程序中,用戶執行緒也在執行(范范的理解)
- 注意:這里的并行是:并行的是多個GC執行緒,而不是并行用戶執行緒和GC執行緒, 可以這樣理解:回收垃圾一定要讓當前用戶執行緒暫停,因為得判斷啊,就像收拾房間的時候,你也不要再制造垃圾

安全點與安全區域
安全點:
- 程式執行并不是在所有的地方都可以停下來進行GC,只有在一些特定的位置才能停下來, 這些位置就稱為安全點,
- 一般安全點選擇在具有讓程式長時間執行的特征的位置上, 比如:方法的呼叫,回圈跳轉和例外跳轉等,
兩種方式:
- 搶先式中斷:要發生GC了,來,所有的執行緒都停啊,看看自己是不在安全點:
- 在,呆在原地別動,
- 不在,你這個執行緒繼續往前跑,跑到你的安全點再停
- 主動式中斷:設定一個中斷標志,所有的執行緒到達自己的安全點后,都看一下中斷標志,(jvm采用的機制)
- 如果中斷標志亮了,就中斷,
- 沒亮,繼續走, 如果一些執行緒還沒到安全點,就繼續跑,直到安全點才能判斷是否要中斷,
安全區域:
- 剛才所有的程式都可以跑到最近的安全點,然后判斷是否安全標志亮了,但是有一些程式, 處于休眠/阻塞狀態,雖然知道GC要來了,但是沒辦法繼續跑到下一個安全點,
- 咋辦呢?就提出安全區域的概念,就是當程式處于不執行的時候(就是休眠/阻塞狀態),就也當做是安全的,可以進行中斷,
java的參考 面試題:強軟弱虛參考有什么區別?具體的應用場景是什么?
- 強參考:不夠也不回收,我們寫的99%都是強參考,比如: String s = new String("小猴子"); 會導致記憶體溢位
- 軟參考:記憶體不夠就回收,記憶體夠不回收,用于快取
- 弱參考:發現就回收
- 虛參考:追蹤物件回收資訊(主要就是當虛參考物件被回收的時候,會把虛參考放在一個參考佇列中,可以從佇列中看到物件回收的資訊)
Object obj =new Object(); obj = null; //消除強應用 SoftReference<Object> sf =new SoftReference<Object>(obj); //實作軟參考 WeakReference<User> uwr = new WeakReference<User>(new User(1,"張三")); //實作弱參考
面試題:開發中使用過WeakHashMap嗎?
- WeakHashMap,用來存盤鍵值對(k-v), 但是它是軟參考,即垃圾回收器執行的時候,就會回收該值,從而消除map中的資料
- 比較適合做本地,堆內快取的存盤機制,快取的失效依賴于GC的行為
寄語:這個時代,認知升級遠比積累知識重要
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/424921.html
標籤:其他
上一篇:Spring運算式語言
