JVM垃圾回收(上):面試必問,白話文講解,小白請進

垃圾回收回收哪?回收誰?

怎麼斷定物件已死?引用計數法可達性分析演算法

可達性分析演算法的實現GC Roots的標記和儲存GC Roots遍歷前GC Roots遍歷前前GC Roots遍歷中

四種引用型別強引用軟引用弱引用虛引用

垃圾回收

其實講到垃圾回收,不外乎弄懂三個哲學問題:

回收哪?

回收誰?

怎麼回收?

回收哪?

JVM垃圾回收(上):面試必問,白話文講解,小白請進

講到這個,我就又要上 JVM 記憶體分佈的圖了,把圖攤開說大事(需要詳細瞭解這些記憶體分佈的請移步前面我的魔性介紹Java跨平臺根本原因,面試必問JVM記憶體結構白話文詳解來了 )。

首先把程式計數器排除,(再囉嗦一遍它的作用,程式計數器存放的是下一條位元組碼指令執行的地址,存放地址的地方,因此只需要一塊較小的記憶體空間,幾乎忽略不計,它的作用是當前執行緒所執行的位元組碼行號指示器,它是程式控制流的指示器,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成),這麼小的地方,可憐的連 OutOfMemoryError 都不會有的地方,不值得垃圾回收器出馬,pass掉。

接著,棧區,包括本地方法棧和虛擬機器棧。遙望棧當年,入棧出棧不亦樂乎,半點不留數。棧是個喜歡自己玩的區域,資料在裡面也就是玩玩而已,進去了入棧了轉一圈計算完了又出棧了,所以這塊記憶體也不用垃圾回收器操心人家累了自己會出來。

因此上面提到的,都不需要垃圾回收,隨執行緒生隨執行緒死。

然後,方法區,這個老哥,存的是主要是類資訊和執行時常量池,類資訊也就算了,撐死能有多大,編譯完就可以確定大小並且不會改變的東西。主要是這個執行時常量池壞事,以前的垃圾回收器不管這部分記憶體,如果執行時好死不死有很多的常量產生,那麼常量池就會變得很大最後記憶體溢位,還經常引發事故,後來JVM重視這一塊也開始垃圾回收。

最後,堆區,這個就比較 happy 了,大量 OOM 的地方,執行期間可能會有數不清的物件產生,不產生我寫個死迴圈new也要產生,是垃圾回收期的親兒子重點照顧物件。

所以,回收哪?不就是回收方法區和堆區嗎老弟。

回收誰?

關於回收誰,方法區和堆區裡面有誰呢?

方法區的常量,從常量資料型別看分為基本型別和引用型別兩種常量,基本型別肯定不用擔心,JAVA裡面八種基本型別說1就是1,和別的物件什麼的沒有牽扯,說回收也就回收了;引用型別可能和別的物件藕斷絲連,不能輕易斬斷。

堆區裡面全是物件,一個個的物件,互相牽扯,要回收任何一個物件都需要看看他還有沒有被別人引用,不能輕易回收。

所以,總的來看,回收引用型別的資料是關鍵。

怎麼斷定物件已死?

突然想起了一個笑話,有村民舉報野外有一具屍體,狄仁傑和李元芳去辦案,狄仁傑指著屍體說:“我斷定此人已死,元芳你怎麼看?”。笑話有點冷。

物件已死,簡單理解就是在任何地方都不需要引用這個物件了,這個需求其實很簡單,讓我們自己來設計的話,就是將所有跟此物件有牽扯的物件都用一個值統計一下,被引用一次+1,取消引用-1。

引用計數法

演算法思想:這個方法,就是給物件加一個引用計數器,每當有一個地方引用,計數器加1;當引用失效,計數器減1;當計數器為0表示這個物件已死沒有再被使用。

這個演算法簡單高效粗暴,但是不能解決互相迴圈引用的問題,虛擬碼也就是如下情況:

public class MyObject { public Object ref = null; public static void main(String[] args) { MyObject myObject1 = new MyObject(); MyObject myObject2 = new MyObject(); myObject1。ref = myObject2; myObject2。ref = myObject1; myObject1 = null; myObject2 = null; }

那麼A和B的引用計數器就都是1,為 null 主動觸發GC也不能回收,JAVA 裡面物件那麼多,可見這種方式,在JVM裡面是行不通的,那就最佳化。

可達性分析演算法

JVM垃圾回收(上):面試必問,白話文講解,小白請進

演算法思想:兩個物件之間互相迴圈引用不好解決,那就給他們同一個父祖先,這個物件不管在哪裡,只要這個物件到達GC Roots有路,也就是有引用鏈,那麼這個物件就是可達的。反之,如果這個物件到達GC Roots沒路了,不可達,那麼這個物件已死不可用。圖中 Object5,6,7就因為不可達,所有可以被回收。

那麼問題來了,這個GC Roots又是何方神聖?

其實這個就是物件,在Java語言中,可作為GC Roots的物件包括下面幾種:

虛擬機器棧(棧幀中的本地變量表)中引用的物件

方法區中類靜態屬性引用的物件

方法區中常量引用的物件

本地方法棧中JNI(即一般說的Native方法)引用的物件

System Class(系統類,例如Java。util。*),

Thread Block(一個物件存活在一個阻塞的執行緒中) ,

Thread(執行緒),正在執行的執行緒

Busy Monitor (呼叫了wait()或notify()或已同步的所有內容。例如,透過呼叫synchronized(Object)或進入 synchronized 同步方法。靜態方法表示類,非靜態方法表示物件。

……

被這些物件引用的物件,都是可達的。

可達性分析演算法的實現

GC Roots的標記和儲存

演算法有了,怎麼實現呢?一步一步來,首先進行標記,這麼多物件都可以作為GC Roots,那麼就標記下來到底哪些是那些不是。兩種方法,第一種還是暴力遍歷,第二種就是準確式的遍歷:

保守式GC:

拿棧區來舉例子,所有的引用都進行遍歷,遇到數字地址就判斷是否是一個引用(這裡會涉及上下邊界檢查(GC堆的上下界是已知的)、對齊檢查(通常分配空間的時候會有對齊要求,假如說是4位元組對齊,那麼不能被4整除的數字就肯定不是指標),是的話就放GCRoots;這樣會有問題

就是這個引用是不是指向了堆物件不知道,如果是個無效的引用那麼還是沒法回收。

其次,這樣其實很慢,遍歷就要考慮時間問題,物件的狀態在這段時間內的變化問題,上面看到方法區的內容也放到了GC Roots,這個地方多的時候上百兆內容,遍歷的話很耗時。

我們在學習mysql,redis一些資料備份的時候,肯定接觸過快照的概念,拍個快照然後接著該幹嘛幹嘛。

準確式GC: 如果我們知道對於某個位置上的資料是什麼型別的,這樣就可以判斷出所有的位置上的資料是不是指向GC堆的引用,那麼我們最終需要遍歷的內容會少太多了,但是我們需要知道這個型別,那麼就需要維護這個型別的表,JVM定義了一種OopMap資料結構來儲存型別,其實就是一張對映表:

類載入完成時候,HotSpot就把物件內什麼偏移量是什麼型別資料計算出來,物件引用自然也算出來

JIT編譯,也會在特定位置記錄下棧和暫存器的哪些位置是引用;

也就是在整個Java生命週期裡面JVM都在維護這張表,所以GC Roots 標記方式以及儲存的資料結構就都明確了。

GC Roots遍歷前

首先繞了這麼一大圈,明確一下,GC ROOTS這個是用來判斷垃圾回收時回收哪些物件的,從目的出發,遍歷這個GC ROOTS,沒在這裡面的物件都可以進行回收。

遍歷的話,肯定要遍歷最新準確的的 GC ROOTS 的儲存結構 OopMap,那麼問題來了,什麼時候GC Roots是最新的準確的?

方法執行的過程中, 引用關係時刻在發生變化,那麼儲存的 OopMap表 的更新就要隨著變化,如果每個物件引用關係變一下都要觸發 OopMap表 的更新,那維護這個的成本也太高了,而且GC也不是時刻都在發生的,沒有必要。這個OopMap表準確性只要保證 GC之前遍歷的時候他是準確的就好了。GC又是什麼時候觸發的,所以這裡就引入了安全點的概念,安全點決定GC的時機,GC來臨之前肯定要先進行 OopMap表 的更新。

GC Roots遍歷前前

安全點 SafePoint 觸發GC,程式只有在到達了安全點才會暫停進行GC,不是隨心所欲的,安全點的選擇就很關鍵了,安全點太少了GC等待時間太長,太多了增大了執行時的壓力,這個點的選擇程式會以他自己“是否具有讓程式長時間執行的特徵”為標準來界定的,長時間執行最明顯的就是指令序列複用,例如方法呼叫,迴圈跳轉,異常跳轉等。

為了讓執行緒在安全點處才停頓,有兩種辦法:

搶斷式中斷

就是在GC的時候,讓所有的執行緒都中斷,如果這些執行緒中發現中斷地方不在安全點上的,就恢復執行緒,讓他們重新跑起來,直到跑到安全點上。

主動式中斷

當GC需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起。

現在都是第二種方式,原因我猜測主要是因為第一種是阻塞的,第二種是非阻塞的效率更高。

GC Roots遍歷中

以上都是遍歷前的準備工作,現在要開始正經遍歷了,遍歷期間又要注意什麼呢?知道了什麼時候開始GC,那麼就可以進行拍快照儲存OopMap了,就又有一個問題,別嫌棄問題多,給女孩子拍照尚且還要等妹子擺好pose,比好剪刀手呢,因此快照肯定不能就隨手咔嚓一下。

我們需要得到準確的可達性分析結果,那就不可以出現在分析期間物件引用關係還在瘋狂的變化情況,因此在進行遍歷 GC Roots 的時候,需要將所有的使用者執行緒都停下來,JVM 有個浪漫的說法叫做“stop the world”,這是 GC 進行時需要停頓的一個最主要原因,即使是 CMS 號稱不停頓 GC 的收集器這一步停頓遍歷根結點也是不可避免的。

四種引用型別

位元組面試官問這個問題,我是始料未及的,我只之前粗略的知道這四種,把四種名字說出來了,然後說是垃圾回收時根據記憶體情況回收不同型別,牙縫裡就再也蹦不出一個字了,因為哪種型別什麼時候回收我渾然忘光了。

JAVA中的引用就相當於C中的指標。

JVM垃圾回收(上):面試必問,白話文講解,小白請進

java。lang。ref包是JDK1。2引入的,包結構和類分佈如下:

java。lang。ref Cleaner。classFinalizer。classFinalizerHistogram。classFinalReference。classPhantomReference。classReference。classReferenceQueue。classSoftReference。classsWeakReference。class

要我一直死記概念是很難的,年紀大了越來越不喜歡這種記憶的活,因此還是從推理的角度來看這個引用問題。我們平時中新建物件 Object object = new Object();上面那麼花裡胡哨的引用我是沒有用過的,相信大部分人也不經常用,然而存在即合理,Java程式碼裡面很多封裝類的原始碼裡面就用到了。

為什麼要設計這四種引用型別呢?JDK1。1就是隻一種的,加的原因猜測有如下三:

拓展引用型別,使得引用型別更加豐富,便於拓展Java語言一些別的功能;

垃圾回收總的來說對程式設計師是比較不透明的,除了最開始的強引用,其他的三個對垃圾回收敏感,給程式設計師自己選擇的權利,可以定義一些物件的垃圾回收方式;

增大回收效率,每種物件回收的時機不一樣,遍歷的時候可以針對性遍歷;

強引用

Object obj = new Object();

生命週期:這樣的常規引用,只要引用還在,就永遠不會回收物件。

軟引用

Object a = new Object();ReferenceQueue queue = new ReferenceQueue<>();SoftReference sf = new SoftReference(a,queue);

生命週期:在發生記憶體溢位之前,進行回收,如果這次回收之後還沒有足夠的記憶體,則報OOM。

具體實現:如程式碼所示,軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。後續,我們可以呼叫 ReferenceQueue 的poll()方法來檢查是否有它所關心的物件被回收。如果佇列為空,將返回一個null,否則該方法返回佇列中前面的一個 Reference 物件。

應用場景:軟引用通常用來實現記憶體敏感的快取。如果還有空閒記憶體,就可以暫時保留快取,直接透過軟引用取值,無需從繁忙的真實來源查詢資料,提升速度;當記憶體不足時,自動刪除這部分快取資料,從真正的來源查詢這些資料。這樣就保證了使用快取的同時,不會耗盡記憶體。

弱引用

生命週期:生命週期比軟引用短,生存到下一次垃圾回收之前,無論當前記憶體是否夠用,都回收掉被弱引用關聯的物件。

Object a = new Object();ReferenceQueue queue = new ReferenceQueue<>();WeakReference wf = new WeakReference<>(a,queue);

具體實現:如程式碼,弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中,後續和軟引用差不多。

應用場景:

同樣可用於記憶體敏感的快取;

ThreaLocal中的map實現,此map繼承了弱引用WeakReference,防止map中的key引用的物件無法被回收;

//繼承了弱引用WeakReferencestatic class Entry extends WeakReference> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }}

虛引用

虛引用也叫幻象引用,透過 PhantomReference 類來實現。不會對物件的生命週期有任何影響,也無法透過它得到物件的例項,唯 一的作用也就是在物件被垃圾回收前收到一個系統通知

應用場景:可用來跟蹤物件被垃圾回收器回收的活動,當一個虛引用關聯的物件被垃圾收集器回收之前會收到一條系統通知。

怎麼回收公眾號《阿甘的碼路》下一篇會繼續更,這些內容要往深了寫內容很深,能堅持看下來的人肯定是狠人。寫作不易,一鍵三連是美德,喜歡可以關注。

關注我,一起成長