JVM如何回收對象--初識

​ 垃圾收集器回收的是什麼?固然是"垃圾"。那什麼是"垃圾"呢?在生活中當一個東西對咱們來講已經沒有使用價值,沒法利用時,便會做爲"垃圾"被咱們丟棄掉。同理在JVM中當一個對象不會再被使用時,就成爲垃圾回收器回收的對象---"垃圾"。爲何要回收"垃圾"呢?生活中,咱們的房間空間是必定的,若是不清理"垃圾",會使得咱們可利用的空間會愈來愈小,JVM中若是沒有垃圾回收,也就是所佔據的空間將不可回收,這就會形成了內存泄露。java

​ 讀過上文的應該知道,Java中幾乎全部的對象實例都在Java堆中分配內存,方法區用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。因此垃圾收集器進行回收的區域是:堆和方法區中算法

如何判斷是否爲"垃圾"

引用計數算法

​ 引用計數算法是最古老的辨別方法。首先在對象中添加一個引用計數器,初始值爲零,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;因此任什麼時候刻當一個對象的計數器值爲零時,表明此對象沒有被任何地方引用,也就是垃圾收集器要回收的"垃圾"。緩存

​ 客觀地說,引用計數算法雖然佔用了一些額外的內存空間來進行計數,但它的原理簡單,斷定效率也很高,在大多數狀況下它都是一個不錯的算法。可是在Java 領域,至少主流的Java虛擬機裏面都沒有選用引用計數算法來管理內存,主要緣由是,這個看似簡單的算法有不少例外狀況要考慮,必需要配合大量額外處理才能保證正確地工做,譬如單純的引用計數就很難解決對象之間相互循環引用的問題。安全

所謂對象之間的相互引用問題:除了對象a和b相互引用着對方以外,這兩個對象之間再無任何引用。可是它們由於互相引用對方,致使它們的引用計數器都不爲0,因而引用計數器法沒法通知GC回收器回收它們。以下圖紅色區域。數據結構

可達性分析算法

​ 基本思路就是經過 一系列稱爲「GC Roots」的根對象做爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱爲「引用鏈」,若是某個對象能夠根據引用鏈到達GC Root,那麼說明這個對象被引用中,反之,則證實此對象是不可能再被使用的。以下圖所示。jvm

在Java技術體系裏面,固定可做爲GC Roots的對象包括如下幾種:佈局

  • 在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的 參數、局部變量、臨時變量等。
  • 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
  • 在方法區中常量引用的對象,譬如字符串常量池(String Table)裏的引用。
  • 在本地方法棧中JNI(即一般所說的Native方法)引用的對象。
  • Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(好比 NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
  • 全部被同步鎖(synchronized關鍵字)持有的對象。
  • 反映Java虛擬機內部狀況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。

如何判斷一個常量是廢棄常量

運行時常量池主要回收的是廢棄的常量。那麼,咱們怎麼判斷一個常量時廢棄常量呢?優化

​ 假如在常量池中存在字符串"abc",若是當前沒有任何String對象引用該字符串常量的話,就說明常量」abc「就是廢棄常量,若是這時發生內存回收的話並且有必要的話,」abc「會被系統清理出常量池。線程

如何判斷一個類是無用的類

須要知足如下三個條件:設計

  • 該類全部的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  • 加載該類的 ClassLoader 已經被回收。
  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

虛擬機能夠對知足上述3個條件的無用類進行回收,這裏僅僅是」能夠「,而並非和對象同樣不適用了就必然會被回收。

引用

​ 不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象是否引用鏈可達,斷定對象是否存活都和「引用」離不開關係。在JDK 1.2版以前,Java裏面的引用是很傳統的定義: 若是reference類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱該reference數據是表明某塊內存、某個對象的引用。

​ 在JDK 1.2版以後,Java對引用的概念進行了擴充,將引用分爲強引用、軟 引用、弱引用和虛引用4種,這4種引用強度依次逐漸減弱。

強引用

強引用是最傳統的「引用」的定義,是指在程序代碼之中廣泛存在的引用賦值,即相似「Object obj=new Object()」這種引用關係。不管任何狀況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象。當內存空間不足時也不回收,將拋出OutOfMemoryError異常,使程序異常終止。若是想中斷強引用和某個對象之間的關聯,能夠顯示地將引用賦值爲null,這樣一來的話,JVM在合適的時間就會回收該對象。

軟引用

軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關聯着的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,若是此次回收尚未足夠的內存, 纔會拋出內存溢出異常。所以,這一點能夠很好地用來解決OOM的問題,而且這個特性很適合用來實現緩存:好比網頁緩存、圖片緩存等。在JDK 1.2版以後提供了SoftReference類來實現軟引用。

弱引用

弱引用也是用來描述那些非必須對象,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生爲止。當垃圾收集器開始工做,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。也就是說軟引用關聯的對象只有在內存不足時纔會被回收,而被弱引用關聯的對象在JVM進行垃圾回收時總會被回收。在JDK 1.2版以後提供了WeakReference類來實現弱引用。

虛引用

虛引用也稱爲「幽靈引用」或者「幻影引用」,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的只是爲了能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2版以後提供 了PhantomReference類來實現虛引用。

小結

引用類型 被回收時間 用途 生存時間
強引用 歷來不會 對象的通常狀態 JVM中止運行時
軟引用 內存不足時 對象緩存 內存不足時
弱引用 jvm垃圾回收時 對象緩存 gc運行後
虛引用 未知 未知 未知

垃圾回收算法

分代收集算法

​ 當前商業虛擬機的垃圾收集器,大多數都遵循了「分代收集」的理論進行設計,分代收集名爲理論,實質是一套符合大多數程序運行實際狀況的經驗法則,它是在創建分代假說之上:

​ 1)弱分代假說:絕大多數對象都是朝生夕滅的。

​ 2)強分代假說:熬過越屢次垃圾收集過程的對象就越難以消亡。

​ 3)跨代引用假說:跨代引用相對於同代引用來講僅佔極少數。

​ 1)2)這兩個分代假說共同奠基了多款經常使用的垃圾收集器的一致的設計原則:收集器應該將Java堆劃分出不一樣的區域,而後將回收對象依據其年齡(年齡即對象熬過垃圾收集過程的次數)分配到不一樣的區域之中存儲。根據前兩條假說邏輯推理得出的隱含推論:存在互相引用關係的兩個對象,是應該傾向於同時生存或者同時消亡的。

​ 依據3)這條假說,咱們就不該再爲了少許的跨代引用去掃描整個老年代,也沒必要浪費空間專門記錄每個對象是否存在及存在哪些跨代引用,只需在新生代上創建一個全局的數據結構(該結構被稱爲「記憶集」,Remembered Set),這個結構把老年代劃分紅若干小塊,標識出老年代的哪一塊內存會存在跨代引用。此後當發生Minor GC時,只有包含了跨代引用的小塊內存裏的對象纔會被加入到GC Roots進行掃描。雖然這種方法須要在對象改變引用關係時維護記錄數據的正確性,會增長一些運行時的開銷,但比起收集時掃描整個老年代來講仍然是划算的。

​ IBM公司曾有一項專門研究對新生代「朝生夕滅」的特色作了更量化的詮釋——新生代中的對象有98%熬不過第一輪收集。所以並不須要按照1∶1的比例來劃分新生代的內存空間。

​ 在1989年,Andrew Appel針對具有「朝生夕滅」特色的對象,提出了一種更優化的半區複製分代策略,如今稱爲「Appel式回收」。HotSpot虛擬機的Serial、ParNew等新生代收集器均採用了這種策略來設計新生代的內存佈局。Appel式回收的具體作法是把新生代分爲一塊較大的Eden空間和兩塊較小的 Survivor空間,每次分配內存只使用Eden和其中一塊Survivor。發生垃圾蒐集時,將Eden和Survivor中仍 然存活的對象一次性複製到另一塊Survivor空間上,而後直接清理掉Eden和已用過的那塊Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內存空間爲整個新生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會被「浪費」的。固然,98%的對象可被回收僅僅是「普通場景」下測得的數據,任何人都沒有辦法百分百保證每次回收都只有很少於10%的對象存活,所以Appel式回收還有一個充當罕見狀況的「逃生門」的安全設計,當Survivor空間不足以容納一次Minor GC以後存活的對象時,就須要依賴其餘內存區域(實際上大多就是老年代)進行分配擔保。

標記-清除算法

它是最基礎的收集算法,之因此說它是最基礎的收集算法,是由於後續的收集算法大多都是以標記-清除算法爲基礎,對其缺點進行改進而獲得的。這個算法分爲兩個階段,「標記」和」清除「。首先標記出全部須要回收的對象,標記過程就是對象是否屬於垃圾的斷定過程。在標記完成後統一回收全部被標記的對象。也能夠反過來,標記存活的對象,統一回收全部未被標記的對象。它有兩個不足的地方:

  1. 效率問題,標記和清除兩個過程的效率都不高;
  2. 空間問題,標記清除後會產生大量不連續的碎片。

標記-複製算法

​ 標記-複製算法常被簡稱爲複製算法。爲了解決面對大量可回收對象時執行效率低的問題,複製算法出現了。它能夠把內存分爲大小相同的兩塊,每次只使用其中的一塊。當這一塊的內存使用完後,就將還存活的對象複製到另外一塊區,而後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收

​ 若是內存中多數對象都是存活的,這種算法將會產生大量的內存間複製的開銷,但對於多數對象都是可回收的狀況,算法須要複製的就是佔少數的存活對象,並且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的複雜狀況,只要移動堆頂針,按順序分配便可。這樣實現簡單,運行高效,不過其缺陷也顯而易見,這種複製回收算法的代價是將可用內存縮小爲了原來的一半,空間浪費未免太多了一 點。

​ 如今的商用Java虛擬機大多都優先採用了這種收集算法去回收新生代。

標記-整理算法

​ 標記-複製算法在對象存活率較高時就要進行較多的複製操做,效率將會下降。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此在老年代通常不能直接選用這種算法。

​ 根據老年代的特色提出的一種標記算法,標記過程和「標記-清除」算法同樣,可是後續步驟不是直接對可回收對象進行回收,而是讓全部存活的對象向一段移動,而後直接清理掉邊界之外的內存。

巨人肩膀

周志明《深刻理解Java虛擬機:JVM高級特性與最佳實踐》

相關文章
相關標籤/搜索