深刻Java虛擬機(五)垃圾收集

Java 虛擬機的堆裏存放着程序運行中所建立的全部對象。虛擬機可使用newnewarrayanewarraymultianewarray指令來建立對象,可是沒有明確的代碼來釋放它們。垃圾收集就是自動釋放再也不被程序所使用的對象的過程。java

本篇文章並非要描述正式的 Java 垃圾收集器,由於根本不存在這樣一個正式的描述。前面說過,Java 虛擬機規範不要求任何特定的垃圾收集技術,這根本不是必需的。可是在發明能夠無限使用的內存前,大部分的 Java 虛擬機都會附帶垃圾收集功能程序員

爲何要使用垃圾收集

垃圾收集這個名字暗示着程序再也不須要的對象就是垃圾,能夠被丟棄。更準確地說,應該被叫作內存回收。當一個對象再也不被程序所引用時,它所使用的堆空間能夠被回收,一遍後續的新對象所使用。算法

垃圾收集器必須可以確認哪些對象是再也不被引用的,而且可以把它們所佔據的堆空間釋放出來。在釋放對象的過程當中,垃圾收集器還要運行將要被釋放對象的終結方法(finalizer)。數據庫

除了釋放再也不被引用的對象,垃圾收集器還要處理堆碎塊。堆碎塊是在程序運行過程當中產生的。在請求分配新對象的內存空間時,可能不得不增大堆空間的大小,雖然可使用的總空閒空間是足夠的。這是由於堆中的空閒空間並不連續,沒法放下一個新的對象。編程

把內存回收的任務交給虛擬機有幾個好處:緩存

  • 提升生產效率。在一個不具有垃圾收集機制的語言下編程時,你可能須要花費好多時間來查找難以琢磨的內存問題(C/C++用得少,沒機會體驗。。。。。)
  • 保持程序的完整性。垃圾回收技術是Java安全策略的一個重要部分,Java程序員不可能由於錯誤釋放內存而致使Java虛擬機的崩潰

可是使用垃圾收集,有一個潛在的缺陷就是加大了程序的負擔,可能影響程序性能。Java虛擬機必須追蹤哪些對象正在使用,哪些對象須要釋放。和明確釋放內存比起來,內存釋放過程還會須要更多的CPU時間片。安全

垃圾收集算法

任何垃圾收集算法必須作兩件事:網絡

  • 檢測垃圾對象
  • 回收垃圾對象所使用的堆空間並還給程序

根對象(Root)

垃圾檢測一般經過創建一個根對象的集合而且檢查從這些跟對象開始的可觸及性來實現。若是正在執行的程序能夠訪問到跟對象和某個對象之間存在引用路徑,這個對象就是可觸及的。對於程序來講,跟對象老是能夠訪問的。從這些根對象開始,任何能夠被觸及的對象都被認爲是活動的對象。沒法被觸及的對象被認爲是垃圾,它們再也不被程序使用到。數據結構

任何根對象引用的對象都是可觸及的,從而是活動的。另外,任何被活動的對象引用對象都是可觸及的。程序能夠訪問任何可觸及的對象,因此這些對象必須保存在堆裏面。任何不可觸及的對象均可以被收集,由於程序被辦法訪問它們。jvm

來源

Java 虛擬機的根對象集合根據實現方式各有不一樣,可是總會包含局部變量中的對象引用和棧幀的操做數棧(以及類變量中的對象引用)。關於根對象的來源大概有這幾種:

  • 一個來源是用類的常量池中的對象引用,好比字符串。被加載的類的常量池可能指向保存在堆中的字符串,好比類名字、超類的名字、字段名、字段特徵簽名、方法名或者方法特徵簽名。
  • 還有一個來源是傳遞到本地方法中的,沒有被本地方法釋放的對象引用(根據本地方法接口,本地方法能夠經過簡單地返回來釋放引用;或者顯示地調用一個回調函數來釋放傳遞來的引用,或者是這二者的結合)
  • 再一個潛在的對象來源是Java虛擬機的運行時數據區中從具備垃圾收集功能的堆中分配的部分。舉例來講,在某些實現中,方法區中的類數據自己可能被存放在使用垃圾收集器的堆中,以便使用和對象一樣的垃圾回收算法檢測和卸載再也不使用的類。

說實話這三條咋這麼抽象嘞,先往下看看把

基本類型的干擾

在 Java 虛擬機的實現中,有些垃圾收集器能夠區分真正的對象引用和看上去很像合法對象引用的基本類型(好比一個 int 變量)之間的差異。(例如一個 int 整數,若是被解釋是一個本地指針,可能指向堆中的一個對象)但是某些垃圾收集器仍然選擇不區分真正的對象引用和假裝品,這種垃圾收集器被稱爲保守的,由於它們可能作不到釋放掉每個再也不引用的對象。對於保守的收集器,有時候垃圾對象也被錯誤的判斷爲活動的,由於有一個看上去像是對象引用的基本類型"引用"了對象。這種保守的垃圾收集器是垃圾回收速度提升了,由於有一些垃圾被遺忘了。

基礎算法分類

區分活動對象和垃圾對象的兩個基本方法是引用計數和跟蹤。

  • 引用計數垃圾收集器經過堆中的每個對象保存一個計數來區分活動對象和垃圾對象。這個計數記錄下了對象被引用的次數。
  • 跟蹤垃圾收集器是追蹤從根節點開始的引用圖。在追蹤過程當中趕上的對象以某種方式打上標記,當追蹤結束,沒有被打上標記的對象就被斷定爲不可觸及的,能夠被回收。

引用計數收集器

引用計數是垃圾回收的早期策略。在這種方法中,堆中每個對象都有一個引用計數。

規則

規則包括:

  • 當一個對象被建立,而且將指向該對象的引用分配給一個變量,這個對象的引用計數被置爲1。
  • 當任何其餘變量被賦值爲這個對象的引用時,計數加1。
  • 當一個對象的引用超過了生存期或者被置爲一個新的值時,對象的引用計數減1。
  • 任何計數爲0的對象均可以被當作垃圾回收。
  • 當一個對象被回收時,它所引用的對象的計數也要減1。

在這種方法中,一個對象被垃圾收集後可能會觸發後續其餘對象的垃圾收集行動。

弊端

引用計數沒法檢測出循環引用(即兩個或者更多對象之間的相互引用)

循環引用的例子如

class A{
  public B b;
}
class B{
  public A a;
}
public class Main{
    public static void main(String[] args){
        A a = new A();
        B b = new B();
        a.b=b;
        b.a=a;
        a=null;
        b=null;
    }
}
複製代碼

a 和 b 雖然置爲了 null,可是按照引用計數的規則永遠不會被收集,由於 a 和 b 分別持有各自的引用。

跟蹤收集器

跟蹤收集器是追蹤從根節點開始的對象引用圖。在追蹤過程當中遇到的對象以某種方式打上標記。標記時,要麼在對象自己設置標記,要麼用一個獨立的位圖來設置標記。當追蹤結束時,未被標記的對象就知道是沒法觸及的,從而能夠被收集。

基本的追蹤算法被稱做標記-清除算法。這個名字指出了垃圾收集過程的兩個階段:

  • 標記階段:垃圾收集遍歷引用樹,標記每個遇到的對象。
  • 清除階段:釋放未被標記對象所佔用的內存。這個階段就要觸發對象的終結方法。

壓縮收集器

Java虛擬機的垃圾收集器可能有對付堆碎塊的策略。標記-清除收集器一般使用的兩種策略是壓縮和拷貝。這兩種方式都是經過快速地移動對象來減小堆碎塊。

壓縮收集器把活動的對象越過空閒區移動到堆的另外一端,這樣堆的另外一端會出現一個大的連續空間。此後,全部被移動的對象的引用也會被更新,指向新的內存地址。

更新被移動對象的引用有時候會經過一個間接對象引用層,不直接引用堆中的對象,對象的引用實際上指向一個對象句柄表。對象句柄纔是真正指向堆中對象的實際位置。當對象被移動了,只須要更新句柄表就能夠。不過在對象的訪問上,由於增長了一個句柄表,性能有所損失。

拷貝收集器

拷貝垃圾收集器把全部的活動對象都移動到一個新的區域。在拷貝的過程當中,它們被緊挨着佈置,因此能夠消除本來它們在舊區域的空隙。而原有的區域被認爲是空閒區。

這種方法的好處就是從根對象開始遍歷的過程當中,一旦發現對象就進行拷貝,再也不有標記和清除的區分。對象被快速拷貝到新區域,同時轉向指針仍然留在原來的位置。轉向指針可讓垃圾收集器發現已經被轉移對象的引用。而後垃圾收集器把和這個對象有關的引用設置爲轉向指針的值。

通常的拷貝收集器算法被稱爲中止-拷貝。方案以下:

  • 堆被分爲兩個區域,任什麼時候候都只是用其中的一個區域。
  • 對象在同一個區域分配,直到這個區域被耗盡。
  • 此時,程序執行被終止,堆開始遍歷,遍歷時遇到的對象被拷貝到另外的一個區域。
  • 中止-拷貝過程結束後,程序恢復執行。
  • 內存將重新的堆區域開始分配,直到它也被用盡。這時程序再次停止,重複上面的步驟。

這種方法的代價就是,對於指定大小的堆來講,實際上須要兩倍的內存來運行。

圖形描述以下:

image
上圖一共是9張堆內存快照:

  • 快照1中,堆的下半部分沒有被使用,上半部分零散的被對象填充(包含對象的部分用橘黃色表示)
  • 快照2中,隨着程序的運行,上半部分逐漸被對象填充。
  • 快照3中,對象已經把上半部分填滿了。
  • 快照4中,由於快照3已經把上半部分填滿了,此時,垃圾收集器中止程序執行,從根節點開始追蹤活動對象圖。當遇到活動的對象時就拷貝到堆的下半部分,而且每個對象都緊挨着。
  • 快照5中,垃圾收集剛剛結束,程序已經恢復運行。上半部分被清理完成,而且做爲未被使用部分。以前存活的對象移動到了下半部分。
  • 快照6中,隨着程序的運行,下半部分逐漸被對象填充。
  • 快照7中,對象已經把下半部分填滿了。
  • 快照8中,垃圾收集器再次停止了程序,追蹤活動對象。此次它把遇到的活動對象都拷貝到堆的上半部分。
  • 快照9中,垃圾收集完成,下半部分垃圾對象被清理,變爲了未使用區。以前存活的對象移動到了上半部分。

在程序執行中,這個上述過程一次有一次地重複。

按代收集的收集器

簡單的中止-拷貝收集器的缺點是,每一次收集時,全部的活動對象都必須被拷貝。大部分語言的大多數程序都有一下特色,若是咱們全面考慮這些,拷貝算法是能夠改進的。

  • 大部分程序建立的大部分對象都具備很短的生命期。
  • 大多數程序都會建立一些具備很是長生命週期的對象。

簡單的中止-拷貝收集器浪費效率的一個主要緣由就是,它們每次把這些生命週期很長的對象來回拷貝,消耗大量的時間

按代收集的收集器經過把對象按照壽命來分組解決中止-拷貝效率低下的問題,更多地收集那些短暫出現的年幼對象,而非壽命比較長的對象。邏輯以下:

  • 堆被劃分紅兩個或者更多的子堆
  • 每個堆爲一代的對象服務
  • 最年幼的那一代進行最頻繁的垃圾收集。由於大多數對象都是短暫出現的,只有不多一部分年幼對象在經歷第一次收集後還存活
  • 若是一個最年幼的對象在經歷了好幾回垃圾收集後仍然存活,那麼這個對象就成長爲壽命更到的一代:轉移到另一個子堆中
  • 年齡更高的每一代的收集都沒有年輕的那一代來的頻繁
  • 每當對象在它所屬的年齡層中變得成熟(逃過了屢次垃圾收集)以後,它們就會被轉移到更高的年齡層中

按代收集技術除了能夠應用於中止-拷貝垃圾收集算法,也能夠用於標記-清除垃圾回收算法。無論哪一種狀況下,把堆按照年齡層分解均可以提升最基本的垃圾收集算法的性能。

自適應收集器

自適應收集器算法李永樂以下事實:在某種狀況下某些垃圾收集算法工做的更好,而另一些收集算法在另外的狀況下工做得更好

自適應算法監視堆中的情形,並對應的調整爲合適的垃圾收集技術。

使用自適應方法,Java虛擬機的實現者不須要只選擇一種特定的算法。可使用多種技術,以便在最擅長的場合使用它們。

火車算法(train GC

火車算法最先是有Richard HudsonEliot Moss提出的,目的是爲了在成熟對象空間提供限定時間的漸進收集,最先用於Sun公司的Hotspot虛擬機。該算法詳細的說明了按代收集的垃圾收集器的成熟對象空間的組織。

唏噓的是到Sun JDK 6的時候就已經完全不包含train GC了,不過更重要的是思想,仍是看一看吧

以往收集算法存在的問題

垃圾收集算法和主動釋放對象內存比起來有一個潛在的缺點,即垃圾收集算法中程序員對安排 CPU 時間進行內存回收的過程缺少控制。

要精確的預測出什麼時候進行垃圾收集、收集須要多長時間基本上是不可能。由於垃圾收集通常會停止整個程序來查找和收集垃圾對象,它們可能在程序執行的任意時刻觸發垃圾收集,而且停止的時間也沒法肯定。這種垃圾收集的暫停有時候長得讓用戶注意到了。

而當一種垃圾收集算法可能致使用戶可察覺到的停頓或者使得程序沒法知足實時系統的要求,這種算法被稱做破壞性的。

漸進式收集算法

達到非破壞性垃圾收集的方法是使用漸進式垃圾收集算法。

漸進式垃圾收集器就是不試圖一次性發現並回收全部的垃圾對象,而是每次發現並回收一部分。所以每次都只有堆的一部分執行垃圾收集,所以理論上說的每一次收集會持續更短的時間。

若是可以保證每次收集不超過一個最大時間長度,就可讓Java虛擬機適合實時環境,而且也能夠消除用戶可察覺的停頓。

一般漸進式收集器都是按代收集的收集器。

車箱、火車和火車站

火車算法把成熟的對象空間劃分爲固定長度的內存塊,算法每次在一個塊中單獨執行。規則以下:

  • 每一個內存塊屬於某一個集合,並在集合中有序排列。
  • 集合與集合之間有序排列。

在原始論文中,內存塊叫作車箱;集合叫作火車。成熟對象的內存空間叫作火車站

算法組織圖以下:

image

命名計劃

火車按照它們建立時的順序分配號碼。
所以,假設咱們將第一列火車(最早進入該年齡層的對象內存)被拉進軌道1,稱爲火車1。到達的第二輛火車被拉到軌道二,稱爲火車2。下一列到達的火車被拉到軌道3。
依次類推,按照這樣的計劃,號碼較小的火車老是更早出現的火車。

在火車內部,車箱(內存塊)老是附加到火車的尾部。
附加的第一節車箱被稱爲車箱1,這列車附加的下一節車箱被稱爲車箱2。 所以,在列車內部,較小的數字總能表示更早出現的車箱。

這個命名計劃給出了成熟對象空間中內存塊的整體順序。

上圖中顯示了三列車,標記爲火車1火車2火車3

  • 火車1擁有四節車箱,標記爲 1.1-1.4
  • 火車2擁有三節車箱,標記爲 2.1-2.3
  • 火車3擁有五節車箱,標記爲 3.1-3.5

而對於加入的順序爲:

  • 車箱1.1在車箱1.2的前面,車箱1.2在車箱1.4的前面,以此類推。
  • 火車1的最後一節車箱老是在火車2的第一節車箱前面,因此車箱1.4在車箱2.1以前。同理,車箱2.1在車箱3.1以前。

火車算法每一次執行的時候,只會對一個塊(號碼最低的塊)執行垃圾收集。對於上圖,它會收集車箱1.1,下次執行時會收集車箱1.2。當它收集了火車1的最後一個車箱,算法在下一次執行時收集火車2的車箱2.1。(從這部分看,在收集完一個車箱後,算法應該是要把收集過的車箱移走)。

對象從更年輕的年齡層的子堆進入成熟對象空間,無論什麼時候進入,它們都會被附加到任何已經存在的火車中(最小數字的火車除外),或者專爲容納它們而創建的一列或多列火車中。也就是說,對象有兩種方法到達火車站:

  • 打包成車箱,掛接在==最小數字的火車==以外的火車尾部
  • 做爲一列新的火車開進火車站

==最小數字的火車除外==是爲何呢?
由於算法始終檢測的是最小數字的火車,或者最小數字的火車最小數字的車箱。這列火車不會直接存放剛剛進入火車站的對象。看下面的車箱收集就明白啦!

車箱收集

每一次算法被執行的時候,它要麼收集最小數字火車中的最小數字車箱,要麼收集整列最小數字火車。思路以下:

  • 首先檢查指向最小數字火車中任何車箱的引用,若是不存在任何來自最小數字火車外的引用指向最小數字火車內部的對象,那麼整列火車包含的都是垃圾對象,能夠拋棄。
  • 若是最小數字火車並不都是垃圾,那麼算法把它的注意力放到火車的最小數字車箱上。在這個過程當中,算法將檢測到的被引用的對象轉移到其餘車箱,而後任何保留着車箱裏的對象都是可回收的。

咱們知道有一種循環引用的問題,而對於火車算法來講,保證整列火車中沒有循環的數據結構的關鍵是算法如何移動對象,包括下面幾個規則:

  • 若是正在被收集的==車箱==(這個車箱其實就是最小數字火車最小數字車箱啦)中有一個對象存在來自火車站外的引用,這個對象就被轉移到正在被收集的火車以外的其餘車箱中去。
  • 若是對象被火車站中的其餘火車引用,對象就被轉移到引用它的火車中去。
    • 而後掃描被轉移過去的對象,把它在原車箱所引用的的對象都轉移到引用它們的車箱,這個過程不斷重複,直到沒有任何來自其餘火車的引用指向正在被收集的那節車箱。
    • 若是接受對象的火車車箱沒有空間了,那麼算法會建立新的車箱,並附加到火車的尾部。
  • 一旦沒有火車站外的引用,也沒有火車站內其餘火車的引用,那麼這節正在被收集的車箱剩餘的外部引用都是來自同一列火車的其餘車箱。
    • 算法把這樣的對象移動到最小數字火車的最後一個車箱去。
    • 而後掃描新移動過去的對象,查找是否有引用指向被收集車箱中的對象。
      • 任何新發現的對象也轉移到最小數字火車的最後一個車箱中
      • 而後繼續掃描新對象,整個過程不斷重複
      • 直到沒有任何形式的引用指向被收集的車箱
    • 而後算法歸還整個最小數字車箱佔據的空間,釋放全部仍然留在車箱中的對象,而且返回

所以,在每次執行時,火車算法或者收集最小數字火車的最小數字車箱,或者手機整列最小數字火車。而將對應移動到引用它們的火車中,相關的對象會變得集中。最後,稱爲垃圾的循環數據結構中的全部對象,無論有多大,會放置到同一列火車中去。而增大循環數據結構只會增大最終組成同一列火車的車箱數。前面已經說明,火車算法會先檢查最小數字火車是否徹底就是垃圾,而對於循環數據結構這種內部引用,它徹底能夠完成收集

記憶集合

火車算法的目標是爲了給按代收集的垃圾收集器提供限定時間內的漸進式收集。

對於車箱來講,分配時能夠指定一個最大的內存size,而且每次執行只收集一個車箱,因此大部分狀況下,火車算法能夠保證每次的執行時間在某個最長時間限度內,不過不能確保每一次都是,由於算法執行的過程當中不只僅是拷貝對象。

爲了優化收集過程,火車算法使用了記憶集合。一個記憶集合是一個數據結構,它包含了對一節車箱或者一列火車的外部引用。算法爲火車站(成熟對象空間)內的每節車箱和每列火車都維護了一個記憶集合。因此一節特定車箱的記憶集合記錄了指向車箱內對象的全部引用。一個空的記憶集合顯示車箱或者火車中的對象已經再也不被車箱或者火車外的任何變量引用(被遺忘了)。被遺忘的就是不可觸及的,能夠被回收。

記憶集合的優點

記憶集合是一種能夠幫助火車算法更有效地完成工做的技術。當回車算法發現一節車箱或者一列火車的記憶集合是空的時,它就知道車箱裏面全是垃圾,能夠釋放回收這部分佔用的內存。
而且在移動一個對象到另外一節車箱是,記憶集合中的信息有助於它高效的更新全部指向被移動對象的引用。

限制

咱們能夠經過限制一個車箱的大小來控制每次字節拷貝的上限,可是當移動一個很受歡迎的對象(有不少外部鏈接)時,所須要的工做幾乎是不可能限制的,每次算法移動一個對象時,它必須遍歷對象的記憶集合,更新每個鏈接,以便於使鏈接指向新的地址。由於指向一個對象的鏈接數是沒法限定的,因此更新一個被移動對象的全部鏈接所須要的的時間也沒法限定。

也就是說,在特定條件下,火車算法仍然多是破壞性的。不過除了這種受歡迎的清下不太實用外,火車算法大部分狀況工做的很好。

再次強調下到Sun JDK 6的時候就已經完全不包含train GC了,不事後續的GC策略能和這個差異有多大呢?對吧

終結

Java語言裏,一個對象能夠擁有終結方法:這個方法是垃圾收集器在釋放對象前必需要運行的。而這個可能存在的終結方法使得任何Java虛擬機的垃圾收集器要完成的工做更加複雜。

終結方法

給一個類加上終結方法,只須要這樣:

public class FinalizerTest {
    @Override
    protected void finalize() throws Throwable {
        //do something 
        super.finalize();
    }
}
複製代碼

垃圾收集器必須檢查它所發現的再也不被引用的對象是否存在finalize()方法。

由於,存在終結方法時,Java虛擬機的垃圾收集器必須每次在收集時執行一些額外的步驟:

  • 首先,垃圾收集器必須使用某種方法檢測出再也不被引用的對象(稱爲第一遍掃描)。
  • 而後,它必須檢查它檢測出的再也不被應用的對象是否聲明瞭終結方法。(若是時間容許的話,可能在這個時候垃圾收集器就着手處理這些存在的終結方法)。
  • 當執行了全部的終結方法後,垃圾收集器必須從根節點開始再次檢測再也不被引用的對象(稱爲第二遍掃描)。這個步驟是必要的,由於終結方法可能復活了某些不在引用的對象,使它們再次被引用了。
  • 最後垃圾收集器才能釋放那些在第一次和第二次掃描中發現沒有被引用的對象。

爲了減小釋放內存的時間,在掃描到某些對象擁有終結方法和運行終結方法之間,垃圾收集器能夠有選擇地插入一個步驟:

  • 一旦垃圾收集器執行了第一遍掃描,而且找到了一些再也不被引用的對象須要執行終結方法時,它能夠運行一次小型的追蹤。從須要執行終結方法的對象開始(而非根節點),執行邏輯以下:
    • 任何知足從根節點開始不可觸及&&從將要被終結的對象開始不可觸及這些對象不可能在執行終結方法時復活,它們能夠被當即釋放。
    • 請注意上條標註的兩個條件,是的關係

若是一個帶有終結方法的對象再也不被引用,而且它的總結方法已經執行過了,垃圾收集器必須使用某種方法記住這一點,而不能再次執行這個對象的終結方法。

若是這個對應已經被本身的終結方法或者其餘對象的終結方法復活了,稍後再次再也不被引用,垃圾收集器必須像對待一個沒有終結方法的對象同樣對待它(也就是finalize()只會執行一次的緣由)。

使用Java編程時請記住,是垃圾收集器運行對象的終結方法。由於沒法預測垃圾收集什麼時候觸發,因此咱們也沒法預測對象的終結方法什麼時候執行。

對象可觸及性的生命週期

在版本1.2以前,在垃圾收集器看來,堆中的每個對象都有三種狀態:

  • 可觸及的:垃圾收集器經過根節點能夠追蹤到的對象
  • 可復活的:一旦程序釋放了全部該對象的引用(從根節點追蹤圖中不可觸及),這個對象就變成了可復活狀態。關於可復活,請注意:
    • 不只僅是聲明瞭finalize()方法的對象,而是全部的對象都會通過可復活狀態。
    • 因爲能夠自定義對象的finalize()方法(再次引用一個對象),任何處於可復活狀態的對象均可能再次復活
    • 垃圾收集器會在保證全部可復活對象執行過finalize()(若是聲明瞭的話)後,再把可復活對象的狀態或者轉化爲可觸及,或者轉化爲不可觸及。
  • 不可觸及的:對象再也不被觸及,而且對象不可能經過任何終結方法復活。不可觸及對象再也不對程序執行產生影響,能夠自由回收。

在版本1.2中,對可觸及狀態延伸擴充了三個新狀態:軟可觸及弱可觸及影子可觸及。而原來的可觸及狀態變成了強可觸及。(其實就是咱們編程用到的弱引用、強引用啥的吧)

任何從根節點開始的任何直接引用,好比一個局部變量,是強可觸及。同理,任何由強可觸及對象所引用到的對象也是強可觸及

引用對象(Reference

Java提供了java.lang.rf.Reference類用來管理對象鏈接,包含SoftReferenceWeakReferencePhantomReference三個實現類,繼承圖以下:

image

  • SoftReference:封裝對引用目標的軟引用
  • WeakReference:封裝對引用目標的弱引用
  • PhantomReference:封裝對引用目標的影子引用

強引用和上述三種引用的區別是,強引用禁止引用目標被垃圾收集,而軟引用、弱引用、影子引用不由止。

當須要建立一個Reference的對象時,簡單的把強引用傳遞到對應的Reference實現類的構造方法中去就能夠。以SoftReference爲例:

public class ReferenceTest{
    public static void main(String[] args) {
        Cow c = new Cow();
        SoftReference<Cow> softReference = new SoftReference<Cow>(c);
        c = null;
    }
}
class Cow{}
複製代碼

咱們經過維護softReference來維護關於Cow實例對象的軟引用。引用示意圖以下:

image

SoftReference對象封裝了一個Cow對象的軟引用。SoftReference對象被一個局部變量softReference強引用,==和全部的局部變量同樣,對於與垃圾收集器來講這是一個根節點==(這部分存疑哈)。

一旦一個引用對象建立後,它將一直維持到它的引用目標的軟引用,直到它被程序或者垃圾收集器清除。要清楚一個引用對象,程序或者垃圾收集器只需調用Referece對象的clear()方法。

可觸及狀態的變化

前面講到,引用對象的目的是爲了可以指向某些對象,使這些對象能夠隨時被垃圾收集器收集。換個說法就是,垃圾收集器能夠隨意改變不是強可觸及對象的可觸及狀態。

若是想監聽這種狀態的變化,咱們可使用java.lang.rf.ReferenceQueue<T>類。怎麼用呢,咱們看下Reference的構造方法:

public abstract class Reference<T> {
    Reference(T referent) {
        this(referent, null);
    }
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}
複製代碼

而對於Reference的總體結構,以下圖:

image

==說實話,Reference的體系以前沒怎接觸,只是在Android中簡單使用WeakReferenceget方法,等結束本篇垃圾收集,單獨撩撥一下==

那咱們就能夠這樣寫:

class ReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Cow> referenceQueue = new ReferenceQueue<>();
        Cow c = new Cow();
        WeakReference<Cow> softReference = new WeakReference<Cow>(c, referenceQueue);
        //把對cow的強引用置爲空
        c = null;
        
        softReference.clear();
        System.out.println("clear Reference");

        System.out.println("獲取軟引用下的Cow = " + softReference.get());

        //加入隊列,這一步虛擬機會去作,可是由於時間上的問題,咱們手動觸發一下
        softReference.enqueue();
        
        Reference<? extends Cow> cow = referenceQueue.remove();
        System.out.println("從釋放隊列中獲取Cow = " + cow);

    }
}
class Cow{}
複製代碼

當垃圾收集器決定收集弱可觸及對象的時候,它會清除WeakReference對象中引用的Cow對象(經過clear()方法)。而後可能當即把這個WeakReference對象當即加入它的引用隊列中,也可能在稍後的某個時間加入。

爲了把引用對象加入它所關聯的隊列中,垃圾收集器會執行它的enqueue()方法。只有在建立引用對象時關聯了一個隊列,而且當且僅當該對象第一次執行enqueue()方法時,才把引用對象加入這個隊列中。

在不一樣的狀況下,垃圾收集器把軟引用、弱引用、影子引用對象加入隊列表示三種不一樣的可觸及性狀態的轉變。這一共表示了6中可觸及狀態,狀況以下:

  • 強可觸及:對象能夠從根節點不經過任何引用對象搜索到。對象生命週期從強可觸及狀態開始,而且只要有根節點或者另一個強可觸及對象引用它,就保持強可觸及狀態。垃圾收集器不會試圖回收該狀態下的對象。
  • 軟可觸及:對象不是強可觸及,可是能夠從根節點開始經過一個或者多個軟引用對象觸及。垃圾收集器==可能==回收軟可觸及對象。若是發生了,它會清除全部到此軟可觸及對象的軟引用。當垃圾收集器清除一個和引用隊列有關聯的軟引用對象時,它會把該軟引用對象加入隊列。
  • 弱可觸及:對象既不是強可觸及也不是軟可觸及,可是從根節點開始能夠經過一個或多個弱引用對象觸及。垃圾收集器==必須==回收弱可觸及對象佔用的內存。垃圾收集器回收時,它會清除全部到此弱可觸及對象的弱引用,並將弱引用對象加入隊列(若是有關聯的話)
  • 可復活的:對象既不是強可觸及、軟可觸及、也不是弱可觸及,可是仍然可能經過某些終結方法復活到這幾個狀態之一。
  • 影子可觸及:對象既不是強可觸及、軟可觸及、也不是弱可觸及,而且已經判定不會被任何終結方法復活(若是對象定義了終結方法。此時終結方法已經被執行過了),而且它能夠從根節點開始經過一個或多個影子引用對象觸及。一個某個影子引用的對象變成影子可觸及狀態,垃圾收集器馬上把該引用對象加入隊列。==垃圾收集器不會清除一個影子引用,全部的影子引用必須由程序明確地清除==
  • 不可觸及:一個對象不是強可觸及、軟可觸及、弱可觸及,也不是影子可觸及,而且他不可復活。不可觸及對象已經準備好被回收了。

請注意,垃圾收集器再把軟引用和弱引用對象加入關聯隊列時,是在他們的引用目標離開相應的可觸及狀態時(調用clear
而影子引用對象加入隊列是在引用目標進入相應狀態時(也就是構造一個影子引用對象,並執行enqueue()後)。
也就是說垃圾收集器把軟引用或者弱引用對象加入隊列標誌着引用對象剛剛離開了軟可觸及或者弱可觸及狀態;而垃圾收集器把影子引用加入隊列標誌着引用目標已經進入了影子可觸及狀態。==影子可觸及對象會保持影子可觸及狀態,直到程序顯式地清除了引用對象==。

不一樣類型引用的用法

垃圾收集器對待軟、弱和影子對象的方法不一樣,是由於每一種都是被設計成爲程序提供不一樣的服務。

  • 軟引用能夠建立內存中的緩存,它與程序的總體內存需求有關。
  • 弱引用能夠建立規範映射,好比哈希表,它的關鍵字和值在沒有其餘引用時能夠從映射表中清除。
  • 影子引用能夠實現除終結方法之外的更加複雜的臨終清理政策。

影子引用的一些注意事項

請注意,要使用軟引用或者弱引用的引用目標,能夠調用對象的get()方法。若是引用目標沒有被清除,則返回被引用的對象;若是被清除了,則返回null

可是對於影子引用對象的get()方法,始終返回null;咱們看下PhantomReference的源碼

public class PhantomReference<T> extends Reference<T> {
    public T get() {
        return null;
    }
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}
複製代碼

真滴是簡潔啊。。。。==爲何要這樣呢?==
咱們前面描述了6個狀態,而對於影子可觸及狀態來講,它表示對象是不可復活的。若是影子引用的get()方法有返回對象的話,那麼這個規則就要被打破了。
請記住==若是一個對象達到了影子可觸及狀態,它不能再復活。==

不過虛擬機設計的真的這麼嚴謹麼?
咱們看下面的代碼:

public static void main(String[] args) {
        ReferenceQueue<Cow> referenceQueue = new ReferenceQueue<>();
        Cow c = new Cow();
        PhantomReference<Cow> softReference = new PhantomReference<Cow>(c, referenceQueue);
        //把對cow的強引用置爲空
        c = null;

        System.out.println("get()獲取影子引用下的Cow = " + softReference.get());

        //加入隊列,這一步虛擬機會去作,可是由於時間上的問題,咱們手動觸發一下
        softReference.enqueue();
        
        //remove 後就能夠取得影子引用對象了
        Reference<? extends Cow> cow = referenceQueue.remove();
        
        //上面的get()沒取到對象,那咱們反射試一下
        Field field = Reference.class.getDeclaredField("referent");
        field.setAccessible(true);
        Object obj = field.get(cow);
        System.out.println("從釋放隊列中獲取Cow = " + obj);
        
        //手動釋放一下
        cow.clear();
        
        //再反射獲取一下
        obj = field.get(cow);
        System.out.println("從釋放隊列中獲取Cow = " + obj);
    }
複製代碼

輸出以下:

獲取影子引用下的Cow = null
從釋放隊列中獲取Cow = hua.lee.jvm.Cow@60e53b93
從釋放隊列中獲取Cow = null
複製代碼

咱們看到,==影子可觸及狀態的對象也是能夠被拿出來的嘛==
另外,有一點須要注意的是==影子可觸及狀態的對象是不會被垃圾收集器給回收的,咱們須要像上面的示例同樣手動clear()來釋放對象==

軟引用的經常使用場景

虛擬機的實現須要在拋出OOM以前清除掉軟引用,但在其餘狀況下能夠自行選擇清理的時間或者是否清除。實現最好是隻在內存不足的狀況下才去清除軟引用,清除的時候先清除老的而不是新的,清除長期未用的而不是最近使用的。

軟引用可讓你在內存中緩存那些須要從外部費時獲取的數據,好比文件中、數據庫裏或者網絡上的數據。
只要虛擬機有足夠的內存,能夠在堆中保存全部的強引用數據和軟引用數據。
若是內存緊張,垃圾收集器會決定清除軟引用,回收被軟引用的數據所佔用的空間。下一次程序須要使用這個數據時,可能不得再也不次從外部數據源進行加載。

弱引用的經常使用場景

弱引用相似於軟引用,但不一樣的是:

  • 垃圾收集器能夠自行決定是否清除處於軟可觸及狀態的對象的軟引用。
  • 而對於弱可觸及狀態的對象,垃圾收集器會當即清除相關的弱引用。

弱引用的這種特性使得咱們能夠用關鍵字和值來建立規範映射。java.lang.WeakHashMap類就是用弱引用提供這樣的規範映射。
能夠經過put()方法加入鍵值對到WeakHashMap的實例。不過和HashMap不一樣的是,在WeakHashMap中,關鍵字對象是經過一個關聯到引用隊列的弱引用實現的。若是垃圾收集器檢測到關鍵字對象時弱可觸及的,它會清除引用而且把弱引用到該對象的引用對象加入到各自的隊列。下次WeakHashMap被訪問的時候,它會從引用隊列中拉出全部的被垃圾收集器存放的弱引用對象,並清除和其有關的映射關係。

相關文章
相關標籤/搜索