深入理解Java虛擬機第三章讀書筆記:垃圾回收算法

更正

之前寫這篇筆記的時候我對標記-清除算法的理解好像有些偏差,最近複習筆記的時候發現之前描述的那個過程好像容易造成誤解,因爲是個人學習筆記,我就不在正文中修改了,方便自己以後回看。

我重新總結一下標記-清除算法的過程:

如果對象通過可達性分析後發現沒有與GC Roots相連的引用鏈,那麼它會被進行第一次標記,並且進行一次篩選(篩選的條件是有沒有必要執行finalize()方法,這一部分下文中沒有寫錯,可以參照),如果對象被判定爲沒有必要執行finalize()方法的話,那麼它就會被放入一個“即將回收集合”中;如果對象被判定爲有必要執行finalize方法,那麼這個對象會被放置在一個F-Queue隊列中,由JVM創建的一個低優先級的finalize線程來執行這些對象的finalize()方法。(這裏所描述的執行,其實只是觸發這個對象的finalize()方法,並不承諾等它執行完,因爲如果一個對象的finalize()方法執行緩慢,或者發生了死循環,很可能會導致整個F-Queue隊列的對象永久處於等待狀態,甚至導致內存回收系統崩潰),如果有對象通過執行finalize()方法重新獲得了引用,那麼就會被移出“即將回收集合”,然後對還存在於“即將回收集合”中的對象進行第二次標記,標記過後就會開始清除工作。


1.引用計數法和可達性分析算法

Q1:垃圾收集器什麼時候會回收一個對象呢?

A1:當一個對象不能再通過任何途徑被引用的時候它就會被垃圾收集器收回

Q2:那麼垃圾收集器是怎麼判斷一個對象是否還能夠被引用的呢?

A2:大致有如下兩種方法

  1. 引用計數法:給對象添加一個引用計數器,有任何一個地方對它進行引用的時候,就給這個計數器加一,有任何一個引用失效時,就將計數器減一。任何時刻計數器的值等於0,那麼對象就不能再被引用。引用計數法的實現較爲簡單,且效率也比較高,大部分情況下都是一個不錯的算法。微軟的COM技術,Python語言等都在使用它。但是,在主流的Java虛擬機中並沒有使用它,主要原因是它難以解決對象之間循環引用的問題。例如對象A和對象B都有instance字段,賦值令A.instance = B,B.instance = A.除此以外兩個對象再無任何引用,但是這兩個對象實際上已經無法再被引用,因爲他們彼此引用着對方,但是他們的引用計數器都不爲0,垃圾收集器也無法回收它們。再一個Java中的引用類型有多種,有的引用類型並不能實際上去使用牽引着的對象。而計數器無法區別這些引用類型,所以使用引用計數器法容易引起衝突。
  2. 可達性分析算法:這個算法的基本思想就是通過一系列的稱爲“GC Roots”的對象作爲起始點‘’,然後從這些節點開始向下搜索,搜索走過的路徑被稱爲引用鏈,當一個對象無法通過引用鏈到達GC Roots的時候,那麼它就是不可再被引用的,例如下圖,object5、6、7無法通過引用鏈到達GC Roots,那麼它們就會被判定爲可回收的對象。可以作爲GC Roots的對象有以下幾種:
  • 虛擬機棧中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI(Java Native Interface)引用的對象

2.強引用、軟引用、弱引用、虛引用

  • 強引用          Object  O  =  new  Object(),這樣的引用就是強引用。只要強引用還在,那麼垃圾收集器永遠不會收回這類對象
  • 軟引用          軟引用用來描述一些還有用但非必須的對象。對於軟引用關聯着的對象,在系統發生內存溢出異常之前,垃圾收集器就會回收被軟引用關聯着的對象,如果回收以後內存還是不夠,系統纔會拋出內存溢出異常。JDK1.2以後,提供了SoftReference類來實現軟引用
  • 弱引用          弱引用用來描述非必須對象的,被弱引用關聯着的對象,在下次垃圾收集發生之前,不管內存夠或者不夠,都會強制將這些對象回收。JDK1.2以後,提供了WeakReference類來實現弱引用
  • 虛引用          無法通過虛引用來獲取一個對象實例,它的唯一作用是在這個被關聯的對象被回收時,收到一個系統通知。JDK1.2y以後,提供了PhantomReference類來實現虛引用

 3.垃圾收集算法

3.1標記-清除算法

  • 第一次標記

如果對象再進行可達性分析算法分析後,發現與GC Roots沒有引用鏈相連,那麼就會被進行第一次標記,並進行一次篩選。

篩選的條件是該對象是否有必要執行finalize()方法,如果該對象finalize()方法沒有被覆蓋過,或者已經被虛擬機調用過了該方法,那麼就會被判斷爲沒有必要執行。(finalize()方法是Object的protected方法,子類可以覆蓋該方法以進行資源清理工作,它不與C++中的析構函數對應,這個方法只能被執行一次,目的是爲了防止對象無限復活

如果被判定爲沒有必要執行finalize()方法,那麼就會被二次標記,進行回收。如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象就會被放置到一個F-Queue隊列中,之後會有一個由虛擬機自動建立的低優先級的Finalizer線程來執行這個隊列中對象的finalize()方法。

  • 第二次標記

finalize()方法是對象逃脫死亡的最後一次機會,稍後GC將會進行第二次小規模的標記,如果對象要在finalize()中成功的拯救自己,即只要重新與任何一個對象建立關聯,比如使用this關鍵字來講自己賦值給某個對象的變量。那麼在第二次標記時它將被移出{即將回收}這個集合。

如果這個時候對象還沒有逃脫,那麼它基本上就會被回收了。

標記-清除算法的缺點:

  • 效率低下,標記和清除兩個過程的效率都不高
  • 空間問題,標記清除後會產生大量的內存空間碎片。空間碎片太多,會導致以後分配較大對象的時候,會沒有足夠連續可用的空間來分配給該對象,從而導致再一次觸發垃圾收集動作

3.2複製收集算法

複製算法是對標記-清除算法的一種改進。

將內存劃分爲兩塊空間大小相等的區域,每次只使用其中的一塊。當使用中的那塊內存空間被消耗完的時候,就將此時還存活的對象複製下來,並移動到另外一塊還沒有被使用過的內存空間中。然後將使用過的那塊內存一次性全部清理掉。這樣就只需要對一半的內存空間進行清理,大大提高了效率,也避免了出現內存碎片這個問題。

複製算法的缺點:

  • 將內存空間分爲兩塊,一次只使用一半,相當於浪費了一半的內存,內存使用率上有點過多的浪費

3.3標記-整理算法

複製收集算法在對象存活率較高時就要進行較多的複製操作,會使得效率變低,在老年代中就非常不適用。標記-整理算法是在標記-清除算法上的一種改進算法。標記過程一樣,但是後續不是對可回收對象進行清理,而是讓所有還存活的對象都向一端移動,然後把端界限以外的空間全部清理出來。

3.4分代收集算法(當前主流虛擬機都使用的垃圾收集策略)

根據對象存活週期的不同將內存劃分爲幾塊,一般都是把Java堆內存劃分爲“新生代”和“老年代”,然後根據新生代和老年代不同的特性來使用不同的垃圾收集算法。

例如新生代中的對象一般都是“朝生夕死”,存活率不高,每次都只有少量的對象存活,那麼就使用複製收集算法來進行垃圾收集。只需要付出少量存活對象的複製成本就可以完成收集。

而老年代中的對象存活率很高,沒有額外的空間能對它進行分配擔保,就必須使用標記-清除算法或者標記-整理算法來進行回收。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章