1. 程式人生 > >一張圖看懂JVM之垃圾回收算法詳解

一張圖看懂JVM之垃圾回收算法詳解

mem gc roots 怎樣 src 操作系統 相關 大小 通過 實現

技術分享圖片

導讀

在之前的內容中,我們通過一張圖的方式(圖??),從總體上對JVM的結構特別是內存結構有了比較清晰的認識,雖然在JDK1.8+的版本中,JVM內存管理結構有了一定的優化調整。主要是方法區(持久代)取消變成了直接使用元數據區(直接內存)

的方式,但是整體上JVM的結構並沒有大改,特別是我們最為關心的堆內存管理方式並沒有在JDK1.8+的版本中有什麽變化,所以圖中的結構整體上是沒有什麽不準確的,之所以將方法區以及持久代標註出來,主要還是為了起到對比認識的作用,大家知道就可以了。

關於持久代元數據區的使用問題,目前可以理解就是使用的物理內存,理論上是不受JVM自動內存回收機制管理的,如果不設置參數大小默認最大使用限制就是操作系統可用物理內存的大小,設置了-XX:MetaspaceSize參數的話,JVM就會在使用物理內存空間時自己進行限制。

至於直接內存與物理內存到底是不是一回事,我認為對於我們理解上沒有區別,只是概念的區別,另外就是對這塊內存使用細節上的區別,如果不受JVM的自動回收管理,那麽怎麽管理呢?說到底還是JVM本身在直接使用物理內存或者說是直接內存(用時直接“malloc”

物理內存區域,而不再是JVM進程啟動時初始化的內存區域),還有一種概念叫native memory,說實話我暫時還不理解他們到底有啥區別,如果大家對這些概念有更好的認識,也可以給我留言哦!之所以對這幾個問題做一些筆墨的說明,主要是在之前的文章中大家對此提出了疑問,所以正好在這節的內容中進行下闡述。

回到今天的主題,我們知道JAVA最大的優點就是可以實現自動內存管理,這極大的便利了JAVA程序員,降低了使用成本。但這也使得平時我們在使用JAVA編程時不太關註JVM到底是怎樣進行內存回收的,只有在需要實際對JVM進行系統性能調優,這裏的場景可能是在系統面臨極致性能優化要求時,我們才發現需要對JAVA的整體內存結構以及內存回收機制要有一定的認識和了解才行。

在??的圖中,我們也大致對整個垃圾回收系統進行了標註,這裏主要涉及回收策略回收算法、垃圾回收器這幾個部分。形象一點表述,就是JVM需要知道那些內存可以被回收,要有一套識別機制,在知道那些內存可以回收以後具體采用什麽樣的回收方式,這就需要設計一些回收算法,而具體的垃圾回收器就是根據不同內存區域的使用特點,采用相應地回收策略和算法的具體實現了。

在??圖中,我們也標註了不同垃圾回收器所適用的特定內存區域,對於JVM垃圾回收這塊的優化,就是我們需要在了解這些垃圾回收算法、垃圾回收器特點後能夠根據自己應用的場景選擇合適的垃圾收集器,以及各區域垃圾收集器的搭配關系。下面我們就從這幾個方面給大家介紹,JVM的垃圾回收相關的知識點。

回收策略

我們知道,JVM進行內存回收的主要目的是為了回收不再使用的內存,因為在進行JAVA程序編寫時,我們只有new的操作,而不需要收工釋放不再使用的空間,如果這些空閑內存不能及時被回收,很快我們的JVM內存空間就會泄露(新申請內存空間的操作失敗,導致程序報錯),所以回收不再使用的內存的目的則是為了及時釋放空間,騰籠換鳥,以防止內存泄漏

那麽問題來了,JAVA程序申請了那麽多的內存空間,那些內存才能被認定是不再使用的內存呢?搞錯了,如果把正在被程序使用的內存給釋放了,程序邏輯就空指針異常了!

我們知道在JVM中內存分配的基本粒度主要是對象、基本類型。而基本類型的使用主要是包括在對象中的局部變量,所以回收對象所占用的內存是JAVA垃圾回收的主要目標。

那麽如何判斷對象是處於可回收狀態的呢?在主流的JVM中是采用“可達性分析算法”來進行判斷的。

這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,並從這些節點開始往下進行搜索,搜索走過的路徑我們稱之為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,我們就稱之為對象引用不可達,則證明這個對象是不可用的,就可以暫時判定這個對象為可回收對象。示意圖如下:

技術分享圖片

在圖中雖然Obj F與Obj J之間互相有關聯但是它們到GC Roots是不可達的,所以將會被判定為可回收對象。既然如此,什麽樣的對象可以作為GC Roots對象呢?

在JAVA中可以被作為GC Roots的對象主要是:虛擬機棧-棧幀中的本地變量表所引用的對象、方法區(<JDK1.8)中類靜態屬性所引用的對象/常量屬性所引用的對象、本地方法棧中引用的對象。

這裏還需要註意一個小的細節,就是被判定為對象不可達的對象也並非會被立刻回收,在學習JAVA語法是我們應該學習過finalize()方法,如果對象重寫了finalize方法,並重新把this關鍵字賦值給了某個類變量或對象的成員變量的話,該對象就會被"救活",具體過程可參考上圖所示,只是這種方式並不鼓勵大家使用,了解下就行。

在關於如何判定對象是否屬於不再使用的內存時,還有個通常會被大家錯誤認為是JVM使用的方式-“引用計數法”,事實上引用計數法的實現比較簡單,判定效率也比較高,在Python語言中就使用了這種算法進行內存管理,但是它有一個比較難解決的對象之間循環引用的問題,所以在JAVA虛擬機裏並沒有選用“引用計數法”來管理內存。這個問題很多人都會搞錯,包括有很多年開發經驗的程序員,需要大家註意下!

回收算法                                                                

在JVM中主要的垃圾收集算法有:標記-清除、標記-清除-壓縮(簡稱“標記-整理”)、標記-復制-清除(簡稱“復制”、分代收集算法。這幾種收集算法互相配合,針對不同的內存區域采取對應的收集算法實現(這裏具體是由相應的垃圾收集器實現)

下面我們就分別來看下這幾種收集算法的特點:

1)、標記-清除

標記-清除算法是最為基礎的一種收集算法,算法分為:“標記”和“清除”兩個階段。首先標記出所有需要回收的對象(標記的過程就是上面介紹過的根節點可達算法),在標記完後統一回收所有被標記對象占用的內存空間。

示意圖如下:

技術分享圖片

技術分享圖片

這種收集算法的優點是簡單直接,不會影響JVM進程的正常運行。而其缺點也是非常明顯,首先,這樣的回收方式會產生大量不連續的內存碎片,不利於後續連續內存的分配;其次,這種方式的效率也不高。

2)、標記-復制-清除

這種算法的思路是將可用的內存空間按容量劃分為大小相等的兩塊,每次只使用其中一塊。當這一塊使用完了,就將還存活著的對象復制到另外一塊上面(移動堆頂指針,按順序分配內存),然後再把已使用過的內存空間一次清理掉。

示意圖如下:

技術分享圖片

技術分享圖片

這種收集方式比較好的解決了效率和內存碎片的問題,但是會浪費掉一般的內存空間。目前此種算法主要用於新生代回收(文頂的圖中有標註)。

因為新生代的中98%的對象都是很快就需要被回收的對象,這一點大家在編程時可以體會到,所以並不需要1:1的比例來劃分內存空間,在新生代中JVM是按照“8:1:1”的比例(文頂圖中有標註)來將整個新生代內存劃分為一塊較大的Eden區和兩塊較小的Survivor區(S0、S1)。

每次使用Eden區和其中一個Survivor區,當發生回收時將Eden區和Survivor區中還存活的對象一次性復制到另一塊Survivor區上,最後清理掉Eden區和剛才使用過的Survivor區。理想情況下,每次新生代中的可用空間是整個新生代容量的90%(80%+10%),只會有10%的內存會被浪費。實際情況中,如果另外一個10%的Survivor區無法裝下所有還存活的對象時,就會將這些對象直接放入老年代空間中(這塊在後面的分代回收算法會說到,這裏先了解下)。

3)、標記-清除-壓縮

如果在對象存活率較高的情況下,仍然采用復制算法的話,因為要進行較多的復制操作,效率就會變得很低,而且如果不想浪費50%的內存空間的話,就還需要額外的空間進行分配擔保以應對存活對象超額的情況顯然老年代不能采用2)中的復制算法。、

根據老年代的特點,標記-清除-壓縮(簡稱標記-整理)算法應運而生,這種算法的標記過程仍然與“標記-清除”算法一樣,只是後續的步驟不再是直接清除可以回收的對象,而是將所有存活的對象都向一端移動後,再直接清理掉端邊界以外的內存。

示意圖如下:

技術分享圖片

技術分享圖片

4)、分代回收算法

實際上在講解復制算法時已經涉及到了分代回收的內容,這種算法根據對象存活周期的不同將內存劃分為幾塊,Java中主要是新生代、年老代這樣就可以根據各個年代的特點,采用合適的收集算法了在文頂的圖中已經標示,新生代采用了復制算法,而老年代采用了整理算法這裏就不再贅述

                                              摘自原文作者:無敵碼農

一張圖看懂JVM之垃圾回收算法詳解