快速瞭解GC有這篇文章就夠了!

一、什麼是GC

GC(Garbage Collection)垃圾收集,回收垃圾,釋放內存(那Java中的垃圾是什麼呢?垃圾一般是沒有被任何對象引用的對象),Java 提供的 GC 功能可以自動監測對象是否過期(超過生命週期)從而達到自動清除過期對象回收內存的目的。

二、爲什麼要了解GC

在實際項目中,可能會遇到內存泄漏的問題,我們需要對項目業務進行合理的內存分配,需要對其進行監控和調節,在這之前,我們需要了解GC。

三、對象被判定爲垃圾的標準

前邊已經說過了,沒有被任何對象引用的對象被判定爲垃圾

四、判斷對象是否爲垃圾的算法

  • 引用計數算法
  • 可達性分析算法

4.1、引用計數算法

  • 通過判斷對象的引用數量來決定對象是否可以被回收
  • 每個對象實例都有一個引用計數器,被引用則+1,完成引用則-1
  • 任何引用計數爲0 的對象實例可以被當做垃圾收集
  • 優點:執行效率高,程序執行受影響小
  • 缺點:無法檢測出循環引用的情況,導致內存泄漏
    比如下邊這個例子:
package com.mtli.jvm.gc;

/**
 * @Description:
 * @Author: Mt.Li
 * @Create: 2020-04-28 13:38
 */
public class MyObject {
    public MyObject childNode;
}

package com.mtli.jvm.gc;

/**
 * @Description:
 * @Author: Mt.Li
 * @Create: 2020-04-28 13:39
 */
public class ReferenceCountProblem {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();

        // 循環引用
        object1.childNode = object2;
        object2.childNode = object1;
    }
}

這種情況下,引用是無法爲0的,無法檢測,這也是一個致命短板

4.2、可達性分析算法

  • 主流的Java垃圾收集器採用的是可達性分析算法。它通過判斷對象的引用鏈是否可達來決定對象是否可以被回收。
  • 程序把所有的引用關係看做一張有向圖,將名稱爲gc root 的對象作爲起始點,從這些節點開始向下搜索,其所走過的路徑被稱爲引用鏈(reference chain),路徑上所有遇到的對象都標記爲存活,當gc root到某個對象沒有任何引用鏈時,就是說gc root到對象是不可達的,那麼該對象也就被標記爲垃圾了。
    在這裏插入圖片描述
    紅色部分是GC Root可達的對象,標記存活,綠色的是不可達的,即垃圾對象。
    那麼問題來了什麼對象可以作爲GC Root的對象?
  • 虛擬機棧中引用的對象(棧幀中的本地變量表)
  • 方法區中的常量引用的對象,即常量保存的是該對象的地址
  • 方法區中的類靜態屬性引用的對象(類似於常量情況)
  • 本地方法棧中INI(Native方法)的引用對象
  • 活躍線程的引用對象也可以作爲GC Root

五、垃圾回收算法

說完如何判定垃圾的算法後,當然是要進行垃圾回收了,這又有哪些算法呢?

  • 標記-清除算法(Mark and Sweep)
  • 複製算法(Copying)
  • 標記-整理算法(Compacting)

5.1、標記-清除算法

  • 標記:從根集合進行掃描,對存活的對象進行標記
  • 清除:對堆內存從頭到尾進行線性遍歷,回收不可達對象內存
    在這裏插入圖片描述
    缺點:
  • 容易造成大量碎片
    • 上圖中,我們回收掉不可達對象之後,剩下的存活對象處於不連續的內存空間中,造成碎片化,當我們想要插入一個新的對象,並且需要的連續內存較大(比如需要3個單位)圖中最多隻有兩個單位,那麼就要進行新一輪的垃圾回收,這樣也會導致新的碎片出現,也會浪費時間,降低效率。

5.2、複製算法

  • 分爲對象面和空閒面

  • 對象在對象面上創建

  • 當被定義的對象面用完的時候,將還存活的對象複製到空閒面,然後將該對象面所有對象內存清除

  • 順序分配內存,簡單高效

  • 解決碎片化問題

  • 適用於對象存活率低的場景,如年輕代
    在這裏插入圖片描述
    在這裏插入圖片描述
    優點很明顯,解決了碎片化的問題,複製算法不使用鏈表,分塊直接用連續的內存空間,放入新對象只需要找到滿足該對象內存大小的內存塊放入即可,省去了大量時間。
    缺點:

  • 這種算法,需要留出一半的空間給空閒面,造成堆使用率低下

  • 不適合高存活率的場景,那樣的話每次複製要複製很多,性能上不允許,故不適合老年代

5.3、標記-整理算法

  • 標記:從根集合進行掃描,對存活的對象進行標記
  • 清除:移動所有存活的對象,且按照內存地址次序一次排列,然後將末端內存地址以後的內存全部回收
  • 在標記-清除算法上增加了對象的移動,增加了成本,卻解決了內存碎片的問題
  • 適用於存活率極高的場景,如老年代
    在這裏插入圖片描述

5.4、主流回收算法——分代收集算法(Generation Collector)

  • 按照對象生命週期的不同劃分區域以採用不同的垃圾回收算法
  • 目的:提高JVM的回收效率
  • 年輕代採用複製算法
  • 老年代採用標記-清除算法、**標記-整理算法

JDK1.8中的分代
在這裏插入圖片描述
JDK1.6中的分代
在這裏插入圖片描述

六、年輕代的垃圾收集

年輕代佔堆空間的1/3,是爲了儘可能快速地收集掉那些生命週期短的對象,它有如下分區

  • Eden區(新創建的對象一般放在這裏),佔新生代的8/10
  • 兩個Survivor區,一個爲From區一個是To區(不是固定的,根據GC執行而定的),是爲了配合複製算法

下面我們用圖來解釋:

我們假設Eden能存4個對象,Survivor分別能存3個對象
在這裏插入圖片描述
1.現在Eden(臨時急救中心)中有4個對象,其中三個掛了,需要回收屍體,空出位置,另外一個需要轉移到醫院,現在有S0和S1兩個醫院,規定是必須有個醫院作爲待命的to隨時準備接收病人,另一個則作爲接診區from如下圖,S0轉移進一個則S0此時是from,S1是to,且病人編號+1
在這裏插入圖片描述
2.現在Eden又來了四個急救病人,有倆沒挺住,另外兩個存活的要送醫院,爲了方便接收和明示病人批次病人編號都+1,我們要將S0醫院的轉移到to中,剛進來的兩個也要轉到to中,兩家醫院的標識對換,病人編號都+1(以此類推)。複製完後S0是to,S1是from
在這裏插入圖片描述
3.沒想到壞事並行啊,這下又有四個人進到Eden臨時急救中心,可惜的是隻有一個活了下來,禍不單行,醫院S1的有一個沒救過來,掛了。剛好都給他們轉到S0去,S1留着待命。此時S0變成from,S1變成to
在這裏插入圖片描述
最後情況(Eden可以是有對象的):
在這裏插入圖片描述
(回收Eden和Survivor區的過期對象的過程被稱爲Minor GC

當編號達到某個值時(由 -XX:MaxTenuringThreshold定義,可以是15或者自定義),需要轉移到老年代去。當然情況不是唯一,有的對象較大,Eden裝不下或者S區裝不下,經過上面的程序,容納不下,就會進入到老年代。

對象如何晉升到老年代

  • 經歷一定Minor次數依然存活的對象
  • Survivor區或者Eden區裝不下的對象
  • 新生成的大對象( -XX: +PretenuerSizeThreshold)

常用的調優參數

  • -XX:SurvivorRatio : Eden和Survivor的比值,默認8:1
  • -XX:NewRatio : 老年代和年輕代內存大小的比例,比如2:1
  • -XX:MaxTenuring Threshold : 對象從年輕代晉升到老年代經過GC次數的最大閾值

七、老年代的垃圾收集

老年代(2/3堆空間):

  • 存放生命週期較長的對象
  • 標記-清理算法
  • 標記-整理算法
  • 垃圾回收:Full GC和Major GC。major gc 通常與Full GC等價,只是由於HotSpot VM發展了這麼多年,外界對各種名詞的解讀已經完全混亂了,所以說到Major的時候,要詢問清楚是full gc還是old gc

Full GC(全局範圍的GC)

Full GC比Minor GC慢很多,它是全局範圍的GC,清理年輕代、老年代、元空間
觸發條件:

  • 老年代空間不足
  • 永久代空間不足(jdk1.7之前)
  • CMS GC時出現promotion failed ,concurrent mode failure
  • Minor GC晉升到老年代的平均大小大於老年代的剩餘空間
  • 調用System.gc(),對老年代和新生代進行回收,具體還是由虛擬機執行

可以這麼理解,當觸發老年代GC時,通常也會觸發年輕代GC回收,也就是對整個堆進行垃圾回收,即full GC。

Stop-the-World

  • JVM由於要執行GC而停止應用程序的執行
  • 任何一種GC算法中都會發生
  • 多數GC優化用減少Stop-the-world發生的時間來提高程序性能

SafePoint

  • 分析過程中對象引用關係不會發生變化的點

  • 產生Safepoint的地方:方法調用;循環跳轉;異常跳轉等等

  • 一旦GC發生,所有的線程跑到安全點再停頓下來,如果某個線程不在安全點,就等其到達安全點,再GC

  • 安全點數量要適中,太少會讓GC等待很久,太多會增加程序運行的負荷

八、常見的垃圾收集器

JVM運行模式

  • Server,啓動較慢,長期穩定運行後,程序運行速度要比Client快。因爲採用重量級虛擬機,其中對程序有更多優化
  • Client,啓動較快

可以在cmd中查詢java -version
在這裏插入圖片描述
默認是以Server啓動

垃圾收集器之間的聯繫

(借圖)
在這裏插入圖片描述

8.1、年輕代常見的垃圾收集器

Serial收集器(-XX:+UseSerialGC,複製算法)
  • 1.3版本之前年輕代的唯一選擇

  • 單線程收集,進行垃圾收集時,必須暫停所有工作線程

  • 簡單高效,Client模式下默認的年輕代收集器

ParNew收集器(-XX:+UseParNewGC,複製算法)
  • 多線程收集,其餘的行爲、特點和Serial收集器一樣
  • 單核CPU情況下執行效率不如Serial,在多核下執行纔有優勢
  • 默認開啓線程數和CPU核數相同,可以加限制
Parallel Scavenge收集器(-XX:+UseParallelGC,複製算法)
  • 吞吐量=運行用戶代碼時間/(運行用戶代碼+垃圾收集時間)
  • 比起其他的收集器關注用戶線程停頓時間,它更關注系統的吞吐量
  • 在多核下執行纔有優勢,Server模式下默認的年輕代收集器
  • 調優參數(-XX:+UseAdaptiveSizePolicy),如果開啓 AdaptiveSizePolicy,則每次 GC 後會重新計算 Eden、From 和 To 區的大小,計算依據是 GC 過程中統計的 GC 時間、吞吐量、內存佔用量。

8.2、老年代常見收集器

Serial Old收集器( -XX:USeSerialOldGC,標記-整理算法)
  • 單線程收集,進行垃圾收集時,必須暫停所有工作線程
  • 簡單高效,Client模式下默認的老年代收集器
CMS收集器(-XX:+UseConcMarkSweepGC,標記-清除算法)(老年代收集器的半壁江山)
  • 初始標記:stop-the-world(std),虛擬機會停頓正在執行的任務,從垃圾回收的根對象開始,只掃描到能夠和根對象關聯的對象,並做標記,並不會耗時太多,很快就能完成。
  • 併發標記:併發追溯標記,程序不會停頓,該階段緊隨初始標記,會繼續向下追溯標記,不會停止應用線程,兩者併發執行
  • 併發預處理:查找執行併發標記階段從年輕代晉升到老年代的對象,減少下一個階段——重新標記的工作量(重新標記會暫停虛擬機)
  • 重新標記:暫停虛擬機,掃描CMS堆中的剩餘對象。從跟對象開始一直向下追溯所有對象,比較耗時
  • 併發清理:清理垃圾對象,程序不會停頓
  • 併發重置:重置CMS收集器的數據結構

缺點:

  • 如果在併發標記後產生的垃圾,只能等待本次清理完畢
  • 最大的問題是本收集器採用的是標記-清除算法,會產生比較多的不連續的碎片空間,如果碎片空間過多,但是又需要一塊較大的連續空間,這時候只能進行一次GC了
G1收集器(-XX:+UseG1GC,複製+標記-整理算法)
  • 並行和併發
  • 分代收集
  • 空間整合(用標記-整理算法)
  • 可預測的停頓
  • 將整個Java內存劃分爲多個大小相等的Region
  • 年輕代和老年代不再物理隔離。堆內存被劃分爲多個大小相等的 heap 區,每個heap區都是邏輯上連續的一段內存(virtual memory). 其中一部分區域被當成老一代收集器相同的角色(eden, survivor, old), 但每個角色的區域個數都不是固定的。這在內存使用上提供了更多的靈活性。
  • 相比CMS,G1消除了內存碎片的問題

詳解及其執行過程可參考G1垃圾收集器入門

九、關於Java中的強引用、軟引用、弱引用、虛引用

強引用(Strong Reference)

  • 最普遍的引用:Object obj = new Object()
  • GC哪怕是拋出OOM終止程序也不會回收具有強引用的對象
  • 通過將對象設置爲null來弱化引用,使其被回收(等待其超過生命週期)

軟引用(Soft Reference)

  • 對象處在有用但非必須的狀態

  • 只有當內存空間不足時,GC會回收該引用的對象的內存

  • 可以用來實現高速緩存

    • 使用方法:

    • String str = new String("abc"); // 強引用
      SoftReference<String> softRef = new SoftReference<String>(str); // 軟引用
      

弱引用(Weak Reference)

  • 非必須的對象,比軟引用更弱一些

  • GC時會被回收(無論內存資源是否緊缺)

  • 被回收的概率也不大,因爲GC線程優先級比較低

  • 適用於引用偶爾被使用且不影響垃圾收集的對象

  • String str = new String("abc"); // 強引用
    WeakReference<String> weakRef = new WeakReference<String>(str); // 弱引用
    

虛引用(PhantomReference)

  • 不會決定對象的生命週期

  • 任何時候都可能被垃圾收集器回收

  • 跟蹤對象被垃圾收集器回收的活動,起哨兵作用,GC在回收對象時,會先判斷該對象是否有虛引用,如果有,則加入到引用隊列中,程序可以判斷,引用隊列是否已經加入虛引用來了解被引用的對象是否被回收

  • 必須和引用隊列ReferenceQueue聯合使用

  • String str = new String("abc");
            ReferenceQueue queue = new ReferenceQueue();
            PhantomReference ref = new PhantomReference(str, queue);
    

綜上,Java中的引用強弱級別爲:

強引用>軟引用>弱引用>虛引用

引用類型 被垃圾回收時間 用途 生存時間
強引用 從來不會 對象的一般狀態 JVM停止運行時終止
軟引用 在內存不足時 對象緩存 內存不足時終止
弱引用 在垃圾回收時 對象緩存 gc運行後終止
虛引用 Unknown 標記、哨兵 Unknown

以上均爲個人理解,如有不足,請及時在評論區提出,謝謝。

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