jvm垃圾回收機制 一

     

目錄

一、回收什麼?

二、何時回收?

引用計數法

根搜索算法-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
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章