<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          解析JVM之垃圾收集篇,超詳細!

          共 8141字,需瀏覽 17分鐘

           ·

          2021-03-06 09:03

          點擊藍色“程序員的時光 ”關注我 ,標注“星標”,及時閱讀最新技術文章!

          寫在前面:
          小伙伴兒們,大家好!上一次我們了解了JVM基礎知識——全面解析JVM,超詳細!
          今天來學習JVM垃圾回收相關內容,作為面試必問的知識點,來深入了解一波!
          思維導圖:

          1,判斷對象是否死亡

          我們在進行垃圾回收(Garbage Collection,簡稱GC)之前肯定要先判斷哪些是垃圾。
          在堆中幾乎放著所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)。
          對象死亡

          1.1,引用計數(shù)算法

          給對象添加一個引用計數(shù)器,每當有一個地方引用該對象時,計數(shù)器+1,當引用失效時,計數(shù)器-1,任何時候當計數(shù)器為0的時候,該對象不再被引用。
          引用計數(shù)器這個方法實現(xiàn)簡單,判定效率也高。但是,當前主流的虛擬機都沒有采用這個算法來管理內存,其中最主要的原因是它很難解決對象之間互相循環(huán)引用的問題。
          所謂對象之間互相循環(huán)引用,如下面代碼所示:除了對象 objA 和 objB 相互引用著對方之外,這兩個對象之間再無任何引用。但是它們因為互相引用對方,導致它們的引用計數(shù)器都不為 0,于是引用計數(shù)算法無法通知 GC 回收器回收他們。
          public class ReferenceCountingGc {
              public Object instance = null;
              public static final int _1MB = 1024*1024;
              public static void main(String[] args) {
                  ReferenceCountingGc objA = new ReferenceCountingGc();
                  ReferenceCountingGc objB = new ReferenceCountingGc();
                  objA.instance = objB;
                  objB.instance = objA;
                  objA = null;
                  objB = null;
              }
          }

          1.2,可達性分析算法

          這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節(jié)點開始向下搜索,節(jié)點所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的。
          可達性分析算法
          在Java語言中,可作為GC Roots的對象包括下面幾種:
          • 虛擬機棧(棧幀中的本地變量表)中引用的對象
          • 方法區(qū)中類靜態(tài)屬性引用的對象
          • 方法區(qū)中常量引用的對象
          • 本地方法棧(Native 方法)中引用的對象

          2,再談引用

          無論是通過引用計數(shù)法判斷對象引用數(shù)量,還是通過可達性分析法判斷對象的引用鏈是否可達,判定對象的存活都與“引用”有關。
          JDK1.2 之前,Java 中引用的定義很傳統(tǒng):如果 reference 類型的數(shù)據(jù)存儲的數(shù)值代表的是另一塊內存的起始地址,就稱這塊內存代表一個引用。JDK1.2 以后,Java 對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)。

          2.1,強引用

          以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。類似于“Object obj=new Object()”這類的引用,如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。

          2.2,軟引用

          如果一個對象只具有軟引用,那就類似于可有可無的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現(xiàn)內存敏感的高速緩存。
          軟引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對象被垃圾回收,JAVA 虛擬機就會把這個軟引用加入到與之關聯(lián)的引用隊列中。

          2.3,弱引用

          如果一個對象只具有弱引用,那就類似于可有可無的生活用品。弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的內存區(qū)域的過程中,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優(yōu)先級很低的線程, 因此不一定會很快發(fā)現(xiàn)那些只具有弱引用的對象。
          弱引用可以和一個引用隊列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對象被垃圾回收,Java 虛擬機就會把這個弱引用加入到與之關聯(lián)的引用隊列中。

          2.4,虛引用

          "虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。
          虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區(qū)別在于:虛引用必須和引用隊列(ReferenceQueue)聯(lián)合使用。當垃圾回收器準備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯(lián)的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發(fā)現(xiàn)某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
          特別注意,在世紀程序設計中一般很少使用弱引用與虛引用,使用軟用的情況較多,這是因為軟引用可以加速JVM對垃圾內存的回收速度,可以維護系統(tǒng)的運行安全,防止內存溢出(OutOfMemory)等問題的產生。

          3,廢棄常量以及無用類

          3.1,如何判斷一個常量是廢棄常量?

          運行時常量池主要回收的是廢棄的常量。那么,我們如何判斷一個常量是廢棄常量呢?
          假如在常量池中存在字符串"abc" ,如果當前沒有任何String對象引用該字符串常量的話,就說明常量"abc"就是廢棄常量,如果這時發(fā)生內存回收的話而且有必要的話," abc"就會被系統(tǒng)清理出常量池。

          3.2,如何判斷一個類是無用的類?

          判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。方法區(qū)主要回收無用的類,類需要同時滿足下面3個條件才能算是 “無用的類” :
          • 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
          • 加載該類的 ClassLoader 已經被回收。
          • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
          虛擬機可以對滿足上述3個條件的無用類進行回收,這里說的僅僅是“可以”,而并不是和對象一樣不使用了就會必然被回收。

          4,垃圾收集算法

          4.1,標記--清除算法

          該算法分為“標記”和“清除”階段:首先標記出所有不需要回收的對象,在標記完成后統(tǒng)一回收掉所有沒有被標記的對象。它是最基礎的收集算法,后續(xù)的算法都是對其不足進行改進得到。這種垃圾收集算法會帶來兩個明顯的問題:
          • 效率問題:標記和清除兩個過程的效率都不高;
          • 空間問題:標記清除之后會產生大量不連續(xù)的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內存而不得不提前觸發(fā)另一次垃圾收集動作。
          標記-清除算法

          4.2,復制算法

          為了解決效率問題,“復制”收集算法出現(xiàn)了。它將可用內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完后,就將還存活的對象復制到另一塊去,然后再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區(qū)間的一半進行回收。實現(xiàn)簡單,運行高效。
          復制算法

          4.3,標記--整理算法

          根據(jù)老年代的特點提出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然后直接清理掉端邊界以外的內存。
          標記-整理算法

          4.4,分代收集算法

          當前虛擬機的垃圾收集都采用分代收集算法,這種算法沒有什么新的思想,只是根據(jù)對象存活周期的不同將內存分為幾塊。一般將 java 堆分為新生代和老年代,這樣我們就可以根據(jù)各個年代的特點選擇合適的垃圾收集算法。
          比如在新生代中,每次收集都會有大量對象死去,所以可以選擇復制算法,只需要付出少量對象的復制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”算法進行垃圾收集。

          5,垃圾收集器

          如果說收集算法是內存回收的方法論,那么垃圾收集器就是內存回收的具體實現(xiàn)。
          雖然我們對各個收集器進行比較,但并非要挑選出一個最好的收集器。因為直到現(xiàn)在為止還沒有最好的垃圾收集器出現(xiàn),更加沒有萬能的垃圾收集器,我們能做的就是根據(jù)具體應用場景選擇適合自己的垃圾收集器。試想一下:如果有一種四海之內、任何場景下都適用的完美收集器存在,那么我們的 HotSpot 虛擬機就不會實現(xiàn)那么多不同的垃圾收集器了。
          常見的垃圾收集器

          5.1,Serial收集器

          Serial收集器是最基本、歷史最悠久的垃圾收集器了。從名字上看是串行的意思,這個收集器是一個單線程的新生代收集器。它的 “單線程” 的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( "Stop The World" ),直到它收集結束。
          Serial收集器
          Serial 采取 “復制算法” 實現(xiàn),如果是在單 CPU 環(huán)境下,Serial 收集器沒有線程交互的開銷,理論上是可以獲得最高的單線程執(zhí)行效率,STW 的時間也可以控制在幾十到幾百毫秒內,這個時間是完全可以接受的。
          與其他單線程收集器相比它的優(yōu)點就是:它簡單而高效(與其他收集器的單線程相比)。簡單而高效 Serial 收集器由于沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial 收集器對于運行在 Client 模式下的虛擬機來說是個不錯的選擇。

          5.2,ParNew收集器

          ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行垃圾收集外,其余行為(控制參數(shù)、收集算法、回收策略等等)和 Serial 收集器完全一樣。ParNew 收集器雖然有多線程優(yōu)勢,但在單 CPU 和多 CPU 環(huán)境下,效果并不一定會比 Serial 好,至少在單 CPU 環(huán)境下是肯定不如的 Serial 的。由于線程交互開銷的時間,效果并不如人意,多線程的好處在于更高效率地利用 CPU ,提高 CPU 的吞吐量,讓 CPU 空閑的時間減少。
          新生代采用復制算法,老年代采用標記-整理算法。
          ParNew收集器
          它是許多運行在 Server 模式下的虛擬機的首要選擇,除了 Serial 收集器外,只有它能與 CMS 收集器(真正意義上的并發(fā)收集器,后面會介紹到)配合工作。
          • 并行(Parallel) :指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態(tài)。
          • 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時執(zhí)行(但不一定是并行,可能會交替執(zhí)行),用戶程序在繼續(xù)運行,而垃圾收集器運行在另一個 CPU 上。

          5.3,Parallel Scavenge收集器

          Parallel Scavenge 收集器也是使用復制算法的多線程收集器,它看上去幾乎和ParNew都一樣。那么它有什么特別之處呢?
          Parallel Scavenge 收集器關注點是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是 CPU 中用于運行用戶代碼的時間與 CPU 總消耗時間的比值。 Parallel Scavenge 收集器提供了很多參數(shù)供用戶找到最合適的停頓時間或最大吞吐量,如果對于收集器運作不太了解,手工優(yōu)化存在困難的時候,使用Parallel Scavenge收集器配合自適應調節(jié)策略,把內存管理優(yōu)化交給虛擬機去完成也是一個不錯的選擇。
          新生代采用復制算法,老年代采用標記-整理算法。
          Parallel Scavenge收集器
          這是 JDK1.8 默認收集器
          使用java -XX:+PrintCommandLineFlags -version命令查看
          -XX:InitialHeapSize=197918400 -XX:MaxHeapSize=3166694400 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
          java version "1.8.0_144"
          Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
          Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
          查看結果
          JDK1.8 默認使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 參數(shù),則默認指定了-XX:+UseParallelOld GC,可以使用-XX:-UseParallelOldGC 來禁用該功能。

          5.4,Serial Old收集器

          Serial 收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在 JDK1.5 以及以前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途是作為 CMS 收集器的后備方案。

          5.5,Parallel Old 收集器

          Parallel Scavenge 收集器的老年代版本。使用多線程和“標記-整理”算法。在注重吞吐量以及 CPU 資源的場合,都可以優(yōu)先考慮 Parallel Scavenge 收集器和 Parallel Old 收集器。

          5.6,CMS 收集器

          CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應用上使用。
          CMS(Concurrent Mark Sweep)收集器是 HotSpot 虛擬機第一款真正意義上的并發(fā)收集器,它第一次實現(xiàn)了讓垃圾收集線程與用戶線程(基本上)同時工作。
          從名字中的Mark Sweep這兩個詞可以看出,CMS 收集器是一種 “標記-清除”算法實現(xiàn)的,它的運作過程相比于前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟:
          • 初始標記: 暫停所有的其他線程,并記錄下直接與 root 相連的對象,速度很快 ;
          • 并發(fā)標記: 同時開啟 GC 和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構并不能保證包含當前所有的可達對象。因為用戶線程可能會不斷的更新引用域,所以 GC 線程無法保證可達性分析的實時性。所以這個算法里會跟蹤記錄這些發(fā)生引用更新的地方。
          • 重新標記: 重新標記階段就是為了修正并發(fā)標記期間因為用戶程序繼續(xù)運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比并發(fā)標記階段時間短
          • 并發(fā)清除: 開啟用戶線程,同時 GC 線程開始對未標記的區(qū)域做清掃。
          CMS收集器
          兩次STW,從它的名字就可以看出它是一款優(yōu)秀的垃圾收集器,主要優(yōu)點:并發(fā)收集、低停頓。但是它有下面三個明顯的缺點:
          • 對 CPU 資源敏感;
          • 無法處理浮動垃圾;
          • 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。
          什么是浮動垃圾?CMS在并發(fā)清理階段,用戶線程還在運行, 伴隨著程序的運行自然也會產生新的垃圾,這一部分垃圾產生在標記過程之后,CMS無法再當次過程中處理,所以只有等到下次gc時候在清理掉,這一部分垃圾就稱作“浮動垃圾”。

          5.7,G1 收集器

          G1 (Garbage-First) 是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足 GC 停頓時間要求的同時,還具備高吞吐量性能特征。
          被視為 JDK1.7 中 HotSpot 虛擬機的一個重要進化特征。它具備一下特點:
          • 并行與并發(fā):G1 能充分利用 CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓時間。部分其他收集器原本需要停頓 Java 線程執(zhí)行的 GC 動作,G1 收集器仍然可以通過并發(fā)的方式讓 java 程序繼續(xù)執(zhí)行。
          • 分代收集:雖然 G1 可以不需要其他收集器配合就能獨立管理整個 GC 堆,但是還是保留了分代的概念。
          • 空間整合:與 CMS 的“標記--清理”算法不同,G1 從整體來看是基于“標記整理”算法實現(xiàn)的收集器;從局部上來看是基于“復制”算法實現(xiàn)的。
          • 可預測的停頓:這是 G1 相對于 CMS 的另一個大優(yōu)勢,降低停頓時間是 G1 和 CMS 共同的關注點,但 G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為 M 毫秒的時間片段內。
          G1收集器運行示意圖
          G1 收集器在后臺維護了一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先選擇回收價值最大的 Region(這也就是它的名字 Garbage-First 的由來)。這種使用 Region 劃分內存空間以及有優(yōu)先級的區(qū)域回收方式,保證了 G1 收集器在有限時間內可以盡可能高的收集效率(把內存化整為零)。

          微信搜索公眾號《程序員的時光》 好了,今天就先分享到這里了,下期繼續(xù)給大家?guī)鞪VM面試內容!更多干貨、優(yōu)質文章,歡迎關注我的原創(chuàng)技術公眾號~

          參考文獻:
          深入理解Java虛擬機(第2版).周志明
          https://xiaozhuanlan.com/topic/1847690325#section1java

          瀏覽 49
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本中文不卡在线 | 乱伦福利片 | 日本不卡免费 | 久久大香蕉伊人一区二区 | 夜夜狠狠躁日日躁 |