虛擬機垃圾回收判定

  • 引用計數法
  • 可達性分析演算法
 
引用計數演算法(Reference Counting)在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的對象就是不可能再被使用的。雖然佔用了一些額外的記憶體空間來進行計數,但它的原理簡單,判定效率也很高,在大多數情況下它都是一個不錯的演算法。但是,在Java 領域,至少主流的Java虛擬機裡面都沒有選用引用計數演算法來管理記憶體,主要原因是,這個看似簡單的演算法有很多例外情況要考慮,必須要配合大量額外處理才能保證正確地工作,譬如單純的引用計數就很難解決對象之間相互循環引用的問題。
 
可達性分析演算法,這個演算法的基本思路就是通過 一系列稱為「GC Roots」的根對象作為起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過 程所走過的路徑稱為「引用鏈」(Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連, 或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。
在Java技術體系裡面,固定可作為GC Roots的對象包括以下幾種:
  • 在虛擬機棧(棧幀中的本地變數表)中引用的對象,譬如各個執行緒被調用的方法堆棧中使用到的參數、局部變數、臨時變數等。
  • 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變數。
  • 在方法區中常量引用的對象,譬如字元串常量池(String Table)里的引用。
  • 在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
  • Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類載入器。
  • 所有被同步鎖(synchronized關鍵字)持有的對象。
  • 反映Java虛擬機內部情況的JM XBean、JVM TI中註冊的回調、本地程式碼快取等。
除了這些固定的GC Roots集合以外,根據用戶所選用的垃圾收集器以及當前回收的記憶體區域不同,還可以有其他對象「臨時性」地加入,共同構成完整GC Roots集合。譬如,分代回收或者局部回收下,某個區域里的對象完全有可能被位於堆中其他區域的對象所引用,這時候就需要將這些關聯區域的對象也一併加入GC Roots集合中去,才能保證可達性分析的正確性。
 
引用類型
無論是通過引用計數演算法判斷對象的引用數量,還是通過可達性分析演算法判斷對象是否引用鏈可 達,判定對象是否存活都和「引用」離不開關係。
Java將引用分為強引用(Strongly Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。
  • 強引用是最傳統的「引用」的定義,是指在程式程式碼之中普遍存在的引用賦值,即類似「Object obj=new Object()」這種引用關係。無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象。
  • 軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關聯著的對象,在系統將要發生內 存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體, 才會拋出記憶體溢出異常。在JDK 1.2版之後提供了SoftReference類來實現軟引用。
  • 弱引用也是用來描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只 能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前記憶體是否足夠,都會回收掉只 被弱引用關聯的對象。在JDK 1.2版之後提供了WeakReference類來實現弱引用。
  • 虛引用也稱為「幽靈引用」或者「幻影引用」,它是最弱的一種引用關係。一個對象是否有虛引用的 存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛 引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2版之後提供 了Phant omReference類來實現虛引用。
 
可回收判斷
如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,並不會立馬判定為可回收。首先要盤判斷該對象是否有必要執行finalize方法。finalize方法沒有被覆蓋或者已經被虛擬機調用,虛擬機判定為沒必要執行。如果這個對象被判定為確有必要執行finalize()方法,那麼該對象將會被放置在一個名為F-Queue的 隊列之中,並在稍後由一條由虛擬機自動建立的、低調度優先順序的Finalizer執行緒去執行它們的finalize() 方法。這裡所說的「執行」是指虛擬機會觸發這個方法開始運行,但並不承諾一定會等待它運行結束。 這樣做的原因是,如果某個對象的finalize()方法執行緩慢,或者更極端地發生了死循環,將很可能導致F-Queue隊列中的其他對象永久處於等待,甚至導致整個記憶體回收子系統的崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後收集器將對F-Queue中的對象進行一次小規模的排查,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己 (this關鍵字)賦值給某個類變數或者對象的成員變數,那在第二次標記時它將被移出「即將回收」的集合;如果對象這時候還沒有逃脫,那基本上它就真的要被回收了。自救的機會只有一次,因為一個對象的finalize()方法最多只會被系統自動調用一次,所以下一次失去可達性後,他就沒得自救了。
注意:不建議使用finalize來拯救對象。運行代價高昂,不確定性大,無法保證各個對象的調用順序,如今已被官方明確聲明為 不推薦使用的語法。finalize()能做的所有工作,使用try -finally 或者其他方式都可以做得更好、 更及時。
 
 
方法區是否有垃圾回收
大多都有,也有沒有的,比如JDK 11時期的ZGC收集器就不支援類卸載。
方法區的垃圾收集主要回收兩部分內容:廢棄的常量和不再使用的類型。
廢棄的常量:已經沒有任何對象引用常量池中的常量,且虛擬機中也沒有其他地方引用這個字面量。如果在這時發生記憶體回收,而且垃圾收集器判斷確有必要的話,這個常量就將會被系統清理出常量池。常量池中的類(介面)、方法、欄位的符號引用也是如此。
不再使用的類型:判定一個常量是否「廢棄」還是相對簡單,而要判定一個類型是否屬於「不再被使用的類」的條件就 比較苛刻了。需要同時滿足下面三個條件,
  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
  • 載入該類的類載入器已經被回收,這個條件除非是經過精心設計的可替換類載入器的場景,如OSGi、JSP的重載入等,否則通常是很難達成的。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Tags: