十個問題弄清JVM&GC(二)

每一個java開發同窗無論是平常工做中仍是面試裏,都會遇到JDK、JVM和GC的問題。本文會從如下10個問題爲切入點,帶着你們一塊兒全面瞭解一下JVM的方方面面。java

  1. JVM、JRE和JDK的區別和聯繫
  2. JVM是什麼?以及它的主要做用
  3. JVM的核心功能有哪些
  4. 類加載機制和過程
  5. 運行時數據區的邏輯結構
  6. JVM的內存模型
  7. 如何肯定對象是垃圾
  8. 垃圾收集的算法有哪些
  9. 各類問世的垃圾收集器
  10. JVM調優的參數配置

上一篇文章結尾時咱們談到,就JVM的設計規範,從使用用途角度JVM的內存大致的分爲:線程私有內存區 和 線程共享內存區。linux

十個問題弄清JVM&GC(二)

線程私有內存區在類加載器編譯某個class文件時就肯定了執行時須要的「程序計數器」和「虛擬棧幀」等所需的空間,而且會伴隨着當前執行線程的產生而產生,執行線程的消亡而消亡,所以「線程私有內存區」並不須要考慮內存管理和垃圾回收的問題。線程共享內存區在虛擬機啓動時建立,被全部線程共享,是Java虛擬機所管理內存中最應該關注的和最大的一塊。首先咱們來一塊兒看一下「線程共享內存區」的內存模型是什麼樣的?面試

六、JVM的內存模型

十個問題弄清JVM&GC(二)

如圖所示,JVM的內存結構分爲堆和非堆兩大塊區域。算法

  • 其中「非堆」就是上篇文章咱們提到的方法區或叫元數據區,用來存儲class類信息的。
  • 而「堆」是用來存儲JVM各線程執行期間所建立的實例對象或數組的。堆區分爲兩大塊,一個是Old區,一個是Young區。Young區分爲兩大塊,一個是Survivor區(S0+S1),一塊是Eden區S0和S1同樣大,也能夠叫From和To。

之因此這樣劃分,設計者的目的無非就是爲了內存管理,也就是咱們說的垃圾回收。那麼什麼樣的對象是垃圾?垃圾回收算法有哪些?目前經常使用的垃圾回收器又有哪些?這篇文章咱們一塊兒弄清楚這些問題和知識點。數組

七、如何肯定一個對象是垃圾?

要想進行垃圾回收,得先知道什麼樣的對象是垃圾。目前確認對象是否爲垃圾的算法主要有兩種:引用計數法和可達性分析法。安全

  • 一、引用計數法:在對象中添加了一個引用計數器,當有地方引用這個對象時,引用計數器的值就加1,當引用失效的時候,引用計數器的值就減1。當引用計數器的值爲0時,JVM就開始回收這個對象。

對於某個對象而言,只要應用程序中持有該對象的引用,就說明該對象不是垃圾,若是一個對象沒有任何指針對其引用,它就是垃圾。這種方法雖然很簡單、高效,可是JVM通常不會選擇這個方法,由於這個方法會出現一個弊端:當對象之間相互指向時,兩個對象的引用計數器的值都會加1,而因爲兩個對象時相互指向,因此引用不會失效,這樣JVM就沒法回收。服務器

  • 二、可達性分析法:針對引用計數算法的弊端,JVM採用了另外一種算法,以一些"GC Roots"的對象做爲起始點向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的,便可以進行垃圾回收。不然,證實這個對象有用,不是垃圾。多線程

    十個問題弄清JVM&GC(二)
    上圖中的obj7和obj8雖然它們互相引用,但從GC Roots出發這兩個對象不可達,因此會被標記爲垃圾。JVM會把如下幾類對象做爲GC Roots:併發

  • (1) 虛擬機棧(棧幀中本地變量表)中引用的對象;
  • (2) 方法區中類靜態屬性引用的對象;
  • (3) 方法區中常量引用的對象;
  • (4) 本地方法棧中JNI(Native方法)引用的對象。

注:在可達性分析算法中不可達的對象,並非直接被回收,這時它們處於緩刑狀態,至少須要進行兩次標記纔會肯定該對象是否被回收:框架

第一次標記:若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記;

第二次標記:第一次標記後接着會進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法(該方法可將此對象與GC Roots創建聯繫)。在finalize()方法中沒有從新與引用鏈創建關聯關係的,將被進行第二次標記。

第二次標記成功的對象將真的會被回收,若是對象在finalize()方法中從新與引用鏈創建了關聯關係,那麼將會逃離本次回收,繼續存活。

八、垃圾收集的算法有哪些

知道了如何JVM肯定哪些對象是垃圾後,下面咱們來看一下,面對這些垃圾對象,JVM的回收算法都有哪些。

一、 標記-清除算法(Mark-Sweep)

  • 第一步「標記」,以下圖所示把堆裏全部的對象都掃描一遍,找出哪些是垃圾須要回收的對象,而且把它們標記出來。

    十個問題弄清JVM&GC(二)

  • 第二步「清除」,把第一步標記爲「UnReference Object」(無引用或不可達)的對象清除掉,釋放內存空間。
    十個問題弄清JVM&GC(二)
    這種算法的缺點主要有兩點:

(1) 標記和清除兩個過程都比較耗時,效率不高

(2) 清除後會產生大量不連續的內存碎片空間,碎片空間太多可能會致使當程序後續須要建立較大對象時,沒法找到足夠連續的內存空間而不得再也不次觸發垃圾回收。

二、 標記-複製算法(Mark-Copying)

將內存劃分爲兩塊區域,每次使用其中一塊,當其中一塊用滿,觸發垃圾回收的時候,將存活的對象複製到另外一塊上去,而後把以前使用的那一塊進行格式化,一次性清除乾淨。
十個問題弄清JVM&GC(二)
(清除前)
十個問題弄清JVM&GC(二)
(清除後)

「標記-複製」算法的缺點顯而易見,就是內存空間利用率低。

三、 標記-整理算法(Mark-Compact)

標記整理算法標記過程仍然與"標記-清除"算法同樣,可是後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
十個問題弄清JVM&GC(二)
將全部存活的對象向一邊移動,清理掉存活邊界之外的所有內存空間。
十個問題弄清JVM&GC(二)
結合這三種算法咱們能夠看到,

  • 「標記-複製」算法的優勢是回收效率高,但空間利用率上有必定的浪費。

  • 而「標記-整理」算法因爲須要向一側移動等一系列操做,其效率相對低一些,但對內存空間管理上十分優異。

  • 所以,「標記-複製」算法適用於那些生命週期短、回收頻率高的內存對象,

  • 而標記-整理」算法適用於那些生命週期長、回收頻率低,但注重回收一次內存空間獲得足夠釋放的場景。

所以JVM的設計者將JVM的堆內存,分爲了兩大塊區域Young區和Old區,Young區存儲的就是那些生命週期短,使用一兩次就再也不使用的對象,回收一次基本上該區域十之有八的對象所有被回收清理掉,所以Young區採用的垃圾回收算法也就是「標記-複製」算法。Old區存儲的是那些生命週期長,通過屢次回收後仍然存活的對象,就把它們放到Old區中,平時再也不去判斷這些對象的可達性,直到Old區不夠用爲止,再進行一次統一的回收,釋放出足夠的連續的內存空間。

九、各類問世的垃圾收集器

鑑於Young區和Old區須要採用不一樣的垃圾回收算法,所以在JVM的整個垃圾收集器的演進各個時代裏,針對Young區和Old區每一個時代都是不一樣的垃圾收集機制。從JDK1.3開始到目前,JVM垃圾收集器的演進大致分爲四個時代:串行時代、並行時代、併發時代和G1時代。

十個問題弄清JVM&GC(二)

一、串行時代:Serial(Young區)+ Serial Old(Old區)

JDK3(1.3)的時候,大概是2000年左右,那個時代基本計算機都是單核一個CPU的,所以垃圾回收最初的設計實現也是基於單核單線程工做的。而且垃圾回收線程的執行相對於正常業務線程執行來講仍是STW(stop the world)的,使用一個CPU或者一條收集線程去完成垃圾收集工做,這個線程執行的時候其它線程須要中止。

十個問題弄清JVM&GC(二)

串行收集器採用單線程stop-the-world的方式進行收集。當內存不足時,串行GC設置停頓標識,待全部線程都進入安全點(Safepoint)時,應用線程暫停,串行GC開始工做,採用單線程方式回收空間並整理內存。單線程也意味着複雜度更低、佔用內存更少,但同時也意味着不能有效利用多核優點。所以,串行收集器特別適合堆內存不高、單核甚至雙核CPU的場合。

二、並行時代:Parallel Scavenge(Young區) + Parallel Old(Old區)

並行收集器是以關注吞吐量爲目標的垃圾收集器,也是server模式下的默認收集器配置,對吞吐量的關注主要體如今年輕代Parallel Scavenge收集器上。

十個問題弄清JVM&GC(二)

並行收集器與串行收集器工做模式類似,都是stop-the-world方式,只是暫停時並行地進行垃圾收集。年輕代採用複製算法,老年代採用標記-整理,在回收的同時還會對內存進行壓縮。關注吞吐量主要指年輕代的Parallel Scavenge收集器,經過兩個目標參數-XX:MaxGCPauseMills和-XX:GCTimeRatio,調整新生代空間大小,來下降GC觸發的頻率。並行收集器適合對吞吐量要求遠遠高於延遲要求的場景,而且在知足最差延時的狀況下,並行收集器將提供最佳的吞吐量。

三、 併發時代:CMS(Old區)

併發標記清除(CMS)是以關注延遲爲目標、十分優秀的垃圾回收算法,CMS是針對Old區的垃圾回收實現。

十個問題弄清JVM&GC(二)

老年代CMS每一個收集週期都要經歷:初始標記、併發標記、從新標記、併發清除。其中,初始標記以STW的方式標記全部的根對象;併發標記則同應用線程一塊兒並行,標記出根對象的可達路徑;在進行垃圾回收前,CMS再以一個STW進行從新標記,標記那些由mutator線程(指引發數據變化的線程,即應用線程)修改而可能錯過的可達對象;最後獲得的不可達對象將在併發清除階段進行回收。值得注意的是,初始標記和從新標記都已優化爲多線程執行。CMS很是適合堆內存大、CPU核數多的服務器端應用,也是G1出現以前大型應用的首選收集器。

- 但CMS有如下兩個缺陷:

  • (1)因爲它是標記-清除不是標記-整理,所以會產生內存碎片,Old區會隨着時間的推移而終究被耗盡或產生沒法分配大對象的狀況。最後不得不經過底層的擔保機制(CMS背後有串行的回收做爲兜底)進行一次Full GC,並進行內存壓縮。
  • (2)因爲標記和清除都是通應用線程併發進行,兩類線程同時執行時會增長堆內存的佔用,一旦某一時刻內存不夠用,就會觸發底層擔保機制,又採用串行回收進行一次STW的垃圾回收。

四、G1時代:Garbage First

G1收集器時代,Java堆的內存佈局與就與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。

十個問題弄清JVM&GC(二)

如上圖所示,每個Region(分區)大小都是同樣的,1~32M之間的數值,但必須是2的指數。設置Region大小經過如下參數:-XX:G1HeapRegionSize=M。
G1收集器的原理或特色主要有如下三點:

(1)內存邏輯上仍保留的分代的概念,每個Region同一時間要麼被標記爲新生代,要麼被標記爲老年代,要麼處於空閒;

(2)總體上採用了「標記-整理算法」,不會產生內存碎片

(3)可預測的停頓,G1總體採用的策略是「篩選回收」,也就是回收前會對各個待回收的Region的回收價值和成本進行排序,根據G1配置所指望的回收時間,選擇排在前面的幾個Region進行回收。

十個問題弄清JVM&GC(二)

其實之因此叫G1(Garbage First)就是由於它優先選擇回收垃圾比較多的Region分區。
總體G1的垃圾回收工做步驟分爲:初始標記、併發標記、最終標記和篩選回收。

五、ZGC:Zero GC

這篇文章簡單提一下這個最新問世的垃圾收集器,之因此叫「Zero GC」是由於它追求的是更低的GC停頓時間,追求的目標是:支持TB級堆內存(最大4T)、最大GC停頓10ms。JDK11新引入的ZGC收集器,無論是物理上仍是邏輯上,ZGC中已經不存在新老年代的概念了會分爲一個個page,當進行GC操做時會對page進行壓縮,所以沒有碎片問題。因爲其是JDK11和只能在64位的linux上使用,所以目前用得還比較少。

結語

以上整體兩篇文章七千字,就是我從JVM的做用、設計框架到JVM內存管理的總體的體系化理解。感謝。

拓展閱讀:十個問題弄清JVM&GC(二)

做者:宜信技術學院 譚文濤

相關文章
相關標籤/搜索