天天看點

JAVA垃圾回收(GC)

JAVA垃圾回收:将已經配置設定出去的,但卻不再使用的記憶體回收回來,以便能夠再次配置設定。在 Java 虛拟機的語境下,垃圾指的是死亡的對象所占據的堆空間。

探索所有存活的對象:

  • 可達性分析

防止在标記過程中堆棧的狀态發生改變:

  • 安全點機制來實作 Stop-the-world 操作,暫停其他非垃圾回收線程

回收死亡對象的記憶體的方式:

  • 會造成記憶體碎片的清除(标記清除)
  • 性能開銷較大的壓縮(标記整理)
  • 以及堆使用效率較低的複制

一、判斷對象可以回收

1.引用計數法(JAVA不用)

簡介:

引用計數法就是如果一個對象沒有被任何引用指向,則可視之為垃圾。

首先需要聲明,至少主流的Java虛拟機裡面都沒有選用引用計數算法來管理記憶體。

實作:

  • 給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值加1;
  • 當引用失效時,計數器值減1.任何時刻計數器值為0的對象就是不可能再被使用的。

弊端:

不能檢測到環的存在,很難解決對象之間互相循環引用的問題,會造成記憶體洩露

2.可達性分析算法

Java 虛拟機中的垃圾回收器采用可達性分析來探索所有存活的對象

掃描堆中的對象,看是否能夠沿着 GC Root對象為起點的引用鍊找到該對象,如果找不到,表示該對象可以回收

優點:

  • 解決循環引用問題

可達性分析算法的問題(在多線程環境下)

  • 誤報:将引用設定為 null時會發生(沒有什麼傷害)
  • 漏報:将引用設定為未被通路過的對象(一旦從原引用通路已經被回收了的對象,則很有可能會直接導緻 Java 虛拟機崩潰)

哪些對象可以作為 GC Root ?

(1). 虛拟機棧(棧幀中的局部變量區,也叫做局部變量表)中引用的對象。

public class Test {
    public static void main(String[] args) {
    Test a = new Test();//其中a是棧幀中的本地變量,充當了GC Root 的作用
    a = null;//a的引用變為空,上面的new Test()對象變為可回收對象
    }
}      

(2). 方法區中的類靜态屬性引用的對象。

public class Test {
    public static Test s;//類中的靜态屬性,充當GC Root 的作用
    public static  void main(String[] args) {
    Test a = new Test();
    a.s = new Test();
    a = null;//Test()對象變為可回收
    }
}      

(3). 方法區中常量引用的對象。

​
public class Test {
    public static final Test s = new Test();//同上,隻是這裡s由靜态變量變為常量
        public static void main(String[] args) {
        Test a = new Test();
        a = null;
        }
}      

(4). 本地方法棧中JNI(Native方法)引用的對象。

本地方法為JAVA調用非Java代碼的接口, Java 通過 JNI 來調用本地方法, 而本地方法是以庫檔案的形式存放的,虛拟機隻是簡單地動态連接配接并直接調用指定的 native 方法。

(5)已啟動且未停止的 Java 線程

二、Stop-the-world (STW)以及安全點(safepoint)

怎麼解決對象引用漏報的問題呢?

停止其他非垃圾回收線程的工作,直到完成垃圾回收。(Stop-the-world),進而産生暫停時間(GC pause)

安全點(safepoint)機制

  • 當 Java 虛拟機收到 Stop-the-world 請求,它便會等待所有的線程都到達安全點,才允許請求 Stop-the-world 的線程進行獨占的工作。

safepoint指的特定位置主要有:

  1. 循環的末尾 (防止大循環的時候一直不進入safepoint,而其他線程在等待它進入safepoint)
  2. 方法傳回前
  3. 調用方法的call之後
  4. 抛出異常的位置

為什麼不在每一條機器碼或者每一個機器碼基本塊處插入安全點檢測?

  • 安全點檢測本身也有一定的開銷
  • 即時編譯器生成的機器碼打亂了原本棧桢上的對象分布狀況在進入安全點時,機器碼還需提供一些額外的資訊,來表明哪些寄存器,或者目前棧幀上的哪些記憶體空間存放着指向對象的引用,以便垃圾回收器能夠枚舉 GC Roots。由于這些資訊需要不少空間來存儲,是以即時編譯器會盡量避免過多的安全點檢測。

三、3種垃圾回收算法

1、标記清除(清除(sweep))Eden區

實作方法:

  1. 把死亡對象所占據的記憶體标記為空閑記憶體
  2. 将空閑記憶體記錄在一個空閑清單(free list)之中
  3. 需要建立對象時,記憶體管理子產品便會從該空閑清單中尋找空閑記憶體,并劃分給建立的對象。

優點:

  • 速度快
  • 原理極其簡單

缺點:

  • 産生大量記憶體碎片
  • 配置設定效率較低:需要逐個通路清單中的項來配置設定合适的記憶體空間

2、标記整理(壓縮(compact))老年區

實作方法:

把存活的對象聚集到記憶體區域的起始位置,進而留下一段連續的記憶體空間。

優點:

  • 沒有記憶體碎片化的問題

缺點:

  • 速度慢(壓縮算法的性能開銷)

3、複制(copy)Survivor區

實作方法:

把記憶體區域分為兩等分,分别用兩個指針 from 和 to 來維護,并且隻是用 from 指針指向的記憶體區域來配置設定記憶體。當發生垃圾回收時,便把存活的對象複制到 to 指針指向的記憶體區域中,并且交換 from 指針和 to 指針的内容。

優點:

  • 沒有記憶體碎片化的問題

缺點:

  • 堆空間的使用效率極其低下

四、分代回收算法

1、堆記憶體空間分布

JAVA垃圾回收(GC)

Java對象特點:

大部分的 Java 對象隻存活一小段時間,而存活下來的小部分 Java 對象則會存活很長一段時間。

這裡考慮新生代和老年區,其中新生代分為三個區,分别為:Eden 區,以及兩個大小相同的 Survivor(from,to) 區,大小比預設為8:1:1。

2、堆空間的配置設定過程為:

  • 對象首先配置設定在伊甸園區域
  • 新生代空間不足時,觸發 minor gc,伊甸園和 from 存活的對象使用 copy 複制到 to 中,存活的對象年齡加 1并且交換 from to(總有一個Survivor區是空的)
  • 如果出現大對象使得新生代空間不足,則會直接晉升到老年代
  • minor gc 會引發 stop the world,暫停其它使用者的線程,等垃圾回收結束,使用者線程才恢複運作
  • 當對象壽命超過門檻值時,會晉升至老年代,最大壽命是15(4bit)
  • 當老年代空間不足,會先嘗試觸發 minor gc,如果之後空間仍不足,那麼觸發 full gc,STW的時間更長

3、記憶體配置設定的方法:TLAB

每個線程可以向 Java 虛拟機申請一段連續的記憶體,比如 2048 位元組,作為線程私有的 TLAB。這個操作需要加鎖,線程需要維護兩個指針(實際上可能更多,但重要也就兩個),一個指向 TLAB 中空餘記憶體的起始位置,一個則指向 TLAB 末尾。接下來的 new 指令,便可以直接通過指針加法(bump the pointer)來實作,即把指向空餘記憶體位置的指針加上所請求的位元組數。

4、卡表(Card Table)

出現原因:

老年代的對象可能引用新生代的對象,在Minor GC标記存活對象的時候,我們需要掃描老年代中的對象。如果該對象擁有對新生代對象的引用,那麼這個引用也會被作為 GC Roots。為了不進行全掃描(full GC),出現了卡表技術。

實作方法:

  • 将整個堆劃分為一個個大小為 512 位元組的卡,并且維護一個卡表,用來存儲每張卡的一個辨別位。這個辨別位代表對應的卡是否可能存有指向新生代對象的引用。
  • 如果可能存在,那麼我們就認為這張卡是髒的。
  • 在進行 Minor GC 的時候,我們便可以不用掃描整個老年代,而是在卡表中尋找髒卡,并将髒卡中的對象加入到 Minor GC 的 GC Roots 裡。
  • 當完成所有髒卡的掃描之後,Java 虛拟機便會将所有髒卡的辨別位清零。
  • 由于 Minor GC 伴随着存活對象的複制,而複制需要更新指向該對象的引用。是以,在更新引用的同時,我們又會設定引用所在的卡的辨別位。這個時候,我們可以確定髒卡中必定包含指向新生代對象的引用。

五、垃圾回收器

1、串行(單線程)

-XX:+UseSerialGC = Serial + SerialOld

分為兩個收集器:分别為Serial,SerialOld

特點:

  • 單線程:隻用一條單線程執行垃圾收集工作
  • 垃圾回收時,所用的線程必須暫停。
  • 優勢:簡單高效,由于采用的是單線程的方法,是以與其他類型的收集器相比,對單個cpu來說沒有了上下文之間的的切換,效率比較高。
  • 缺點:使用者會在不知道的情況下停止所有工作線程,使用者體驗感極差,令人難以接受。
  • 适用場景:Client 模式(桌面應用);單核伺服器。

其中SerialOld用法:

  • 和Serial一起使用
  • 與Parallel Scavenge收集器搭配
  • 作為CMS收集器的後備方案,在并發收集發生Concurrent Mode Failure時使用

2、 吞吐量優先

分為兩個收集器:Parallel Scavenge,Parallel Old

實作方法:要垃圾收集時所有使用者線程暫停并全部進入GC線程進行垃圾回收

優點:

  • 追求高吞吐量,高效利用CPU,是吞吐量優先,且能進行精确控制。
  • 支援多線程

缺點:stop the world 次數較多

3、響應時間優先

分為兩個收集器:分别為ParNewGC,CMS

ParNewGC工作方式:當使用者線程都執行到安全點時,所有線程暫停執行,采用複制算法進行垃圾收集工作,完成之後,使用者線程繼續開始執行。

與Parallel Scavenge不同的是,ParNew收集器關注點在于盡可能的縮短垃圾收集時使用者線程的停頓時間

CMS工作流程:

  • 初始标記,标記GC Roots 能夠直接關聯到達對象
  • 并發标記,進行GC Roots Tracing 的過程
  • 重新标記,修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分标記記錄
  • 并發清除,用标記清除算法清除對象。

其中初始标記和重新标記這兩個步驟仍然需要"stop the world"。耗時最長的并發标記與并發清除過程收集器線程都可以與使用者線程一起工作,總體上來說CMS收集器的記憶體回收過程是與使用者線程一起并發執行的。

GC Roots Tracing(跟搜尋算法):JVM中對記憶體進行回收時,需要判斷對象是否仍在使用中,可以通過GC Roots Tracing辨識。

CMS優點:

  • 并發收集
  • 低停頓

CMS缺點:

  • CMS收集器對CPU資源非常敏感,CMS預設啟動對回收線程數(CPU數量+3)/4,當CPU數量在4個以上時,并發回收時垃圾收集線程不少于25%,并随着CPU數量的增加而下降,但當CPU數量不足4個時,對使用者影響較大。
  • CMS無法處理浮動垃圾,可能會出現“Concurrent Mode Failure”失敗而導緻一次FullGC的産生。這時會地洞後備預案,臨時用SerialOld來重新進行老年代的垃圾收集。由于CMS并發清理階段使用者線程還在運作,伴随程式運作自然還會有新的垃圾産生,這部分垃圾出現在标記過程之後,CMS無法在當次處理掉,隻能等到下一次GC,這部分垃圾就是浮動垃圾。同時也由于在垃圾收集階段使用者線程還需要運作,那也就需要預留足夠的記憶體空間給使用者線程使用,是以CMS收集器不能像其他老年代幾乎完全填滿再進行收集。可以通過參數-XX:CMSInitiatingOccupancyFraction修改CMS觸發的百分比。
  • 因為CMS采用的是标記清除算法,是以垃圾回收後會産生空間碎片。通過參數可以進行優化。

适用場景:

  • 重視伺服器響應速度,要求系統停頓時間最短。

4、G1收集器

是JDK9的預設垃圾收集器

特點:

  • 并行與并發。G1能充分利用多CPU,多核環境下的硬體優勢。
  • 分代收集。能夠采用不同的方式去處理新建立的對象和已經存活了一段時間的對象,不需要與其他收集器進行合作。
  • 空間整合。G1從整體上來看基于“标記-整理”算法實作的收集器,從局部上看是基于複制算法實作的,是以G1運作期間不會産生空間碎片。
  • 可預測的停頓。G1能建立可預測的時間停頓模型,能讓使用者明确指定一個長度為M毫秒的時間片段内,消耗在垃圾收集上的時間不得超過N毫秒。

實作方法:

  • G1收集器将這個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但兩者之間不是實體隔離的。他們都是一部分Region的集合。
  • 每一個方塊就是一個區域,每個區域可能是 Eden、Survivor、老年代,每種區域的數量也不一定。JVM 啟動時會自動設定每個區域的大小(1M ~ 32M,必須是 2 的次幂),最多可以設定 2048 個區域(即支援的最大堆記憶體為 32M*2048 = 64G),假如設定 -Xmx8g -Xms8g,則每個區域大小為 8g/2048=4M。
  • G1收集器可以有計劃地避免在整個Java堆全區域的垃圾收集。G1可以跟蹤各個Region裡面垃圾堆積的價值大小(回收所獲得的空間大小及回收所需時間的經驗值),在背景維護一個優先清單,每次根據允許的收集時間,收集加載最大的region,這種方式保證了有限時間内可以擷取盡可能多高的收集效率。
  • 為了在 GC Roots Tracing 的時候避免掃描全堆,在每個 Region 中,都有一個 Remembered Set 來實時記錄該區域内的引用類型資料與其他區域資料的引用關系(在前面的幾款分代收集中,新生代、老年代中也有一個 Remembered Set 來實時記錄與其他區域的引用關系),在标記時直接參考這些引用關系就可以知道這些對象是否應該被清除,而不用掃描全堆的資料。
  • 過程為:初始标記、并發标記、最終标記、篩選回收

具體過程:

  • 初始标記STW。标記出GC Roots直接關聯的對象,這個階段速度較快,需要停止使用者線程,單線程執行。
  • 并發标記。從 GC Root 開始對堆中的對象進行可達性分析,找出存活對象,這個階段耗時較長,但可以和使用者線程并發執行。
  • 最終标記STW。修改在并發标記階段使用者程式執行而産生變動的标記記錄。
  • 篩選回收STW。在回收階段會對各個 Region 的回收價值和成本進行排序,根據使用者所期望的 GC 停頓時間來指定回收計劃(用最少的時間來回收包含垃圾最多的區域,這就是 Garbage First ,第一時間清理垃圾最多的區塊),這裡為了提高回收效率,并沒有采用和使用者線程并發執行的方式,而是停頓使用者線程。

适用場景:要求盡可能可控 GC 停頓時間;記憶體占用較大的應用。可以用 -XX:+UseG1GC 使用 G1 收集器,jdk9 預設使用 G1 收集器

六、GC相關問題:

\1. JVM的stop-the-world機制非常不友好,有哪些解決之道?原理是什麼?

采用并行GC可以減少需要STW的時間,它們會在即時編譯器生成的代碼中加入寫屏障或者讀屏障。

壓測時出現頻繁的GC容易了解,但是有時出現毛刺(CPU占用一下子變低)是因為什麼呢?

Y軸應該是時間,那毛刺就是長暫停,一般Full GC就會造成長暫停。

Full GC有卡頓,對性能很不利,怎麼避免呢?

通過調整新生代大小,使對象在其生命周期内都待在新生代中。這樣一來,Minor GC時就可以收集完這些短命對象了。

不管什麼垃圾回收器都會出現stop the word嗎?

目前的垃圾回收器多多少少需要stop the world,但都在朝着盡量減少STW時間發展。

完全的并發GC算法是存在的,但是在實作上一般都會在枚舉GC roots時進行STW。

壓縮算法是不是也用到了複制呢?

确實是需要複制資料,這樣起名主要是為了區分複制到同一個區域中(需要複雜的算法保證引用能夠正确更新),還是複制到另一個區域中(可以複制完後統一更新引用)。

JVM分代收集新生代對象進入老年代,年齡為什麼是15而不是其他的?

HotSpot會在對象頭中的标記字段裡記錄年齡,配置設定到的空間隻有4位,最多隻能記錄到15。