談談JVM垃圾回收

Tips:關注公衆號:松花皮蛋的黑板報,領取程序員月薪25K+祕籍,進軍BAT必備!
java


Java堆中存放着大量的Java對象實例,在垃圾收集器回收內存前,第一件事情就是肯定哪些對象是「活着的」,哪些是能夠回收的。程序員

引用計數算法

引用計數算法是判斷對象是否存活的基本算法:給每一個對象添加一個引用計數器,沒當一個地方引用它的時候,計數器值加1;當引用失效後,計數器值減1。可是這種方法有一個致命的缺陷,當兩個對象相互引用時會致使這兩個都沒法被回收。算法

根搜索算法

在主流的商用語言中(Java、C#…)都是使用根搜索算法來判斷對象是否存活。對於程序來講,根對象老是能夠訪問的。*從這些根對象開始,任何能夠被觸及的對象都被認爲是」活着的」的對象。沒法觸及的對象被認爲是垃圾,須要被回收*。緩存

Java虛擬機的根對象集合根據實現不一樣而不一樣,可是總會包含如下幾個方面: - 虛擬機棧(棧幀中的本地變量表)中引用的對象。 - 方法區中的類靜態屬性引用的變量。 - 方法區中的常量引用的變量。 - 本地方法JNI的引用對象。微信

區分活動對象和垃圾的兩個基本方法是引用計數和根搜索。 引用計數是經過爲堆中每一個對象保存一個計數來區分活動對象和垃圾。根搜索算法其實是追蹤從根結點開始的引用圖。多線程

引用對象

引用對象封裝了指向其餘對象的鏈接:被指向的對象稱爲引用目標。Reference有三個直接子類SoftReferenceWeakReferencePhantomReference分別表明:軟引用、弱引用、虛引用。強引用在Java中是廣泛存在的,相似Object o = new Object();這類引用就是強引用,強引用和以上引用的區別在於:強引用禁止引用目標被垃圾收集器收集,而其餘引用不由止。併發

當使用軟引用、弱引用、虛引用時,而且對可觸及性狀態的改變有興趣,能夠把引用對象和引用隊列關聯起來。函數

對象有六種可觸及狀態變化:this

  • 強可觸及:對象能夠從根節點不經過任何引用對象搜索到。垃圾收集器不會回收這個對象的內存空間。spa

  • 軟可觸及:對象能夠從根節點經過一個或多個(未被清除的)軟引用對象觸及,垃圾收集器在要發生內存溢出前將這些對象列入回收範圍中進行回收,若是該軟引用對象和引用隊列相關聯,它會把該軟引用對象加入隊列。

SoftReference能夠用來建立內存中緩存,JVM的實現須要在拋出OutOfMemoryError以前清除軟引用,但在其餘的狀況下能夠選擇清理的時間或者是否清除它們。

  • 弱可觸及:對象能夠從根節點開始經過一個或多個(未被清除的)弱引用對象觸及,垃圾收集器在一次GC的時候會回收全部的弱引用對象,若是該弱引用對象和引用隊列相關聯,它會把該弱引用對象加入隊列。

  • 可復活的:對象既不是強可觸及、軟可觸及、也不是弱可觸及,但仍然可能經過執行某些終結方法復活到這幾個狀態之一。

Java類能夠經過重寫finalize方法復活準備回收的對象,但finalize方法只是在對象第一次回收時會調用。

  • 虛可觸及:垃圾收集器不會清除一個虛引用,全部的虛引用都必須由程序明確的清除。 同時也不能經過虛引用來取得一個對象的實例。

  • 不可觸及:不可觸及對象已經準備好回收了。

若一個對象的引用類型有多個,那到底如何判斷它的可達性呢?其實規則以下: 1. 單條引用鏈的可達性以最弱的一個引用類型來決定; 2. 多條引用鏈的可達性以最強的一個引用類型來決定;

垃圾回收算法

標記–清除算法

首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象,標記的方法使用根搜索算法。主要有兩個缺點:

  • 效率問題,標記和清除的效率都不高。

  • 空間問題,標記清除後會產生大量不連續的內存碎片。

複製回收算法

將可用內存分爲大小相等的兩份,在同一時刻只使用其中的一份。當這一分內存使用完了,就將還存活的對象複製到另外一份上,而後將這一份上的內存清空。複製算法能有效避免內存碎片,可是算法須要將內存一分爲二,致使內存使用率大大下降。

標記–整理算法

複製算法在對象存活率較高的狀況下會複製不少的對象,效率會很低。標記–整理算法就解決了這樣的問題,標記過程和標記–清除算法同樣,但後續是將全部存活的對象都移動到內存的一端,而後清理掉端外界的對象。

分代回收(HotSpot)

在JVM中不一樣的對象擁有不一樣的生命週期,所以對於不一樣生命週期的對象也能夠採用不一樣的垃圾回收方法,以提升效率,這就是分代回收算法的核心思想。

在不進行對象存活時間區分的狀況下,每次垃圾回收都是對整個堆空間進行回收,花費的時間相對會長。同時,由於每次回收都須要遍歷全部存活對象,但實際上,對於生命週期長的對象而言,這種遍歷是沒有效果的,由於可能進行了不少次遍歷,可是他們依舊存在。所以,分代垃圾回收採用分治的思想,進行代的劃分,把不一樣生命週期的對象放在不一樣代上,不一樣代上採用最適合它的垃圾回收方式進行回收。

JVM中的共劃分爲三個代:新生代(Young Generation)老年代(Old Generation)永久代(Permanent Generation)。其中永久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對象關係不大。

  • 新生代:全部新生成的對象首先都是放在新生代的,新生代採用複製回收算法。新生代的目標就是儘量快速的收集掉那些生命週期短的對象。新生代分三個區。一個Eden區,兩個Survivor區(通常而言)。大部分對象在Eden區中生成。當Eden區滿時,還存活的對象將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活對象將被複制到另一個Survivor區,當一個對象通過屢次的 GC 後尚未被回收,那麼它將被移動到「年老區(Tenured)」。須要注意,Survivor 的兩個區是對稱的,沒前後關係,因此同一個區中可能同時存在從 Eden 複製過來 對象,和從前一個 Survivor 複製過來的對象,而複製到年老區的只有從第一個 Survivor 去過來的對象。並且,Survivor 區總有一個是空的。 > 在HotSpot虛擬機內部默認Eden和Survivor的大小比例是8:1, 也就是每次新生代中可用內存爲整個新生代的90%,這大大提升了複製回收算法的效率。

  • 老年代:在新生代中經歷了N次垃圾回收後仍然存活的對象,就會被放到老年代中,老年代採用標記整理回收算法。所以,能夠認爲老年代中存放的都是一些生命週期較長的對象。

  • 永久代:HotSpot 的方法區實現,用於存儲類信息、常量池、靜態變量、JIT編譯後的代碼等數據

HotSpot 各版本永久代變化

  • 在Java 6中,方法區中包含的數據,除了JIT編譯生成的代碼存放在native memoryCodeCache區域,其餘都存放在永久代;
  • 在Java 7中,Symbol 的存儲從 PermGen 移動到了 native memory ,而且把靜態變量從instanceKlass末尾(位於PermGen內)移動到了java.lang.Class對象的末尾(位於普通Java heap內);
  • 在Java 8中,永久代被完全移除,取而代之的是另外一塊與堆不相連的本地內存——元空間(Metaspace),‑XX:MaxPermSize 參數失去了意義,取而代之的是-XX:MaxMetaspaceSize

移除永久代

Java 8 完全將永久代 (PermGen) 移除出了 HotSpot JVM,將其原有的數據遷移至 Java HeapMetaspace

HotSpot JVM 中,永久代中用於存放類和方法的元數據以及常量池,好比ClassMethod。每當一個類初次被加載的時候,它的元數據都會放到永久代中。

永久代是有大小限制的,所以若是加載的類太多,頗有可能致使永久代內存溢出,即萬惡的 java.lang.OutOfMemoryError: PermGen ,爲此咱們不得不對虛擬機作調優。

那麼,Java 8PermGen 爲何被移出 HotSpot JVM 了?

  • 因爲 · 內存常常會溢出,引起惱人的 java.lang.OutOfMemoryError: PermGen,所以 JVM 的開發者但願這一塊內存能夠更靈活地被管理,不要再常常出現這樣的 OOM
  • 移除 PermGen 能夠促進HotSpot JVMJRockit VM 的融合,由於 JRockit 沒有永久代。

根據上面的各類緣由,PermGen 最終被移除,方法區移至 Metaspace,字符串常量移至 Java Heap。

元空間

首先,Metaspace(元空間)是哪一塊區域?官方的解釋是:

In JDK 8, classes metadata is now stored in the native heap and this space is called Metaspace.

也就是說,JDK 8 開始把類的元數據放到本地堆內存(native heap)中,這一塊區域就叫 Metaspace,中文名叫元空間。

垃圾回收觸發條件

因爲對象進行了分代處理,所以垃圾回收區域、時間也不同。GC有兩種類型:Scavenge GC和Full GC。對於一個擁有終結方法的對象,在垃圾收集器釋放對象前必須執行終結方法。可是當垃圾收集器第二次收集這個對象時便不會再次調用終結方法。

Scavenge GC

通常狀況下,當新對象生成,而且在 Eden 申請空間失敗時,就會觸發 Scavenge GC,對 Eden 區域進行 GC ,清除非存活對象,而且把尚且存活的對象移動到 Survivor 區,而後整理 Survivor 的兩個區。這種方式的 GC是對新生代的 Eden 區進行,不會影響到老年代。由於大部分對象都是從 Eden 區開始的,同時 Eden 區不會分配的很大,因此 Eden 區的 GC 會頻繁進行。

Full GC

對整個堆進行整理,包括 YoungTenuredPerm 。Full GC由於須要對整個對進行回收,因此比 Scavenge GC 要慢,所以應該儘量減小 Full GC 的次數。在對 JVM 調優的過程當中,很大一部分工做就是對於 FullGC 的調節。有以下緣由可能致使Full GC:

  • 老年代(Tenured)被寫滿
  • 永久代(Perm)被寫滿
  • System.gc()被顯示調用

堆外內存 GC

DirectBuffer 的引用是直接分配在堆得 Old 區的,所以其回收時機是在 FullGC 時。所以,須要避免頻繁的分配 DirectBuffer ,這樣很容易致使 Native Memory 溢出。

DirectByteBuffer 申請的直接內存,再也不GC範圍以內,沒法自動回收。JDK提供了一種機制,能夠爲堆內存對象註冊一個鉤子函數(其實就是實現 Runnable 接口的子類),當堆內存對象被GC回收的時候,會回調run方法,咱們能夠在這個方法中執行釋放 DirectByteBuffer 引用的直接內存,即在run方法中調用 UnsafefreeMemory 方法。註冊是經過sun.misc.Cleaner類來實現的。

垃圾收集器

垃圾收集器是內存回收的具體實現,下圖展現了7種用於不一樣分代的收集器,兩個收集器之間有連線表示能夠搭配使用。下面的這些收集器沒有「最好的」這一說,每種收集器都有最適合的使用場景。

Serial收集器

Serial收集器是最基本的收集器,這是一個單線程收集器,它「單線程」的意義不只僅是說明它只用一個線程去完成垃圾收集工做,更重要的是在它進行垃圾收集工做時,必須暫停其餘工做線程,直到它收集完成。Sun將這件事稱之爲」Stop the world「。

沒有一個收集器能徹底不停頓,只是停頓的時間長短。

雖然Serial收集器的缺點很明顯,可是它仍然是JVM在Client模式下的默認新生代收集器。它有着優於其餘收集器的地方:簡單而高效(與其餘收集器的單線程比較),Serial收集器因爲沒有線程交互的開銷,專心只作垃圾收集天然也得到最高的效率。在用戶桌面場景下,分配給JVM的內存不會太多,停頓時間徹底能夠在幾十到一百多毫秒之間,只要收集不頻繁,這是徹底能夠接受的。

ParNew收集器

ParNew是Serial的多線程版本,在回收算法、對象分配原則上都是一致的。ParNew收集器是許多運行在Server模式下的默認新生代垃圾收集器,其主要在於除了Serial收集器,目前只有ParNew收集器可以與CMS收集器配合工做。

Parallel Scavenge收集器(1.8默認新生代)

Parallel Scavenge收集器是一個新生代垃圾收集器,其使用的算法是複製算法,也是並行的多線程收集器。

Parallel Scavenge 收集器更關注可控制的吞吐量,吞吐量等於運行用戶代碼的時間/(運行用戶代碼的時間+垃圾收集時間)。直觀上,只要最大的垃圾收集停頓時間越小,吞吐量是越高的,可是GC停頓時間的縮短是以犧牲吞吐量和新生代空間做爲代價的。好比原來10秒收集一次,每次停頓100毫秒,如今變成5秒收集一次,每次停頓70毫秒。停頓時間降低的同時,吞吐量也降低了。

停頓時間越短就越適合須要與用戶交互的程序;而高吞吐量則能夠最高效的利用CPU的時間,儘快的完成計算任務,主要適用於後臺運算。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一個單線程收集器,採用「標記-整理算法」進行回收。其運行過程與Serial收集器同樣。

Parallel Old收集器(1.8默認老年代)

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法進行垃圾回收。其一般與Parallel Scavenge收集器配合使用,「吞吐量優先」收集器是這個組合的特色,在注重吞吐量和CPU資源敏感的場合,均可以使用這個組合。

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短停頓時間爲目標的收集器,CMS收集器採用標記--清除算法,運行在老年代。主要包含如下幾個步驟:

  • 初始標記
  • 併發標記
  • 從新標記
  • 併發清除

其中初始標記和從新標記仍然須要「Stop the world」。初始標記僅僅標記GC Root能直接關聯的對象,併發標記就是進行GC Root Tracing過程,而從新標記則是爲了修正併發標記期間,因用戶程序繼續運行而致使標記變更的那部分對象的標記記錄。

因爲整個過程當中最耗時的併發標記和併發清除,收集線程和用戶線程一塊兒工做,因此整體上來講,CMS收集器回收過程是與用戶線程併發執行的。雖然CMS優勢是併發收集、低停頓,很大程度上已是一個不錯的垃圾收集器,可是仍是有三個顯著的缺點:

  • CMS收集器對CPU資源很敏感。在併發階段,雖然它不會致使用戶線程停頓,可是會由於佔用一部分線程(CPU資源)而致使應用程序變慢。

  • CMS收集器不能處理浮動垃圾。所謂的「浮動垃圾」,就是在併發標記階段,因爲用戶程序在運行,那麼天然就會有新的垃圾產生,這部分垃圾被標記事後,CMS沒法在當次集中處理它們,只好在下一次GC的時候處理,這部分未處理的垃圾就稱爲「浮動垃圾」。也是因爲在垃圾收集階段程序還須要運行,即還須要預留足夠的內存空間供用戶使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎填滿才進行收集,須要預留一部分空間提供併發收集時程序運做使用。要是CMS預留的內存空間不能知足程序的要求,這是JVM就會啓動預備方案:臨時啓動Serial Old收集器來收集老年代,這樣停頓的時間就會很長。

  • 因爲CMS使用標記–清除算法,因此在收集以後會產生大量內存碎片。當內存碎片過多時,將會給分配大對象帶來困難,這是就會進行Full GC。

G1收集器

G1收集器與CMS相比有很大的改進:

  • G1收集器採用標記–整理算法實現。
  • 能夠很是精確地控制停頓。

G1收集器能夠實如今基本不犧牲吞吐量的狀況下完成低停頓的內存回收,這是因爲它極力的避免全區域的回收,G1收集器將Java堆(包括新生代和老年代)劃分爲多個區域(Region),並在後臺維護一個優先列表,每次根據容許的時間,優先回收垃圾最多的區域 。

文章來源:www.liangsonghua.me

關注微信公衆號:松花皮蛋的黑板報,獲取更多精彩!

公衆號介紹:分享在京東工做的技術感悟,還有JAVA技術和業內最佳實踐,大部分都是務實的、能看懂的、可復現的

相關文章
相關標籤/搜索