深入瞭解JVM之垃圾回收(二)

一、前言

    爲了深化知識體系的建立,筆者將採用提問的方式展開論述,欲通過一個個不斷深入的問題強化知識點之間的聯繫。

二、問題

1、哪些內存需要回收?

    引用計數算法:
    爲每一個對象創建一個引用計數器,當其他對象引用該對象,引用計數器的值就加一,當引用失效時,引用計數器的值就減一。如果引用計數器的值爲0,則說明該對象可以被回收。
    這種算法的弊端是無法解決兩個對象互相引用的問題。
    根搜索算法(目前使用):
    通過一個叫做“GC Roots”的對象作爲起始點,向下開始搜索,經過的路徑稱爲引用鏈。如果一個對象與GC Roots之間沒有任何引用鏈相連,則認爲該對象不可達,可以回收。

2、在根搜索算法中,什麼對象可以成爲GC Roots?

    虛擬機棧局部變量表引用的對象
    本地方法棧native方法引用的對象
    方法區中的類靜態變量引用的對象
    方法區中的常量引用的對象

3、在根搜索算法中,如何確定對象死亡?

    確定對象死亡至少需要經歷兩次標記。
    當對象與GC Roots之間沒有任何引用鏈(也就是對象不可達)時,會進行第一次標記。標記完會進行一次篩選,如果該對象重寫了finalize方法或者沒有執行過finalize方法,那麼就會將該對象加入F-Queue隊列,稍後JVM會分配一個低優先級的Finalizer線程去異步執行隊列中所有對象的finalize方法。如果是同步執行,只要其中一個對象的finalize方法阻塞了,就會影響其他對象的finalize方法的執行,以至於其他對象不能進行回收。
    稍後,JVM會在F-Queue隊列中進行小規模的第二次標記

4、方法區需要回收對象嗎?回收什麼對象?這個對象需要具備什麼條件?

    需要。雖然方法區回收對象的效率低,但是不回收對象可能會引起OOM問題。
    方法區回收的對象是無用的常量和類。無用常量指的是如果一個常量不被任何地方引用,那麼這個常量就可以回收。無用類的判斷就相對複雜一點,需要滿足下列3個條件:
    1)該類的所有實例均已回收。
    2)加載該類的ClassLoader已回收。
    3)該類對應Class對象沒有任何地方被引用。
    達到了這三個條件,說明類可以回收了。是否回收需要根據JVM是否設置了-Xnoclassgc參數來判斷。

5、什麼時候觸發垃圾回收?

    創建一個新對象,優先在Eden區分配內存。如果Eden區沒有足夠大的空間分配內存,這個時候會觸發Minor GC。Minor GC將Eden區和From Survivor區的內存進行垃圾回收,將存活的對象複製到To Survivor區,清空Eden和From Survivor區,這就完成了一次Minor GC。From Survivor區和To Survivor區是相對的關係,哪個區中有對象,哪個區就是From Survivor區。
    當Survior區的剩餘空間不足以存放新對象時,JVM會進行分配擔保,新對象直接晉升到老年代。當老年代的剩餘空間不足以存放晉升到老年代的對象時,會進行一次Major GC(Full GC)。這個時候有人就會問了,JVM怎麼知道這次晉升到老年代的對象需要多少空間?是的,它不知道,所以它只能取之前每一次晉升到老年代的對象容量的平均大小值作爲參考。
    Major GC的效率要比Minor GC慢十倍以上,所以要盡力避免Major GC。

6、什麼情況下新生代的對象會晉升到老年代?

    1)Survivor區不足以存放新對象時,新對象會通過分配擔保晉升到老年代。
    2)大對象直接晉升老年代。 爲了避免大對象在Eden區和Survivor區的低效的內存拷貝,JVM設置了一個對象大小閾值(可通過JVM參數設置),只要超過這個閾值的對象,就直接晉升到老年代。
    3)長期存活的對象晉升老年代。 因爲新生代和老生代的內存使用情況是不同的,所以虛擬機使用分代收集的思想來回收內存,即針對不同區域使用不同的垃圾回收算法。因爲需要知道在內存回收後存活的對象應該放在哪裏區域,所以JVM爲每個對象設置一個對象年齡計數器。每經過一次Minor GC並且被Survior區容納,年齡就增加1,當年齡達到一定程度(默認是15歲),該對象就會晉升到老年代。
    4)動態對象年齡判定。 如果Survivor空間中相同年齡的對象大小總和大於總空間的一半,那麼大於或者等於這個年齡的對象直接進入老年代,無需達到年齡閾值。

7、爲了減少Major GC的觸發,JVM做哪些優化?

    默認開啓擔保失敗。在發生Minor GC時,JVM會檢測當老年代的剩餘空間是否小於之前每一次晉升到老年代的對象容量的平均大小。如果小於,則進行一次Major GC。如果大於,則要看是否開啓了擔保失敗,如果開啓了,就只會進行Minor GC。否則,還是會進行Full GC。

8、垃圾收集算法有哪些?

    標記-清除算法:
    採用之前提到的根搜索算法進行標記,之後統一回收被標記的對象。這個垃圾收集算法是最基礎的一個,往後的垃圾收集算法很多是基於這個進行改進的。
    這個算法有兩個缺點:一是效率問題,標記和回收的效率都不高;二是空間問題,清除後產生大量的不連續的空間碎片,空間碎片的增多會使得可分配的連續的空間減少,這樣就加快下一次GC。
    複製算法:
    將內存空間分爲容量大小相等的兩塊,一次只使用其中的一塊。當正在使用的內存空間即將用完時,會觸發一次GC。JVM會將已標記的對象複製到另一塊未使用的內存空間,再把已使用的空間清理掉。
    實際上,新生代大部分是朝生夕死的,GC後仍存活的對象只佔少部分,如果按照1:1去劃分內存空間,那將會是極大的浪費。正因爲這個原因,JVM將內存劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次只使用Eden空間和其中一塊Survivor空間。當回收時,將Eden空間和其中一塊Survivor空間存活的對象複製到另一塊Survivor空間上,並清除Eden空間和之前的Survivor空間。
    萬一複製的時候另一塊Survivor空間的內存空間不足以存放存活的對象,那怎麼辦?別擔心,JVM還提供了分配擔保的機制,可以把放不下的對象存到老年代中,相當於向老年代“借”了點空間。
    現在商用虛擬機都採用這個算法來回收新生代。
    標記-整理算法:
    對於老年代,大部分對象都是很難回收的,如果採用複製算法,那麼絕大部分的對象都需要進行復制,性能會下降很多。這個時候就有人提出了標記-整理算法——回收時讓所以已存活對象移動到同一端,再清理掉端界外的對象。
    從JVM中對象訪問定位兩種方式一文中可以推斷出,移動對象的開銷是要比複製對象小很多的,所以對於老年代,標記-整理算法比複製算法的效率要高得多。
    分代收集算法:
    根據各個年代的特點採用適當的垃圾收集算法。新生代採用複製算法,老年代使用標記-整理算法或標記-清除算法。

9、垃圾收集器有哪些?各有什麼特點?

    Serial收集器(新生代,複製算法):
    最基本、歷史最悠久的單線程收集器。因爲是單線程,所以不能一邊運行工作線程,一邊進行垃圾回收,在垃圾回收的時候必須暫停其他所有的工作線程,這種現象就叫做“Stop The World”。
    它是虛擬機運行在Client模式下的默認收集器,簡單而高效。在用戶的桌面應用場景中,分配給虛擬機管理的內存一般不會太大,收集幾十兆甚至兩百兆的新生代,停頓時間完全可以控制在幾十毫秒最多一百多毫秒之內,只要不是頻繁發生,這點停頓是可以接受的。
    ParNew收集器(新生代,複製算法):
    ParNew收集器是Serial收集器的多線程版本。相對於Serial收集器,在單線程的環境下,性能沒有Serial收集器好,但是隨着CPU數量的增加,多線程帶來的性能提升會愈發明顯。
    達到一個可控制的吞吐量
    Parallel Scavenger 收集器(新生代,複製算法,並行):
    Parallel Scavenger 收集器的目標是達到一個可控制的吞吐量吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。
    提供了兩個參數來控制吞吐量,分別是控制最大垃圾收集時間和-XX:MacGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。最大垃圾收集時間不應該設置得過於小,因爲GC停頓時間的縮短是以犧牲吞吐量和新生代空間來換取的。
    開啓-XX:+UseAdaptiveSizePolicy參數,則開啓了GC的自適應調節策略——虛擬機根據當前程序的運行情況收集性能監控信息,動態調整晉升年齡、Eden區和Survivor區比例等參數以提供最合適的停頓時間或最大吞吐量。
    Serial Old收集器(老年代,標記-整理算法):
    Serial Old收集器是一個單線程收集器。這個收集器的主要意義是被Client模式下的虛擬機使用。如果在Server模式下,它有兩個用途:
    一是在JDK1.5及之前版本與Parallel Scavenger 收集器搭配使用。
    二是作爲CMS收集器的後備預案
    Parallel Old收集器(老年代,標記-整理算法):
    Parallel Scavenger 收集器老年代的版本。JDK1.6中才開始提供,在此之前Parallel Scavenger 收集器的處境會比較尷尬——能與它匹配的老年代收集器只有Serial Old。在多CPU等硬件比較高級的環境中,Parallel Scavenger+Serial Old的組合甚至比不上ParNew+CMS的組合。
    CMS收集器(老年代,標記-清除算法):
    全稱是Concurrent Mak Sweep,目的是獲取最短回收停頓時間。它的運作過程複雜一點,流程如下:
在這裏插入圖片描述
    CMS收集器的優點是併發收集、低停頓,它的缺點主要有三點:
    一是CMS收集器對CPU資源比較敏感。在併發階段,會佔用一部分CPU資源(線程),從而導致程序變慢。
    二是無法處理浮動垃圾。所謂的浮動垃圾指的是併發清除過程中產生的垃圾。因爲浮動垃圾的存在,JVM不能等到老年代會滿了才進行GC,需要預留一定的空間,默認是68%會觸發GC,這個數值是可以通過參數修改的。如果CMS運行期間預留的內存無法滿足,就會出現一次"Concurrent Mode Fail"失敗,這個時候JVM會啓動Serial Old收集器重新進行GC。
    三是標記-清除算法帶來的空間碎片問題——GC後產生大量的不連續的空間碎片。爲了解決這個問題,JVM提供XX:UseCMSCompactAtFullCollection參數,作用是在Full GC後進行一次碎片整理。
    G1收集器(老年代,標記-整理算法):
    JDK1.6開始提供使用。它有兩個顯著的優點:
    一是標記-整理算法不會產生內存碎片。
    二是它可以非常精確地控制停頓,能讓使用者明確指定一個長度爲M毫秒的時間片段內,消耗在垃圾收集的時間不超過N毫秒。
    G1收集器可以實現在基本不犧牲吞吐量的情況下完成低停頓的內存回收,這是由於它極力的避免全區域的回收,G1收集器將Java堆(包括新生代和老年代)劃分爲多個大小固定的獨立區域(Region),並且追蹤這些區域的垃圾堆積程度,在後臺維護一個優先列表,每次根據允許的時間,優先回收垃圾最多的區域 。(這就是Garbage First名稱的來由)

10、如何根據業務場景選擇合適的垃圾收集器?

    引用Java——七種垃圾收集器+JDK11最新ZGC文章的一張圖解答:在這裏插入圖片描述

三、參考

《深入瞭解Java虛擬機:JVM高級特性與最佳實踐》書籍
圖解 JVM GC 過程
JVM中對象訪問定位兩種方式
Java——七種垃圾收集器+JDK11最新ZGC

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