JVM如何判斷對象能否被回收


  •寫在前面

  說起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到這個對象不可達),則證明對象是不可用的,大致可以像下圖理解。

  那么哪些對象可以作為GC Roots對象呢?在java中大致有如下幾種:

  •虛擬機棧(棧幀中的本地變量表)中引用的對象;(不知道棧幀是啥的看我另一篇文章,運行時數據區)

  •方法區中類靜態屬性引用的對象;

  •方法區常量引用的對象;

  •本地方法棧中JNI(即一般說的Native方法)引用的對象;

  •引用鄭州治療婦科的醫院 http://www.120kdfk.com/

  引用是啥?搞過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虛擬機規范中,只是說可以不要求在方法區實現回收機制。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM