1 垃圾收集三件事 哪些記憶體需要回收:死去的對象需要回收 什麼時候回收 如何回收 按照jvm記憶體區域劃分原則:程式計數器、虛擬機棧、本地方法棧3個區域的記憶體隨線程創建而劃分,因此線程結束時,記憶體也自動釋放。 本章節分析的是Java堆和方法區的記憶體管理策略 1、虛擬機棧、本地方法棧,棧中的棧幀隨著方法 ...
目錄
1 垃圾收集三件事
- 哪些記憶體需要回收:死去的對象需要回收
- 什麼時候回收
- 如何回收
按照jvm記憶體區域劃分原則:程式計數器、虛擬機棧、本地方法棧3個區域的記憶體隨線程創建而劃分,因此線程結束時,記憶體也自動釋放。
本章節分析的是Java堆和方法區的記憶體管理策略
1、虛擬機棧、本地方法棧,棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。
每一個棧幀中分配多少記憶體基 本上是在類結構確定下來時就已知的(儘管在運行期會由即時編譯器進行一些優化,但在基於概念模 型的討論里,大體上可以認為是編譯期可知的),因此這幾個區域的記憶體分配和回收都具備確定性。
2、堆和方法區這兩個區域則有著很顯著的不確定性:一個介面的多個實現類需要的記憶體可能會不一樣,一個方法所執行的不同條件分支所需要的記憶體也可能不一樣,
只有處於運行期間,我們才能知道程式究竟會創建哪些對象,創建多少個對象,這部分記憶體的分配和回收是動態的。
2 對象存活判定演算法
回收堆,也就是回收對象,判斷對象是否需要回收,也就是判斷對象是否死亡,有兩種策略:引用計數演算法和可達性分析演算法
2.1 引用計數演算法
在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的對象就是不可能再被使用的
缺點:
- 當對象存在相互引用時,該判斷方法失效
- 當前較少java虛擬機應用該演算法
關於引用的說明
分類 | 定義 | 垃圾回收 |
---|---|---|
強引用 | 程式代碼之中普遍存在的引用賦值 | 不回收 |
軟引用 | 一些還有用,但非必須的對象。SoftReference修飾 | 將要發生溢出時,才會被回收 |
弱引用 | 強度比軟引用 弱一點。 WeakReference 修飾 | 垃圾收集器啟動,就會被回收,而不管是否發生溢出 |
虛引用 | 目的只是為了能在這個對象被收集器回收時收到一個系統通知 | 垃圾收集器啟動,就會被回收 【設置虛引用的目的僅是為了在對象被回收時收到一個通知】 |
2.2 可達性分析演算法
通過GC Root節點,根據引用關係向下遍歷,當存在對象不在引用連上,則該對象可能不在被引用。
註意:當前下,根節點選舉,還是需要暫停所有用戶線程,以便保證快照一致性
在Java技術體系裡面,固定可作為GC Roots的對象包括以下幾種:
- 虛擬機棧中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變數、臨時變數等
- 方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變數
- 方法區中常量引用的對象,譬如字元串常量池裡的引用
- 本地方法棧中Native方法引用的對象
- 所有被同步鎖(synchronized)持有的對象
2.2.1 不可達對象的後置處理
當對象被判斷為不可達對象後,它仍有可能不被回收:調用了finalize()方法並且在方法里調用其它存活對象。
因此,不可達對象在第一次標誌後,還會有一個執行判斷過程:
- 當對象被判定為不可達對象後,進行第一次標記。
- 對已經被標記的對象篩選出來,判斷是否需要執行finalize()方法,需要就放到執行隊列裡面。
- 在finalize(),如果產生對存活對象的引用,jvm會將該不可達對象移除待回收的集合。
過程如下所示:
關於finalize()方法
- 它的運行代價高昂,不確定性大,無法保證各個對象的調用順序,因此不推薦使用
- finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好
2.3 方法區回收判定
- 方法區的回收條件比較苛刻,成本高,《Java虛擬機規範》不要求實現方法區域的垃圾回收
- HotSpot虛擬機中的元空間或者永久代是沒有垃圾收集行為
- 方法區的垃圾收集主要回收兩部分內容:廢棄的常量和不再使用的類型
一、判斷常量是否被廢棄
- 沒有任何對象引用常量池中的這個常量
- 虛擬機中也沒有其他地方引用這個常量
二、判斷類型是否不再使用
- 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例
- 載入該類的類載入器已經被回收
- 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這裡說的僅僅是“被允許”,而並不是 和對象一樣,沒有引用了就必然會回收。
關於是否要對類型進行回收,HotSpot虛擬機提供了- Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading查看類載入和卸載信息
5 垃圾收集演算法介紹
5.1 分代收集理論
!!!重要重要重要
- 將堆記憶體按照區域劃分,存儲不同"年齡"對象。(即分為新生代和老年代);
- 新生代對象可以轉到老年代去;
- 對於新生代,回收時只關註少量需要保留的對象;
- 對於老年代,使用低頻率來進行回收;
- 由於分區的出現,促使回收可以針對特定區域進行,或者不同的區域使用不同的回收演算法。
- 對於跨代對象,在新生代建立記憶表,存儲老年代哪些區域存在跨帶引用,在回收處理時,僅處理該區域的對象;
關於收集的補充說明
部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾收集,分為:
- 老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。
- 混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集
- 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。
5.1.1 記憶集與卡表
- 記憶集的目的:解決跨帶引用帶來的可能要掃描整個老年代的問題
- 它是一個存儲在非收集區的指針集合的數據結構,元素指向收集區
- 卡表:記憶集精度到記憶體區域,稱為卡表;抽象為一個位元組數組
- 卡頁:卡表的一個元素:記憶體塊:存儲一個記憶體地址,根據指定頁碼的大小(2的N次冪位元組數),構成地址範圍;
- 一個卡頁的記憶體中通常包含不止一個對象,只要卡頁內有一個(或更多)對象的欄位存在著跨代 指針,那就將對應卡表的數組元素的值標識為1,稱為這個元素變臟(Dirty),沒有則標識為0。在垃 圾收集發生時,只要篩選出卡表中變髒的元素,就能輕易得出哪些卡頁記憶體塊中包含跨代指針,把它 們加入GC Roots中一併掃描。
5.2 標記-清除演算法
- 標記出所有待回收對象
- 對被標記對象進行清除
- 缺點:1、如果有大量待清除對象,則出現多次的標記-清除操作(我理解是既然被清除,則無需進行標記);2、產生記憶體碎片
5.3 標誌-複製演算法
- 將記憶體同等劃分2個區域,新對象都被放在其中一個區域A;
- 當該區域記憶體用完後,將活著的對象複製到另外一塊區域中B;
- 將已使用過半區進行回收清除;同時新對象只會出現在區域B中;
- 優點:不會產生記憶體碎片(存活對象被放在一起,待回收對象被整塊清除)
- 缺點:1、複製會產生開銷;2、記憶體浪費:可用記憶體只剩一半
5.4 優化後的標誌-複製演算法
- 將新生代跨分成三個區域:一大:Eden,兩小Survivor。分別占位10:8 10:1 10:1,新產生的對象隨機進入使用Eden和其中一塊正在使用的Survivor。
- 發生垃圾搜集時,將Eden和Survivor中仍然存活的對象一次性複製到另外一塊Survivor空間上,然後直接清理掉Eden和已用過的那塊Survivor
- 當Survivor不足以存放存活對象時,轉入到入老年代。如果對象經過經過18GC後,還存活,那麼也會轉入到老年代
過程如圖所示:
5.5 標記-整理演算法
- 其中的標記過程仍然與“標記-清除”演算法一樣;
- 不對可回收對象直接回收,而是讓所有存活的對象都向記憶體空間一端移動,然後直接清理掉邊界以外的記憶體
- 缺點:對象被移動,需要更新其引用,需要停止所有運行線程
過程如圖所示:
6 根節點枚舉
6.1 關於根節點及其枚舉
- 可以作為根節點的數據集中在:方法區【量池、靜態變數】、虛擬機棧:【本地變數表】
- 根節點枚舉,需要暫停用戶線程
- 另外,查找引用鏈過程已經實現了跟用戶線程併發
6.2 oopMap數據結構
oopMap是什麼?
- oopMap是一個數據結構,它存儲的內容可以作為根節點。
- jvm執行某些位元組碼指令時,會創建該數據結構
為什麼需要oopMap?
- 優點:通過oopMap,jvm可以直接找到對象的引用, jvm不需要一個不漏地檢查完所有 執行上下文和全局的引用位置,從而快速地完成根節點枚舉
- 缺點:引起OopMap內容變化的指令非常多,如果為每一條指令都生成對應的oopMap,那將會需要大量的額外存儲空間
oopMap如何實現:
- 類載入完成後,保存對象的屬性的偏移地址。【備註一】
- 即時編譯時,保存棧里對象的引用地址【備註二】
6.3 安全點
安全點是什麼?
- 編譯生成位元組碼指令時,只針對特定的指令,才創建oopMap,我們把這些特定指令的地址稱為安全點。
為什麼需要安全點?
- 如果對所有的指令都生成oopMap,會耗費大量的空間;選擇在安全點位置創建oopMap,節省記憶體空間
- 有了安全點的設定,也就決定了用戶程式執行時並非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集,而是強制要求必須執行到達安全點後才能夠暫停。
安全點如何實現?
- 安全點位置的選取在具備讓程式長時間執行的復用型指令,例如方法調用、迴圈跳轉、異常跳轉 ,只有具有這些功能的指令才會產生安全點;
- JVM使用主動式中斷方案,讓線程暫停執行,來響應GC事件。
關於主動式中斷
- 當垃圾收集需要中斷線程的時候,不直接對線程暫停,僅設置一個標誌位,各個線程執行時輪詢這個標誌,一旦發現中斷標誌為真時就自己在最近的安全點上主動中斷掛起;
另一種方案是搶先式中斷:【主流虛擬機不使用該方案】
搶先式中斷不需要線程的執行代碼 主動去配合,在垃圾收集發生時,系統首先把所有用戶線程全部中斷,如果發現有用戶線程中斷的地 方不在安全點上,就恢復這條線程執行,讓它一會再重新中斷,直到跑到安全點上
6.4 安全區域
安全區域是什麼?
- 在該代碼片中,對象引用關係不會發生變化,那麼這塊代碼(或者位元組碼指令片段)就是安全區域
為什麼需要安全區域?
- 線程沒有分配cpu時間時(處於Sleep狀或Blocked狀態)無法響應虛擬機的中斷請求,不能再走到安全的地方去中斷掛起自己,安全點的設置就不起效,因此需要安全區域。
- 在安全區域進行垃圾收集是安全的。
安全區域對線程和回收器的影響:
- 當用戶線程執行到安全區域裡面的代碼時,首先會標識自己已經進入了安全區域。
- 虛擬機要發起垃圾收集時,將不會給處於安全區域的線程打上暫停標誌【對應主動式中斷的打標識】
- 當線程要離開安全區域時,它要檢查虛擬機是否已經完成了根節點枚舉:
- 完成:線程就當作沒事發生過,繼續執行
- 未完成:線程一直等待,直到收到可以離開安全區域的信號為止。
用一張圖來描述:
6.5 oopMap、安全點、安全區域對比總結
用一張圖來總結:
7 可達性遍歷的併發分析
未完成待續