天天看點

全面詳解Java垃圾回收器(一)

一:什麼是垃圾回收

Java 方法棧、本地方法棧随着方法結束或者線程結束,堆中的對象是用完,都會進行回收記憶體,是以這些區域的記憶體配置設定和回收都具備确定性,不需要額外考慮回收的問題。而堆和方法區存儲的對象可能隻有在運作期間才确定的,這部分記憶體的配置設定和回收都是動态的,垃圾回收關注的也是這部分。而由于堆中存儲的是大對象,是最容易 OOM 的地方。是以垃圾回收主要針對的是堆的。

那怎樣才能确定一個對象是否需要回收? 這就涉及到了垃圾判斷算法,其主要包括引用計數法和可達性分析法,在說垃圾回收算法前,我們先了解下對象的引用關系。

二:對象的引用關系

全面詳解Java垃圾回收器(一)
  • 強引用

如果一個對象具有強引用,那垃圾回收器絕不會回收它,當記憶體空間不足,Java虛拟機甯願抛出OutOfMemoryError錯誤,使程式異常終止,也不會靠随意回收具有強引用的對象來解決記憶體不足的問題;Gcroot就是一種引用的存在     

  • 弱引用

如果一個對象隻具有弱引用,那就類似于可有可物的生活用品。弱引用與軟引用的差別在于:隻具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它 所管轄的記憶體區域的過程中,一旦發現了隻具有弱引用的對象,不管目前記憶體空間足夠與否,都會回收它的記憶體。不過,由于垃圾回收器是一個優先級很低的線程, 是以不一定會很快發現那些隻具有弱引用的對象。如果這個對象是偶爾的使用,并且希望在使用時随時就能擷取到,但又不想影響此對象的垃圾收集,那麼你應該用 Weak Reference 來記住此對象

  • 軟引用

軟引用是用來描述一些有用但并不是必需的對象,在Java中用java.lang.ref.SoftReference類來表示。對于軟引用關聯着的對象,隻有在記憶體不足的時候JVM才會回收該對象。是以,這一點可以很好地用來解決OOM的問題,并且這個特性很适合用來實作緩存:比如網頁緩存、圖檔緩存等。(1)如果一個網頁在浏覽結束時就進行内容的回收,則按後退檢視前面浏覽過的頁面時,需要重新建構(2)如果将浏覽過的網頁存儲到記憶體中會造成記憶體的大量浪費,甚至會造成記憶體溢出這時候就可以使用軟引用

Object o = new Object();
SoftReference softReference = new SoftReference(o);
o = null;
Object o1 = softReference.get();
System.out.println(o1 == null);
           
  • 虛引用

如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個差別在于:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之關聯的引用隊列中。程式可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否将要被垃圾回收。程式如果發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的記憶體被回收之前采取必要的行動。

特别注意,在實際程式設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速JVM對垃圾記憶體的回收速度,可以維護系統的運作安全,防止記憶體溢出(OutOfMemory)等問題的産生

三:對象建立的記憶體配置設定

全面詳解Java垃圾回收器(一)

Java技術體系中所提倡的自動記憶體管理最終可以歸結為自動化地解決了兩個問題:給對象配置設定記憶體 以及 回收配置設定給對象的記憶體。一般而言,對象主要配置設定在新生代的Eden區上,如果啟動了本地線程配置設定緩存(TLAB),将按線程優先在TLAB上配置設定。少數情況下也可能直接配置設定在老年代中。總的來說,記憶體配置設定規則并不是一層不變的,其細節取決于目前使用的是哪一種垃圾收集器組合,還有虛拟機中與記憶體相關的參數的設定。

  1.  大對象直接進入老年代。所謂的大對象是指,需要大量連續記憶體空間的Java對象,最典型的大對象就是那種很長的字元串以及數組。
  2. 長期存活的對象将進入老年代。當對象在新生代中經曆過一定次數(預設為15)的Minor GC後,就會被晉升到老年代中。
  3. 動态對象年齡判定。為了更好地适應不同程式的記憶體狀況,虛拟機并不是永遠地要求對象年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
  4. 空間配置設定擔保。 對象優先在Eden配置設定,當Eden區沒有足夠空間進行配置設定時,虛拟機将發起一次MinorGC。現在的商業虛拟機一般都采用複制算法來回收新生代,将記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。 當進行垃圾回收時,将Eden和Survivor中還存活的對象一次性地複制到另外一塊Survivor空間上,最後處理掉Eden和剛才的Survivor空間。(HotSpot虛拟機預設Eden和Survivor的大小比例是8:1)當Survivor空間不夠用時,需要依賴老年代進行配置設定擔保。
全面詳解Java垃圾回收器(一)

四:垃圾回收算法

  1. 引用計數法
全面詳解Java垃圾回收器(一)

綠色的小雲表示它們指向的objects仍然在被使用。這些可能是正在執行的方法中的局部變量,或是靜态變量等等。

藍色的小環代表記憶體裡目前活躍的objects,上面的數字表示它的引用計數。

最後,灰色的小環表示沒有被任何目前在使用的object(也就是之别被綠色的小雲引用的)引用的objects。也就是說,灰色的小環就是需要被垃圾回收器清理的垃圾。

但是它有一個很大的問題,即:如果是存在一個獨立的有向回環的話,則這些object永遠不會被回收

全面詳解Java垃圾回收器(一)

紅色的圓環其實是需要被收集的垃圾,但是由于互相引用,引用計數不為1,是以不會被回收。是以這個方法仍舊會造成memory leak。

  2、可達性分析算法

全面詳解Java垃圾回收器(一)

可達性分析法也被稱之為根搜尋法,可達性是指,如果一個對象會被至少一個在程式中的變量通過直接或間接的方式被其他可達的對象引用,則稱該對象就是可達的。更準确的說,一個對象隻有滿足下述兩個條件之一,就會被判斷為可達的:

  • 對象是屬于根集中的對象
  • 對象被一個可達的對象引用

根集,其是指正在執行的 Java 程式可以通路的引用變量(注意,不是對象)的集合,程式可以使用引用變量通路對象的屬性和調用對象的方法。在 JVM 中,會将以下對象标記為根集中的對象,具體包括:

1、虛拟機棧中(棧幀中的本地變量表)中引用的對象

2、方法區中類靜态屬性引用的對象和常量引用的對象

3、本地方法棧中JNO(即一般說的native方法)引用的對象

五:算法的執行規則

  • 标記清除

标記-清除算法分為标記和清除兩個階段。該算法首先從根集合進行掃描,對存活的對象對象标記,标記完畢後,再掃描整個空間中未被标記的對象并進行回收,标記-清除算法的主要不足有兩個:

1、效率問題:标記和清除兩個過程的效率都不高;

2、空間問題:标記-清除算法不需要進行對象的移動,并且僅對不存活的對象進行處理,是以标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻以後在程式運作過程中需要配置設定較大對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

全面詳解Java垃圾回收器(一)
  • 标記整理

标記-整理算法标記的過程與“标記-清除”算法中的标記過程一樣,但對标記後出的垃圾對象的處理情況有所不同,它不是直接對可回收對象進行清理,而是讓所有的對象都向一端移動,然後直接清理掉端邊界以外的記憶體

       優點:經過整理之後,新對象的配置設定隻需要通過指針碰撞便能完成,比較簡單;使用這種方法,空閑區域的位置是始終可知的,也不會再有碎片的問題了。

       缺點:GC 暫停的時間會增長,因為你需要将所有的對象都拷貝到一個新的地方,還得更新它們的引用位址。

全面詳解Java垃圾回收器(一)
  • 複制算法

複制算法是為了克服句柄的開銷和解決堆碎片的垃圾回收。它将記憶體按容量分為大小相等的兩塊,每次隻使用其中的一塊,當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊記憶體上面,然後再把已使用過的記憶體空間一次清理掉。

複制算法比較适合于新生代(短生存期的對象),在老年代(長生存期的對象)中,對象存活率比較高,如果執行較多的複制操作,效率将會變低,是以老年代一般會選用其他算法,如标記—整理算法。

      優點:(1)标記階段和複制階段可以同時進行。

                (2)每次隻對一塊記憶體進行回收,運作高效。

                (3)隻需移動棧頂指針,按順序配置設定記憶體即可,實作簡單。

                (4)記憶體回收時不用考慮記憶體碎片的出現(得活動對象所占的記憶體空間之間沒有空閑間隔)。

      缺點:需要一塊能容納下所有存活對象的額外的記憶體空間。是以,可一次性配置設定的最大記憶體縮小了一半。

全面詳解Java垃圾回收器(一)
  • 分代算法

  由上一遍文章JVM記憶體模型可知,堆記憶體劃分為新生代、老年代。新生代又被進一步劃分為 Eden 和 Survivor 區,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)組成。所有通過new建立的對象的記憶體都在堆中配置設定,其大小可以通過-Xmx和-Xms來控制。分代收集,是基于這樣一個事實:不同的對象的生命周期是不一樣的。是以,可以将不同生命周期的對象分代,不同的代采取不同的回收算法進行垃圾回收,以便提高回收效率

全面詳解Java垃圾回收器(一)
  • 新生代(Young Generation)

幾乎所有新生成的對象首先都是放在年輕代的。新生代記憶體按照 8:1:1 的比例分為一個 Eden 區和兩個 Survivor(Survivor0,Survivor1)區。大部分對象在 Eden 區中生成。當新對象生成,Eden 空間申請失敗(因為空間不足等),則會發起一次 GC(Scavenge GC)。回收時先将 Eden 區存活對象複制到一個 Survivor0 區,然後清空 Eden 區,當這個 Survivor0 區也存放滿了時,則将 Eden 區和 Survivor0 區存活對象複制到另一個 Survivor1 區,然後清空 Eden 和這個 Survivor0 區,此時 Survivor0 區是空的,然後将 Survivor0 區和 Survivor1 區交換,即保持 Survivor1 區為空, 如此往複。當 Survivor1 區不足以存放 Eden 和 Survivor0 的存活對象時,就将存活對象直接存放到老年代。當對象在 Survivor 區躲過一次 GC 的話,其對象年齡便會加 1,預設情況下,如果對象年齡達到 15 歲,就會移動到老年代中。若是老年代也滿了就會觸發一次 Full GC,也就是新生代、老年代都進行回收。新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制 Eden 和 Survivor 的比例。

  • 老年代(Old Generation)

在新生代中經曆了 N 次垃圾回收後仍然存活的對象,就會被放到年老代中。是以,可以認為年老代中存放的都是一些生命周期較長的對象。記憶體比新生代也大很多(大概比例是 1:2),當老年代記憶體滿時觸發 Major GC 即 Full GC,Full GC 發生頻率比較低,老年代對象存活時間比較長,存活率高。一般來說,大對象會被直接配置設定到老年代。所謂的大對象是指需要大量連續存儲空間的對象,最常見的一種大對象就是大數組。當然配置設定的規則并不是百分之百固定的,這要取決于目前使用的是哪種垃圾收集器組合和 JVM 的相關參數

擔心篇幅過長。是以本文主要從什麼是垃圾回收、對象的引用關系、垃圾的算法、算法的執行規則說明了JVM中垃圾收集器回收垃圾的執行規則與标準,下一篇将會講解垃圾回收機的分類以及如何進行回收過程。

格局越大的人,越不愛糾纏

全面詳解Java垃圾回收器(一)