jvm垃圾回收機制 一

塵虛緣_KY發表於2017-05-09

     

目錄

一、回收什麼?

二、何時回收?

引用計數法

根搜尋演算法-GC-root演算法

引用

強引用

軟引用

弱引用

虛引用

垃圾回收條件

方法區的回收

三、如何回收?

垃圾回收演算法

1.標記-複製

2.標記-清理

3.標記-整理

垃圾收集

新生代垃圾收集器

1. Serial收集器 - 複製演算法

2. ParNew 收集器 - 複製演算法

3. Parallel Scavenge並行回收收集器 - 複製演算法

老年代垃圾收集器

1. Serial Old 收集器 - 標記整理演算法

2. Parallel Old收集器 - 標記整理演算法

備註:常用引數


進過技術不斷的發展,記憶體的動態分配與記憶體回收技術已經相當成熟,一切看起來都以進入“自動化”的時代,那我們為什麼還要去了解gc和記憶體分配呢?因為自動的好處是我們暫時可以不用管,省心,前提是一切執行正常。但是如果程式執行中發生了記憶體的洩漏和溢位,或者當垃圾回收成為系統達到高併發的瓶頸的時候,我們就需要對這些“自動化”的技術實施必要的腳控和調節

問題導讀:

  1. 回收什麼?
  2. 何時回收?
  3. 如何回收?

一、回收什麼?

    java記憶體執行時區域的各個部分,其中程式計數器/虛擬機器棧/本地方法棧三個都是隨著執行緒而生,隨執行緒而滅;棧中的每一個棧幀分配多少記憶體基本上是在類結構確定下來的時候就大體已經確定了【除了JIT編譯器進行的一些優化】,所以隨著方法的結束和執行緒的結束,記憶體自然就跟著回收了。但是堆和方法區不一樣,我們只有在程式執行的時候才知道分配那些記憶體和建立的物件,這部分的記憶體分配和回收都是動態的,由於其不確定性所以需要格外的關注。

      java所建立的物件基本上都儲存在堆區,雖然“永久代”方法區的垃圾回收效率比較低,但是“永久代”對於廢棄的常量和無用的類,也會進行相應的回收,尤其大量使用反射和動態代理CGlib等場景都需要虛擬機器具備類解除安裝功能,保證無用的資源可以被回收,不會發生溢位。這裡要回收就要確定該物件是否“存活”或者“死去”;“死去”的對像就是不可能再被任何途徑使用的物件,這些物件就可以被回收。回收什麼,就是要回收記憶體中一切不再利用的資源。例如回收一些堆中無用的物件,方法區中廢棄的常量和類等。

二、何時回收?

引用計數法

    當該物件沒有任何其他物件和程式的引用,即計數為0就可以回收。

  • 優點:實現簡單,效率也很高;
  • 缺點:無法解決迴圈引用的問題;

根搜尋演算法-GC-root演算法

    基本思想是:進過一系列名為“gc-root"的對像作為起始點,從這些節點向下走的路徑稱之為引用鏈,當一個物件到gc-root沒有任何引用鏈相連的時候,則證明此物件是不可達的。不可達的物件即可回收。

GC-root物件包含:

  • 虛擬機器棧中引用的物件-棧幀中的本地變數表;
  • 方法區中的類靜態屬性引用的物件;
  • 方法區中常量引用的物件;
  • 本地方法棧中JNI的引用物件;

引用

如果reference型別的資料中存放的數值是另外一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。

JDK.1.2 之後,Java 對引用的概念進行了擴充,將引用分為了:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4 種,這 4 種引用的強度依次減弱。

強引用

     只要強引用存在,垃圾回收器將永遠不會回收被引用的物件,哪怕記憶體不足時,JVM也會直接丟擲OutOfMemoryError,不會去回收。如果想中斷強引用與物件之間的聯絡,可以顯示的將強引用賦值為null,這樣一來,JVM就可以適時的回收物件了

軟引用

        軟引用SoftReference是用來描述一些非必需但仍有用的物件。在記憶體足夠的時候,軟引用物件不會被回收,只有在記憶體不足時,系統則會回收軟引用物件,如果回收了軟引用物件之後仍然沒有足夠的記憶體,才會丟擲記憶體溢位異常。這種特性常常被用來實現快取技術,比如網頁快取,圖片快取等。

弱引用

      弱引用的引用強度比軟引用要更弱一些,無論記憶體是否足夠,只要 JVM 開始進行垃圾回收,那些被弱引用關聯的物件都會被回收。在 JDK1.2 之後,用 java.lang.ref.WeakReference 來表示弱引用。

虛引用

     虛引用是最弱的一種引用關係,如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,它隨時可能會被回收,在 JDK1.2 之後,用 PhantomReference 類來表示,通過檢視這個類的原始碼,發現它只有一個建構函式和一個 get() 方法,而且它的 get() 方法僅僅是返回一個null,也就是說將永遠無法通過虛引用來獲取物件,虛引用必須要和 ReferenceQueue 引用佇列一起使用,它的唯一目的是當物件被系統回收時收到一個系統通知。

引入引用的作用

  • 可以讓程式的方式決定某些象的生命週期;
  • 有利於JVM進行垃圾回收

垃圾回收條件

在搜尋演算法中的不可達物件,也並非是非死不可的。真正宣告一個物件的死亡需要經以下兩個條件:

  1. 沒有與GC-root相連的引用鏈;
  2. 該物件是否有必要執行finalize方法;【判斷物件是否覆蓋過finalize方法,是否已經呼叫過一次】

   finalize()方法是物件逃過死亡的最後一次機會。但是finalize方法只會被呼叫一次,如果有必要執行finalize方法,且該物件不行被回收,則只需要在F-Queue佇列中重新引用上任何一個物件即可。如果此時物件任然沒有任何的引用,那麼就會被稍後GC正式的回收。流程如下:

  圖一:GC-Roots法流程圖

方法區的回收

廢棄的常量:

  • 沒有任何物件引用該常量池中的常量。

無用的類:

  • 該類的所有例項都已經被回收;
  • 載入該類的ClassLoader被回收;
  • 該類對應的class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該方法

三、如何回收?

    如何回收就需要良好的垃圾回收演算法,這個也是能讓程式設計師解脫記憶體管理,專心編碼的關鍵所在。大體主要分為3類垃圾回收演算法。

垃圾回收演算法

1.標記-複製

主要思想是:將可用記憶體容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊用完之後,就將還存活的物件複製到另外一塊上面,然後在把已使用過的記憶體空間一次理掉。

優點

實現簡單,效率高;

不會存在記憶體碎片;

缺點

就是需要2倍的記憶體來管理;

複製需要時間開銷;

使用場景 新生代垃圾收集器基本都採用標記複製;

   我們可以看到引數:-XX:SurvivorRatio=8,表示新生代中Eden區域和Survivor區域的容量比值,代表Eden:Survivor=8:1。一個Survior佔新生代的1/10,表達的意思是Survivor中的from區間和to區間 與 Edon的比例是1:1:8,from和to的空間也就是採用了標記複製的演算法,所以空間大小一樣,且程式執行期間浪費年輕代的1/10。

2.標記-清理

標記清除演算法分為“標記”和“清除”兩個階段:首先標記出需要回收的物件,標記完成之後統一清除物件。

優點

清除效率高;

缺點 存在記憶體碎片;
使用場景

老年代垃圾收集器CMS採用“標記-清理”但是可以通過設定靈活改變;

G1垃圾收集的全域性看是基於“標記-清除”,區域性是標記-整理;

3.標記-整理

與“標記-清理”不同的是標記完存活物件,清理完物件後,將所有存活的物件都向一端移動,並更新引用其物件的指標,進行了記憶體的複製整理。因為要移動物件,所以它的效率要比“標記-清理”效率低,但是不會產生記憶體碎片。

優點 沒有碎片;
缺點

複製移動資源需要時間的開銷;

指標的改變增加了程式的消耗;

使用場景 老年代的序列收集器和並行收集器都採用“標記-整理”的演算法;

基於分代的思想

   由於java大部分物件都具有“朝生夕死”的特性,所以對於存活時間長的物件,減少被gc的次數可以避免不必要的開銷。這樣我們就把記憶體分成新生代和老年代,新生代存放剛建立的和存活時間比較短的物件,老年代存放存活時間比較長的物件。這樣每次僅僅清理年輕代,也就是Minor GC,非常頻繁,但是速度較快;老年代的Major GC又稱[Full GC]僅在必要時時再做清理可以極大的提高GC效率,節省GC時間。

垃圾收集

1.新生代的收集器包括

  •  Serial  / PraNew / Parallel Scavenge

2.老年代的收集器包括:

  •  Serial Old  / Parallel Old / CMS

3.回收整個Java堆(新生代和老年代)

  •  G1收集器

幾種收集器及它們之間的組合關係如下:

圖二:垃圾收集器及之間的組合

新生代垃圾收集器

1. Serial收集器 - 複製演算法

Serial收集器是一個新生代收集器,單執行緒收集器,使用複製演算法。進行收集時,必須暫停所有的執行緒。

圖三: serial + serial old收集器執行示意圖
優點 對與單cpu,實現簡單高效
缺點 單執行緒工作不能充分利用多核cpu效能;之間會產生STW,給使用者帶來不良體驗;
使用場景 新生代垃圾收集器,虛擬機器執行在Client模式下的預設新生代收集器
使用引數

序列收集器,分為年輕代 Serial 和老年代 Serial Old 收集器。

  1. -XX:+UseSerialGC 這個引數就是可以指定使用新生代序列收集器和老年代序列收集器;

  2. -XX:+UseParNewGC 新生代使用 ParNew 回收器,老年代使用序列收集器;

  3. -XX:+UseParallelGC 新生代私用 ParallelGC 回收器,老年代使用序列收集器;

而 Serial 收集器出現的日誌為 DefNew .

注:【“+” 號的意思是ture,開啟,反之,如果是 “-”號,則是關閉】

2. ParNew 收集器 - 複製演算法

ParNew收集器其實就是serial收集器的多執行緒版本,除了使用多條執行緒進行垃圾收集之外,其餘行為與Serial收集器一樣。

單CPU,ParNew 不會比Serial收集效果更好,但是隨著CPU的數量增加。ParNew則在GC時對系統資源有效利用更好。另外,從圖一可以看出,除了serial收集器外,只有ParNew收集器可以和cms收集器愉快的合作;

圖四: ParNew + serial old收集器執行示意圖
優點 在多核cpu的情況下,gc效率更高;
缺點 單核的效率 <= Serial; 產生STW;
使用場景 新生代垃圾收集器,在Server模式下,ParNew收集器是一個非常重要的收集器,且能與CMS垃圾收集配合使用。
引數使用

並行收集器是 Serial 的多執行緒版本,在 CPU 並行能力強大的計算機上有很大優勢。

其中:

  1. -XX:+UseParNewGC 強制設定新生代使用 ParNew 收集器,老年代使用序列收集器。

  2. -XX:+UseConcMarkSweepGC:  會預設使用ParNew作為新生代收集器,老年代使用 CMS。

  3. -XX:ParallelGCThreads={value} 這個引數是指定並行 GC 執行緒的數量,一般最好和 CPU 核心數量相當。預設情況下,當 CPU 數量小於8, ParallelGCThreads 的值等於 CPU 數量,當 CPU 數量大於 8 時,則使用公式:3+((5*CPU)/ 8);同時這個引數只要是並行 GC 都可以使用,不只是 ParNew。

而 ParNew 的 GC 日誌則表吸納出 ParNew。

3. Parallel Scavenge並行回收收集器 - 複製演算法

    Parallel Scavenge收集器也是一個新生代並行多執行緒收集器。parallel Scavenge收集器的目標則是:追求可控制高吞吐量+高效利用 CPU吞吐量= 程式執行時間/(程式執行時間 + 垃圾收集時間),虛擬機器總共執行了100分鐘。其中垃圾收集花掉1分鐘,那吞吐量就是99%。

圖五:paraller scavenge+ parall old 執行示意圖
優點 吞吐量可控,讓使用者程式碼獲得更長的執行時間;並行收集高效利用多核cpu;
缺點

(1) 標記和清理產生2次STW;

(2) 不能和cms一起愉快的合作,框架不同;

使用場景 收集器是 Java 8 的預設新生代收集器,因為它能夠根據系統當前狀態給出吞吐量最高的GC 配置。所以,在一些手工調優複雜的場合或者對實時性要求不高的場合,可以使用該處理器。停頓時間越短就越適合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,而高吞吐量則可用高效率地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。
引數使用
  1. -XX:MaxGCPauseMillis 設定最大垃圾收集停頓時間,他的值是一個大於0的整數。ParallelGC 工作時,會調整 Java 堆大小或者其他的一些引數,儘可能的把停頓時間控制在 MaxGCPauseMillis 以內。注意,此值不是越小越好,原因是GC停頓時間縮短是以犧牲吞吐量和調整新生代空間來換取的,這將會到值頻繁 GC ,雖然系統停頓時間小了,但總吞吐量下降了。

  2. -XX:GCTimeRatio 設定吞吐量大小,他的值是一個0 到100之間的整數,假設 GCTimeRatio 的值是 n ,那麼系統將花費不超過 1/(1+n) 的時間用於垃圾收集,預設 n 是99,即不超過1% 的時間用於垃圾收集。

  3. -XX:UseAdaptiveSizePolicy: 開啟自適應策略,與ParNew的區別。在這種模式下,新生代的大小,eden 和 Survivor 的比例,晉升老年代的物件年齡等引數會被自動調整。以達到堆大小,吞吐量,停頓時間的平衡點。

注: 引數1-2其實是矛盾的,吞吐量和停頓時間是反比的,需要找到一個平衡點。如果是調參場景比較複雜的情況下,可採用自適應策略。PS 處理器的 GC 日誌則是 PSYoungGen

老年代垃圾收集器

1. Serial Old 收集器 - 標記整理演算法

    Serial Old是Serial收集器的老年代垃圾回收,它同樣是一個單執行緒(序列)收集器,使用標記整理演算法。這個收集器的主要意義也是在於給Client模式下的虛擬機器使用。工作流程如圖三所示,這裡不再重複。

優點 單執行緒工作效率高,實現簡單;
缺點 產生STW;多核CPU不能充分利用其效能
使用場景
  1. 在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用;
  2. 作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用;
引數使用 參考Serial收集器的使用

2. Parallel Old收集器 - 標記整理演算法

Parallel Old是Parallel Scavenge收集器的老年代版本,也是一種關注系統吞吐量的老年代垃圾回收機制。使用多執行緒“標記-整理”演算法。 之前一直是Parallel Scavenge + old serial的組合,由於無法真正的利用多核cpu的效能,所以其吞吐量反而不一定有PreNew+CMS組合給力,現在終於才有了新生代和老年代都關注的吞吐量的新組合:Parallel Scavenge + Parallel Old

優點 高效利用cpu;獲得較大的吞吐量;可以和Parallel Scavenge一起愉快的合作;
缺點  
使用場景 老年代垃圾回收,注重吞吐量以及CPU資源敏感的場景“吞吐量優先” 收集器的名副其實組合:Parallel Scavenge + Parallel Old組合。
引數使用
  1. -XX:+UseParallelOldGC 新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。該引數可以啟用 ParallelOldGC。

  2. -XX:ParallelGCGThreads 同時可以指定該引數設定並行執行緒數量。

備註:常用引數

JVM常用配置引數

配置引數 功能
-Xms 初始堆大小。如:-Xms4g
-Xmx 最大堆大小。如:-Xmx4g
-Xmn 新生代大小。通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90%
-Xss JDK1.5+ 每個執行緒堆疊大小為 1M,一般來說如果棧不是很深的話, 1M 是絕對夠用了的。
-XX:NewRatio 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代佔整個堆空間的1/3,老年代佔2/3
-XX:SurvivorRatio 新生代中 Eden 與 Survivor 的比值。預設值為 8。即 Eden 佔新生代空間的 8/10,另外兩個 Survivor 各佔 1/10
-XX:PermSize 永久代(方法區)的初始大小
-XX:MaxPermSize 永久代(方法區)的最大值

GC日誌列印引數參考

gc日誌列印引數
  1. -XX:+PrintGCDateStamps 列印 GC 日誌時間戳。

  2. -XX:+PrintGCDetails 列印 GC 詳情。

  3. -XX:+PrintGCTimeStamps: 列印此次垃圾回收距離jvm開始執行的所耗時間。

  4. -Xloggc: 將垃圾回收資訊輸出到指定檔案

  5. -verbose:gc 列印 GC 日誌

  6. -XX:+PrintGCApplicationStopedTime 檢視 gc 造成的應用暫停時間

  7. XX:+PrintTenuringDistribution, 物件晉升的日誌

  8. -XX:+HeapDumpOnOutOfMemoryError 記憶體溢位時輸出 dump 檔案。


參考資料:
《深入瞭解jvm虛擬機器》
https://www.cnblogs.com/liyutian/p/9690974.html
https://www.jianshu.com/p/dbd32622ad20
https://blog.csdn.net/qq_31156277/article/details/79962445
https://www.cnblogs.com/yang-hao/p/5936059.html
https://www.cnblogs.com/ASPNET2008/p/6496481.html
https://www.cnblogs.com/yunxitalk/p/8987318.html
 

相關文章