首頁 > 遊戲

JVM如何判斷物件能否被回收

由 黑馬程式設計師西安中心 發表于 遊戲2023-01-03

簡介弱引用,也是用來描述非必需的物件,但是它的強度比軟引用更弱,被弱引用關聯的物件只能生存到下一次回收發生之前,當垃圾回收器工作時,無論當前記憶體是否足夠,都會回收掉

弱引用什麼時候被回收

寫在前面

說起Java和C++,很容易想到讓人瘋狂的指標,Java使用了記憶體動態分配和垃圾回收技術,讓我們從C++的各種指標問題中擺脫出來,更加專心於業務邏輯,不過如果我們需要深入瞭解java的JVM相關原理,我們必須要面對這些東西,深入瞭解JVM在記憶體動態分配和垃圾回收技術的原理知識,這篇文章就是來做一個先導,在jvm進行垃圾回收之前,它必須要知道回收的物件是否已“死”,這樣才能保證程式的正常穩定。

物件的建立

我們將回收物件前,先講講在虛擬機器上,物件是怎麼被建立的。在我們編寫程式碼的角度(語言層面)來看,我們建立一個物件例項,只需要使用new關鍵詞就完事兒了,很簡單,不過你享受的簡單是因為虛擬機器幫你承受了所有繁瑣的工作,那虛擬機器是怎麼工作建立一個物件的呢?

當虛擬機器遇到new指令時,首先會去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用(沒有類,建立個錘子的物件),並且檢查這個符號引用代表的類是否已被載入、解析和初始化過,如果沒有,那必須要執行相應的類載入過程。這是第一步,在類載入檢查過後,接下來虛擬機器將為新生物件分配記憶體,物件所需的記憶體大小在類載入完成後便已經完全確定了(這裡插一句,如何確定的?這就和物件的記憶體佈局有關了,物件在記憶體中的佈局可以分為3個區域,分別是物件頭、例項資料和對齊填充,物件頭裡面存的是物件自身的執行時資料,比如雜湊碼、GC分代年齡、鎖狀態、執行緒持有的鎖、偏向執行緒ID等等之類的資訊,也就是和儲存資料無關的額外記憶體空間,按道理這一塊空間應該是固定的,不過在設計上還是被弄成了非固定的資料結構,這樣更具不同的類節省空間,不深入不然扯不完,想要可以看另外一篇文章。接下來例項資料就是物件真正儲存的有效資訊,也是程式程式碼中所定義的各種型別的欄位內容。最後一個對齊填充,顧名思義就是填補空間,因為以HotSpotVM為例,物件的大小必須是8位元組的整數倍,所以就靠這個補全),給物件分配空間的任務相當於把一塊確定大小的記憶體從Java堆中劃分出來(為啥可以看我另一篇文章,執行時資料區)。

劃分的時候會出現兩種情況,第一種就是java堆中的記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間的一邊移動物件大小相等的距離,這種分配方式就是“指標碰撞”。第二種情況就是空間不規整,也就是已使用的記憶體和空閒記憶體相互交錯,這個時候指標碰撞起不來作用,那麼這個時候虛擬機器必須維護一個列表,記錄哪些記憶體可用,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新相關記憶體資訊,這種方式叫做“空閒列表”。因為建立物件非常頻繁,所以會涉及到併發的時候,會出現一個叫做“本地執行緒分配緩衝”的概念,我這裡也不深入,自己去查,哈哈哈。空間分配完成之後,虛擬機器需要分配到的記憶體空間都進行初始化為零值(注意不包括物件頭),這樣就保證物件的例項欄位在java程式碼中可以不賦初始值就直接使用。最後虛擬機器要對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的元資料資訊,物件的雜湊碼、物件的GC分代等資訊。到此,對於虛擬機器來說,物件建立完畢。

引用計數演算法

引用計數是一個很好理解的概念,就是給物件新增一個引用計數器,每當有一個地方引用這個物件時,計數器值加1,每當一個引用失效時,計數器減1,任何時刻計數器為0的物件就是不可能再被使用的。是不是很好理解,而且判定物件是否可用效率很高,在大部分時候它是一個很不錯的演算法,不過要注意,是大部分時候。在java虛擬機器中,並沒有使用這個演算法來管理記憶體,其中最主要的原因就是它很難解決物件之間迴圈引用的問題。來,舉個例子來理解,比如現在有兩個物件objectA和objectB都有欄位instance,賦值讓objectA。instance = objectB, objectB。instance = objectA,除此之外沒有任何其他引用,實際上這兩個物件已經不可能再被訪問了,但是因為它們兩個互相引用這對方,導致它們的引用計數不為0,則演算法不能通知GC收集器回收它們。所以這種演算法不適合在虛擬機器上使用,但是並不是說這個演算法很垃圾,它可是在其他方面有很多著名的案例。

可達性分析演算法

JVM的主流實現是可達性分析,可達性分析在概念上其實也不難理解,它的基本思路就是透過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時(圖論裡面專業一點來說,就是從GC Roots到這個物件不可達),則證明物件是不可用的,大致可以像下圖理解。

JVM如何判斷物件能否被回收

那麼哪些物件可以作為GC Roots物件呢?在java中大致有如下幾種:

虛擬機器棧(棧幀中的本地變量表)中引用的物件;(不知道棧幀是啥的看我另一篇文章,執行時資料區)

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

方法區常量引用的物件;

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

引用

引用是啥?搞過C++的我們第一反應就會回答,如果reference型別的資料中儲存的數值代表的是另一個記憶體的起始地址,就稱這塊記憶體代表著一個引用。這種定義沒有錯,不過太狹隘了,一個物件在這種定義下只能被引用或者沒有被引用兩種狀態,顯然在回收中不足以應付碰到的情況。所以,java對引用概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種,這四種引用強度一次逐漸減弱。

強引用,就是指在程式程式碼之中普遍存在的,類似A a = new A()這樣的引用,只要強引用存在,垃圾回收器就不會回收掉被引用的物件;

軟引用,用來描述一些還有用但並非必須的物件,對於軟引用的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會出現記憶體溢位異常;

弱引用,也是用來描述非必需的物件,但是它的強度比軟引用更弱,被弱引用關聯的物件只能生存到下一次回收發生之前,當垃圾回收器工作時,無論當前記憶體是否足夠,都會回收掉;

虛引用,它是最弱的一種引用關係,一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法透過虛引用取得一個物件例項、為一個物件設定引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。

不可達必須“死”?

其實在實際中,就算在可達性分析演算法中不可達的物件,也並非一定會回收,這個時候不可達的物件暫時處於暫緩的階段,一個物件要真正宣告死亡,至少要經歷兩次標記的過程,當物件進行可達性分析而不可達時,它會被第一次標記並且進行一次篩選,篩選條件是這個物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被呼叫過了,虛擬機器將會把這兩種情況視為沒有必要執行finalize。當物件被判定有必要執行finalize時,物件將會被放置在一個叫做F-Queue的佇列中,並在稍後的一個由虛擬機器自動建立的、優先順序低的一個Finalizer執行緒去出發這些物件的finalize(要注意的是,虛擬機器並不承諾會等待這些物件finalize方法執行結束,這是因為如果一個物件的finalize方法執行緩慢、或者發生死迴圈,將導致F-Queue佇列其他物件處於永久等待,甚至導致記憶體回收系統崩潰)。finalize是物件逃脫回收的最後一次機會,GC會將F-Queue中的物件進行第二次小規模的標記,如果物件在finalize中重新和引用鏈連上了,那麼就被移出回收集合,沒有逃脫則將會被回收(要記住哦,物件的finalize只能被執行一次,也就是說當物件透過finalize逃脫回收之後,下一次如果再被可達性分析標記,那麼就逃不了了)。

最後

其實很多時候我們談論回收都在java堆上進行的,上面物件例項都是在java堆上進行的,很少談及方法區的回收,因為方法區(一般被稱為永久代)中的回收條件很苛刻,比如在java堆上進行回收可以達到70%-95%的空間,在方法區卻低很低,但並不代表方法區不能有垃圾回收,Java虛擬機器規範中,只是說可以不要求在方法區實現回收機制

Tags:物件引用回收虛擬機器記憶體