日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

看完这篇文章,我再也不怕面试官问「垃圾回收」了...

發(fā)布時間:2025/3/11 编程问答 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 看完这篇文章,我再也不怕面试官问「垃圾回收」了... 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

前言

Java 相比 C/C++ 最顯著的特點便是引入了自動垃圾回收 (下文統(tǒng)一用 GC 指代自動垃圾回收),它解決了 C/C++ 最令人頭疼的內(nèi)存管理問題,讓程序員專注于程序本身,不用關(guān)心內(nèi)存回收這些惱人的問題,這也是 Java 能大行其道的重要原因之一,GC 真正讓程序員的生產(chǎn)力得到了釋放,但是程序員很難感知到它的存在,這就好比,我們吃完飯后在桌上放下餐盤即走,服務(wù)員會替你收拾好這些餐盤,你不會關(guān)心服務(wù)員什么時候來收,怎么收。

有人說既然 GC 已經(jīng)自動我們完成了清理,不了解 GC 貌似也沒啥問題。在大多數(shù)情況下確實沒問題,不過如果涉及到一些性能調(diào)優(yōu),問題排查等,深入地了解 GC 還是必不可少的,曾經(jīng)美團通過調(diào)整 JVM 相關(guān) GC 參數(shù)讓服務(wù)響應(yīng)時間 TP90,TP99都下降了10ms+,服務(wù)可用性得到了很大的提升!所以深入了解 GC 是成為一名優(yōu)秀 Java 程序員的必修課!

垃圾回收分上下篇,上篇會先講垃圾回收理論,主要包括

  • GC 的幾種主要的收集方法:標(biāo)記清除、標(biāo)記整理、復(fù)制算法的原理與特點,各自的優(yōu)劣勢

  • 為啥會有 Serial ,CMS, G1 等各式樣的回收器,各自的優(yōu)劣勢是什么,為啥沒有一個統(tǒng)一的萬能的垃圾回收器

  • 新生代為啥要設(shè)置成 Eden, S0,S1 這三個區(qū),基于什么考慮呢

  • 堆外內(nèi)存不受 GC 控制,那該怎么釋放呢

  • 對象可回收,就一定會被回收嗎?

  • 什么是 SafePoint,什么是 Stop The World

  • 下篇主要講垃圾回收的實踐,主要包括

  • GC 日志格式怎么看

  • 主要有哪些發(fā)生?OOM 的場景

  • 發(fā)生 OOM,如何定位,常用的內(nèi)存調(diào)試工具有哪些

  • 本文會從以下幾方面來闡述垃圾回收

  • JVM 內(nèi)存區(qū)域

  • 如何識別垃圾

    • 引用計數(shù)法

    • 可達性算法

    • 垃圾回收主要方法

      • 標(biāo)記清除法

      • 復(fù)制法

      • 標(biāo)記整理法

      • 分代收集算法

    • 垃圾回收器對比

    • 文字比較多,不過也為了便于讀者理解加了不少 GC 的動畫,相信看完會有不少收獲

      JVM 內(nèi)存區(qū)域

      要搞懂垃圾回收的機制,我們首先要知道垃圾回收主要回收的是哪些數(shù)據(jù),這些數(shù)據(jù)主要在哪一塊區(qū)域,所以我們一起來看下 JVM 的內(nèi)存區(qū)域

      • 虛擬機棧:描述的是方法執(zhí)行時的內(nèi)存模型,是線程私有的,生命周期與線程相同,每個方法被執(zhí)行的同時會創(chuàng)建棧楨(下文會看到),主要保存執(zhí)行方法時的局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息,方法執(zhí)行時入棧,方法執(zhí)行完出棧,出棧就相當(dāng)于清空了數(shù)據(jù),入棧出棧的時機很明確,所以這塊區(qū)域不需要進行 GC

      • 本地方法棧:與虛擬機棧功能非常類似,主要區(qū)別在于虛擬機棧為虛擬機執(zhí)行 Java 方法時服務(wù),而本地方法棧為虛擬機執(zhí)行本地方法時服務(wù)的。這塊區(qū)域也不需要進行 GC

      • 程序計數(shù)器:線程獨有的, 可以把它看作是當(dāng)前線程執(zhí)行的字節(jié)碼的行號指示器,比如如下字節(jié)碼內(nèi)容,在每個字節(jié)碼`前面都有一個數(shù)字(行號),我們可以認(rèn)為它就是程序計數(shù)器存儲的內(nèi)容記錄這些數(shù)字(指令地址)有啥用呢,我們知道 Java 虛擬機的多線程是通過線程輪流切換并分配處理器的時間來完成的,在任何一個時刻,一個處理器只會執(zhí)行一個線程,如果這個線程被分配的時間片執(zhí)行完了(線程被掛起),處理器會切換到另外一個線程執(zhí)行,當(dāng)下次輪到執(zhí)行被掛起的線程(喚醒線程)時,怎么知道上次執(zhí)行到哪了呢,通過記錄在程序計數(shù)器中的行號指示器即可知道,所以程序計數(shù)器的主要作用是記錄線程運行時的狀態(tài),方便線程被喚醒時能從上一次被掛起時的狀態(tài)繼續(xù)執(zhí)行,需要注意的是,程序計數(shù)器是唯一一個在 Java 虛擬機規(guī)范中沒有規(guī)定任何 OOM 情況的區(qū)域,所以這塊區(qū)域也不需要進行 GC

      • 本地內(nèi)存:線程共享區(qū)域,Java 8 中,本地內(nèi)存,也是我們通常說的堆外內(nèi)存,包含元空間和直接內(nèi)存,注意到上圖中 Java 8 和 Java 8 之前的 JVM 內(nèi)存區(qū)域的區(qū)別了嗎,在 Java 8 之前有個永久代的概念,實際上指的是 HotSpot 虛擬機上的永久代,它用永久代實現(xiàn)了 JVM 規(guī)范定義的方法區(qū)功能,主要存儲類的信息,常量,靜態(tài)變量,即時編譯器編譯后代碼等,這部分由于是在堆中實現(xiàn)的,受 GC 的管理,不過由于永久代有 -XX:MaxPermSize 的上限,所以如果動態(tài)生成類(將類信息放入永久代)或大量地執(zhí)行 String.intern (將字段串放入永久代中的常量區(qū)),很容易造成 OOM,有人說可以把永久代設(shè)置得足夠大,但很難確定一個合適的大小,受類數(shù)量,常量數(shù)量的多少影響很大。所以在 Java 8 中就把方法區(qū)的實現(xiàn)移到了本地內(nèi)存中的元空間中,這樣方法區(qū)就不受 JVM 的控制了,也就不會進行 GC,也因此提升了性能(發(fā)生 GC 會發(fā)生 Stop The Word,造成性能受到一定影響,后文會提到),也就不存在由于永久代限制大小而導(dǎo)致的 OOM 異常了(假設(shè)總內(nèi)存1G,JVM 被分配內(nèi)存 100M, 理論上元空間可以分配 2G-100M = 1.9G,空間大小足夠),也方便在元空間中統(tǒng)一管理。綜上所述,在 Java 8 以后這一區(qū)域也不需要進行 GC

      ????????畫外音: 思考一個問題,堆外內(nèi)存不受 GC控制,無法通過 GC 釋放內(nèi)存,那該以什么樣的形式釋放呢,總不能只創(chuàng)建不釋放吧,這樣的話內(nèi)存可能很快就滿了,這里不做詳細(xì)闡述,請看文末的參考文章

      • 堆:前面幾塊數(shù)據(jù)區(qū)域都不進行 GC,那只剩下堆了,是的,這里是 GC 發(fā)生的區(qū)域!對象實例和數(shù)組都是在堆上分配的,GC 也主要對這兩類數(shù)據(jù)進行回收,這塊也是我們之后重點需要分析的區(qū)域

      如何識別垃圾

      上一節(jié)我們詳細(xì)講述了 JVM 的內(nèi)存區(qū)域,知道了 GC 主要發(fā)生在堆,那么 GC 該怎么判斷堆中的對象實例或數(shù)據(jù)是不是垃圾呢,或者說判斷某些數(shù)據(jù)是否是垃圾的方法有哪些。

      引用計數(shù)法

      最容易想到的一種方式是引用計數(shù)法,啥叫引用計數(shù)法,簡單地說,就是對象被引用一次,在它的對象頭上加一次引用次數(shù),如果沒有被引用(引用次數(shù)為 0),則此對象可回收

      String ref = new String("Java");

      以上代碼 ref1 引用了右側(cè)定義的對象,所以引用次數(shù)是 1

      如果在上述代碼后面添加一個 ref = null,則由于對象沒被引用,引用次數(shù)置為 0,由于不被任何變量引用,此時即被回收,動圖如下

      看起來用引用計數(shù)確實沒啥問題了,不過它無法解決一個主要的問題:循環(huán)引用!啥叫循環(huán)引用

      public class TestRC {TestRC instance;public TestRC(String name) {}public static void main(String[] args) {// 第一步A a = new TestRC("a");B b = new TestRC("b");// 第二步a.instance = b;b.instance = a;// 第三步a = null;b = null;} }

      按步驟一步步畫圖

      到了第三步,雖然 a,b 都被置為 null 了,但是由于之前它們指向的對象互相指向了對方(引用計數(shù)都為 1),所以無法回收,也正是由于無法解決循環(huán)引用的問題,所以現(xiàn)代虛擬機都不用引用計數(shù)法來判斷對象是否應(yīng)該被回收。

      可達性算法

      現(xiàn)代虛擬機基本都是采用這種算法來判斷對象是否存活,可達性算法的原理是以一系列叫做 ?GC Root ?的對象為起點出發(fā),引出它們指向的下一個節(jié)點,再以下個節(jié)點為起點,引出此節(jié)點指向的下一個結(jié)點。。。(這樣通過 GC Root 串成的一條線就叫引用鏈),直到所有的結(jié)點都遍歷完畢,如果相關(guān)對象不在任意一個以 GC Root 為起點的引用鏈中,則這些對象會被判斷為「垃圾」,會被 GC 回收。

      如圖示,如果用可達性算法即可解決上述循環(huán)引用的問題,因為從GC Root 出發(fā)沒有到達 a,b,所以 a,b 可回收

      a, b 對象可回收,就一定會被回收嗎?并不是,對象的 finalize 方法給了對象一次垂死掙扎的機會,當(dāng)對象不可達(可回收)時,當(dāng)發(fā)生GC時,會先判斷對象是否執(zhí)行了 finalize 方法,如果未執(zhí)行,則會先執(zhí)行 finalize 方法,我們可以在此方法里將當(dāng)前對象與 GC Roots 關(guān)聯(lián),這樣執(zhí)行 finalize 方法之后,GC 會再次判斷對象是否可達,如果不可達,則會被回收,如果可達,則不回收!

      注意: finalize 方法只會被執(zhí)行一次,如果第一次執(zhí)行 finalize 方法此對象變成了可達確實不會回收,但如果對象再次被 GC,則會忽略 finalize 方法,對象會被回收!這一點切記!

      那么這些 GC Roots 到底是什么東西呢,哪些對象可以作為 GC Root 呢,有以下幾類

      • 虛擬機棧(棧幀中的本地變量表)中引用的對象

      • 方法區(qū)中類靜態(tài)屬性引用的對象

      • 方法區(qū)中常量引用的對象

      • 本地方法棧中 JNI(即一般說的 Native 方法)引用的對象

      虛擬機棧中引用的對象

      如下代碼所示,a 是棧幀中的本地變量,當(dāng) a = null 時,由于此時 a 充當(dāng)了 GC Root 的作用,a 與原來指向的實例 new Test() 斷開了連接,所以對象會被回收。

      public class Test {public static void main(String[] args) {Test a = new Test();a = null;} }

      方法區(qū)中類靜態(tài)屬性引用的對象

      如下代碼所示,當(dāng)棧幀中的本地變量 a = null 時,由于 a 原來指向的對象與 GC Root (變量 a) 斷開了連接,所以 a 原來指向的對象會被回收,而由于我們給 s 賦值了變量的引用,s 在此時是類靜態(tài)屬性引用,充當(dāng)了 GC Root 的作用,它指向的對象依然存活!

      public class Test {public static Test s;public static void main(String[] args) {Test a = new Test();a.s = new Test();a = null;} }

      方法區(qū)中常量引用的對象

      如下代碼所示,常量 s 指向的對象并不會因為 a 指向的對象被回收而回收

      public class Test {public static final Test s = new Test();public static void main(String[] args) {Test a = new Test();a = null;} }

      本地方法棧中 JNI 引用的對象

      這是簡單給不清楚本地方法為何物的童鞋簡單解釋一下:所謂本地方法就是一個 java 調(diào)用非 java 代碼的接口,該方法并非 Java 實現(xiàn)的,可能由 C 或 Python等其他語言實現(xiàn)的, Java 通過 JNI 來調(diào)用本地方法, 而本地方法是以庫文件的形式存放的(在 WINDOWS 平臺上是 DLL 文件形式,在 UNIX 機器上是 SO 文件形式)。通過調(diào)用本地的庫文件的內(nèi)部方法,使 JAVA 可以實現(xiàn)和本地機器的緊密聯(lián)系,調(diào)用系統(tǒng)級的各接口方法,還是不明白?見文末參考,對本地方法定義與使用有詳細(xì)介紹。

      當(dāng)調(diào)用 Java 方法時,虛擬機會創(chuàng)建一個棧楨并壓入 Java 棧,而當(dāng)它調(diào)用的是本地方法時,虛擬機會保持 Java 棧不變,不會在 Java 棧禎中壓入新的禎,虛擬機只是簡單地動態(tài)連接并直接調(diào)用指定的本地方法。

      JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) { ...// 緩存String的classjclass jc = (*env)->FindClass(env, STRING_PATH); }

      如上代碼所示,當(dāng) java 調(diào)用以上本地方法時,jc 會被本地方法棧壓入棧中, jc 就是我們說的本地方法棧中 JNI 的對象引用,因此只會在此本地方法執(zhí)行完成后才會被釋放。

      垃圾回收主要方法

      上一節(jié)我們知道了可以通過可達性算法來識別哪些數(shù)據(jù)是垃圾,那該怎么對這些垃圾進行回收呢。主要有以下幾種方式方式

      • 標(biāo)記清除算法

      • 復(fù)制算法

      • 標(biāo)記整理法

      標(biāo)記清除算法

      步驟很簡單

    • 先根據(jù)可達性算法標(biāo)記出相應(yīng)的可回收對象(圖中黃色部分)

    • 對可回收的對象進行回收操作起來確實很簡單,也不用做移動數(shù)據(jù)的操作,那有啥問題呢?仔細(xì)看上圖,沒錯,內(nèi)存碎片!假如我們想在上圖中的堆中分配一塊需要連續(xù)內(nèi)存占用 4M 或 5M 的區(qū)域,顯然是會失敗,怎么解決呢,如果能把上面未使用的 2M, 2M,1M 內(nèi)存能連起來就能連成一片可用空間為 5M 的區(qū)域即可,怎么做呢?

    • 復(fù)制算法

      把堆等分成兩塊區(qū)域, A 和 B,區(qū)域 A 負(fù)責(zé)分配對象,區(qū)域 B 不分配, 對區(qū)域 A 使用以上所說的標(biāo)記法把存活的對象標(biāo)記出來(下圖有誤無需清除),然后把區(qū)域 A 中存活的對象都復(fù)制到區(qū)域 B(存活對象都依次緊鄰排列)最后把 A 區(qū)對象全部清理掉釋放出空間,這樣就解決了內(nèi)存碎片的問題了。

      不過復(fù)制算法的缺點很明顯,比如給堆分配了 500M 內(nèi)存,結(jié)果只有 250M 可用,空間平白無故減少了一半!這肯定是不能接受的!另外每次回收也要把存活對象移動到另一半,效率低下(我們可以想想刪除數(shù)組元素再把非刪除的元素往一端移,效率顯然堪憂)

      標(biāo)記整理法

      前面兩步和標(biāo)記清除法一樣,不同的是它在標(biāo)記清除法的基礎(chǔ)上添加了一個整理的過程 ,即將所有的存活對象都往一端移動,緊鄰排列(如圖示),再清理掉另一端的所有區(qū)域,這樣的話就解決了內(nèi)存碎片的問題。

      但是缺點也很明顯:每進一次垃圾清除都要頻繁地移動存活的對象,效率十分低下。

      分代收集算法

      分代收集算法整合了以上算法,綜合了這些算法的優(yōu)點,最大程度避免了它們的缺點,所以是現(xiàn)代虛擬機采用的首選算法,與其說它是算法,倒不是說它是一種策略,因為它是把上述幾種算法整合在了一起,為啥需要分代收集呢,來看一下對象的分配有啥規(guī)律如圖示:縱軸代表已分配的字節(jié),而橫軸代表程序運行時間

      由圖可知,大部分的對象都很短命,都在很短的時間內(nèi)都被回收了(IBM 專業(yè)研究表明,一般來說,98% 的對象都是朝生夕死的,經(jīng)過一次 Minor GC 后就會被回收),所以分代收集算法根據(jù)對象存活周期的不同將堆分成新生代和老生代(Java8以前還有個永久代),默認(rèn)比例為 1 : 2,新生代又分為 Eden 區(qū), from Survivor 區(qū)(簡稱S0),to Survivor 區(qū)(簡稱 S1),三者的比例為 8: 1 : 1,這樣就可以根據(jù)新老生代的特點選擇最合適的垃圾回收算法,我們把新生代發(fā)生的 GC 稱為 Young GC(也叫 Minor GC),老年代發(fā)生的 GC 稱為 Old GC(也稱為 Full GC)。

      畫外音:思考一下,新生代為啥要分這么多區(qū)?

      那么分代垃圾收集是怎么工作的呢,我們一起來看看

      分代收集工作原理

      1、對象在新生代的分配與回收

      由以上的分析可知,大部分對象在很短的時間內(nèi)都會被回收,對象一般分配在 Eden 區(qū)

      當(dāng) Eden 區(qū)將滿時,觸發(fā) Minor GC

      我們之前怎么說來著,大部分對象在短時間內(nèi)都會被回收, 所以經(jīng)過 Minor GC 后只有少部分對象會存活,它們會被移到 S0 區(qū)(這就是為啥空間大小 ?Eden: S0: S1 = 8:1:1, Eden 區(qū)遠大于 S0,S1 的原因,因為在 Eden 區(qū)觸發(fā)的 Minor GC 把大部對象(接近98%)都回收了,只留下少量存活的對象,此時把它們移到 S0 或 S1 綽綽有余)同時對象年齡加一(對象的年齡即發(fā)生 Minor GC 的次數(shù)),最后把 Eden 區(qū)對象全部清理以釋放出空間,動圖如下

      當(dāng)觸發(fā)下一次 Minor GC 時,會把 Eden 區(qū)的存活對象和 S0(或S1) 中的存活對象(S0 或 S1 中的存活對象經(jīng)過每次 Minor GC 都可能被回收)一起移到 S1(Eden 和 S0 的存活對象年齡+1), 同時清空 Eden 和 S0 的空間。

      若再觸發(fā)下一次 Minor GC,則重復(fù)上一步,只不過此時變成了 從 Eden,S1 區(qū)將存活對象復(fù)制到 S0 區(qū),每次垃圾回收, S0, S1 角色互換,都是從 Eden ,S0(或S1) 將存活對象移動到 S1(或S0)。也就是說在 Eden 區(qū)的垃圾回收我們采用的是復(fù)制算法,因為在 Eden 區(qū)分配的對象大部分在 Minor GC 后都消亡了,只剩下極少部分存活對象(這也是為啥 Eden:S0:S1 默認(rèn)為 8:1:1 的原因),S0,S1 區(qū)域也比較小,所以最大限度地降低了復(fù)制算法造成的對象頻繁拷貝帶來的開銷。

      2、對象何時晉升老年代

      • 當(dāng)對象的年齡達到了我們設(shè)定的閾值,則會從S0(或S1)晉升到老年代如圖示:年齡閾值設(shè)置為 15, 當(dāng)發(fā)生下一次 Minor GC 時,S0 中有個對象年齡達到 15,達到我們的設(shè)定閾值,晉升到老年代!

      • 大對象 當(dāng)某個對象分配需要大量的連續(xù)內(nèi)存時,此時對象的創(chuàng)建不會分配在 Eden 區(qū),會直接分配在老年代,因為如果把大對象分配在 Eden 區(qū), Minor GC 后再移動到 S0,S1 會有很大的開銷(對象比較大,復(fù)制會比較慢,也占空間),也很快會占滿 S0,S1 區(qū),所以干脆就直接移到老年代.

      • 還有一種情況也會讓對象晉升到老年代,即在 S0(或S1) 區(qū)相同年齡的對象大小之和大于 S0(或S1)空間一半以上時,則年齡大于等于該年齡的對象也會晉升到老年代。

      3、空間分配擔(dān)保

      在發(fā)生 MinorGC 之前,虛擬機會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象的總空間,如果大于,那么Minor GC 可以確保是安全的,如果不大于,那么虛擬機會查看 HandlePromotionFailure 設(shè)置值是否允許擔(dān)保失敗。如果允許,那么會繼續(xù)檢查老年代最大可用連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于則進行 Minor GC,否則可能進行一次 Full GC。

      4、Stop The World

      如果老年代滿了,會觸發(fā) Full GC, Full GC 會同時回收新生代和老年代(即對整個堆進行GC),它會導(dǎo)致 Stop The World(簡稱 STW),造成挺大的性能開銷。

      什么是 STW ?所謂的 STW, 即在 GC(minor GC 或 Full GC)期間,只有垃圾回收器線程在工作,其他工作線程則被掛起。

      畫外音:為啥在垃圾收集期間其他工作線程會被掛起?想象一下,你一邊在收垃圾,另外一群人一邊丟垃圾,垃圾能收拾干凈嗎。

      一般 Full GC 會導(dǎo)致工作線程停頓時間過長(因為Full GC 會清理整個堆中的不可用對象,一般要花較長的時間),如果在此 server 收到了很多請求,則會被拒絕服務(wù)!所以我們要盡量減少 Full GC(Minor GC 也會造成 STW,但只會觸發(fā)輕微的 STW,因為 Eden 區(qū)的對象大部分都被回收了,只有極少數(shù)存活對象會通過復(fù)制算法轉(zhuǎn)移到 S0 或 S1 區(qū),所以相對還好)。

      現(xiàn)在我們應(yīng)該明白把新生代設(shè)置成 Eden, S0,S1區(qū)或者給對象設(shè)置年齡閾值或者默認(rèn)把新生代與老年代的空間大小設(shè)置成 1:2 都是為了盡可能地避免對象過早地進入老年代,盡可能晚地觸發(fā) Full GC。想想新生代如果只設(shè)置 Eden 會發(fā)生什么,后果就是每經(jīng)過一次 Minor GC,存活對象會過早地進入老年代,那么老年代很快就會裝滿,很快會觸發(fā) Full GC,而對象其實在經(jīng)過兩三次的 Minor GC 后大部分都會消亡,所以有了 S0,S1的緩沖,只有少數(shù)的對象會進入老年代,老年代大小也就不會這么快地增長,也就避免了過早地觸發(fā) Full GC。

      由于 Full GC(或Minor GC) 會影響性能,所以我們要在一個合適的時間點發(fā)起 GC,這個時間點被稱為 Safe Point,這個時間點的選定既不能太少以讓 GC 時間太長導(dǎo)致程序過長時間卡頓,也不能過于頻繁以至于過分增大運行時的負(fù)荷。一般當(dāng)線程在這個時間點上狀態(tài)是可以確定的,如確定 GC Root 的信息等,可以使 JVM 開始安全地 GC。Safe Point 主要指的是以下特定位置:

      • 循環(huán)的末尾

      • 方法返回前

      • 調(diào)用方法的 call 之后

      • 拋出異常的位置 另外需要注意的是由于新生代的特點(大部分對象經(jīng)過 Minor GC后會消亡), Minor GC 用的是復(fù)制算法,而在老生代由于對象比較多,占用的空間較大,使用復(fù)制算法會有較大開銷(復(fù)制算法在對象存活率較高時要進行多次復(fù)制操作,同時浪費一半空間)所以根據(jù)老生代特點,在老年代進行的 GC 一般采用的是標(biāo)記整理法來進行回收。

      垃圾收集器種類

      如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn)。Java 虛擬機規(guī)范并沒有規(guī)定垃圾收集器應(yīng)該如何實現(xiàn),因此一般來說不同廠商,不同版本的虛擬機提供的垃圾收集器實現(xiàn)可能會有差別,一般會給出參數(shù)來讓用戶根據(jù)應(yīng)用的特點來組合各個年代使用的收集器,主要有以下垃圾收集器

      • 在新生代工作的垃圾回收器:Serial, ParNew, ParallelScavenge

      • 在老年代工作的垃圾回收器:CMS,Serial Old, Parallel Old

      • 同時在新老生代工作的垃圾回收器:G1

      圖片中的垃圾收集器如果存在連線,則代表它們之間可以配合使用,接下來我們來看看各個垃圾收集器的具體功能。

      新生代收集器

      Serial 收集器

      Serial 收集器是工作在新生代的,單線程的垃圾收集器,單線程意味著它只會使用一個 CPU 或一個收集線程來完成垃圾回收,不僅如此,還記得我們上文提到的 STW 了嗎,它在進行垃圾收集時,其他用戶線程會暫停,直到垃圾收集結(jié)束,也就是說在 GC 期間,此時的應(yīng)用不可用。

      看起來單線程垃圾收集器不太實用,不過我們需要知道的任何技術(shù)的使用都不能脫離場景,在 Client 模式下,它簡單有效(與其他收集器的單線程比),對于限定單個 CPU 的環(huán)境來說,Serial 單線程模式無需與其他線程交互,減少了開銷,專心做 GC 能將其單線程的優(yōu)勢發(fā)揮到極致,另外在用戶的桌面應(yīng)用場景,分配給虛擬機的內(nèi)存一般不會很大,收集幾十甚至一兩百兆(僅是新生代的內(nèi)存,桌面應(yīng)用基本不會再大了),STW 時間可以控制在一百多毫秒內(nèi),只要不是頻繁發(fā)生,這點停頓是可以接受的,所以對于運行在 Client 模式下的虛擬機,Serial 收集器是新生代的默認(rèn)收集器

      ParNew 收集器

      ParNew 收集器是 Serial 收集器的多線程版本,除了使用多線程,其他像收集算法,STW,對象分配規(guī)則,回收策略與 Serial 收集器完成一樣,在底層上,這兩種收集器也共用了相當(dāng)多的代碼,它的垃圾收集過程如下

      ParNew 主要工作在 Server 模式,我們知道服務(wù)端如果接收的請求多了,響應(yīng)時間就很重要了,多線程可以讓垃圾回收得更快,也就是減少了 STW 時間,能提升響應(yīng)時間,所以是許多運行在 Server 模式下的虛擬機的首選新生代收集器,另一個與性能無關(guān)的原因是因為除了 Serial ?收集器,只有它能與 CMS 收集器配合工作,CMS 是一個劃時代的垃圾收集器,是真正意義上的并發(fā)收集器,它第一次實現(xiàn)了垃圾收集線程與用戶線程(基本上)同時工作,它采用的是傳統(tǒng)的 GC 收集器代碼框架,與 Serial,ParNew 共用一套代碼框架,所以能與這兩者一起配合工作,而后文提到的 Parallel Scavenge 與 G1 收集器沒有使用傳統(tǒng)的 GC 收集器代碼框架,而是另起爐灶獨立實現(xiàn)的,另外一些收集器則只是共用了部分的框架代碼,所以無法與 CMS 收集器一起配合工作。

      在多 CPU 的情況下,由于 ParNew 的多線程回收特性,毫無疑問垃圾收集會更快,也能有效地減少 STW 的時間,提升應(yīng)用的響應(yīng)速度。

      Parallel Scavenge 收集器

      Parallel Scavenge 收集器也是一個使用復(fù)制算法多線程,工作于新生代的垃圾收集器,看起來功能和 ParNew 收集器一樣,它有啥特別之處嗎

      關(guān)注點不同,CMS 等垃圾收集器關(guān)注的是盡可能縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 目標(biāo)是達到一個可控制的吞吐量(吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間+垃圾收集時間)),也就是說 CMS 等垃圾收集器更適合用到與用戶交互的程序,因為停頓時間越短,用戶體驗越好,而 Parallel Scavenge 收集器關(guān)注的是吞吐量,所以更適合做后臺運算等不需要太多用戶交互的任務(wù)。

      Parallel Scavenge 收集器提供了兩個參數(shù)來精確控制吞吐量,分別是控制最大垃圾收集時間的 -XX:MaxGCPauseMillis 參數(shù)及直接設(shè)置吞吐量大小的 -XX:GCTimeRatio(默認(rèn)99%)

      除了以上兩個參數(shù),還可以用 Parallel Scavenge 收集器提供的第三個參數(shù) -XX:UseAdaptiveSizePolicy,開啟這個參數(shù)后,就不需要手工指定新生代大小,Eden 與 Survivor 比例(SurvivorRatio)等細(xì)節(jié),只需要設(shè)置好基本的堆大小(-Xmx 設(shè)置最大堆),以及最大垃圾收集時間與吞吐量大小,虛擬機就會根據(jù)當(dāng)前系統(tǒng)運行情況收集監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以盡可能地達到我們設(shè)定的最大垃圾收集時間或吞吐量大小這兩個指標(biāo)。自適應(yīng)策略也是 Parallel Scavenge ?與 ParNew 的重要區(qū)別!

      老年代收集器

      Serial Old 收集器

      上文我們知道, Serial 收集器是工作于新生代的單線程收集器,與之相對地,Serial Old 是工作于老年代的單線程收集器,此收集器的主要意義在于給 Client 模式下的虛擬機使用,如果在 Server 模式下,則它還有兩大用途:一種是在 JDK 1.5 及之前的版本中與 Parallel Scavenge 配合使用,另一種是作為 CMS 收集器的后備預(yù)案,在并發(fā)收集發(fā)生 Concurrent Mode Failure 時使用(后文講述),它與 Serial 收集器配合使用示意圖如下

      Parallel Old 收集器

      Parallel Old 是相對于 Parallel Scavenge 收集器的老年代版本,使用多線程和標(biāo)記整理法,兩者組合示意圖如下,這兩者的組合由于都是多線程收集器,真正實現(xiàn)了「吞吐量優(yōu)先」的目標(biāo)

      CMS 收集器

      CMS 收集器是以實現(xiàn)最短 STW 時間為目標(biāo)的收集器,如果應(yīng)用很重視服務(wù)的響應(yīng)速度,希望給用戶最好的體驗,則 CMS 收集器是個很不錯的選擇!

      我們之前說老年代主要用標(biāo)記整理法,而 CMS 雖然工作于老年代,但采用的是標(biāo)記清除法,主要有以下四個步驟

    • 初始標(biāo)記

    • 并發(fā)標(biāo)記

    • 重新標(biāo)記

    • 并發(fā)清除

    • 從圖中可以的看到初始標(biāo)記和重新標(biāo)記兩個階段會發(fā)生 STW,造成用戶線程掛起,不過初始標(biāo)記僅標(biāo)記 GC Roots 能關(guān)聯(lián)的對象,速度很快,并發(fā)標(biāo)記是進行 GC Roots ?Tracing 的過程,重新標(biāo)記是為了修正并發(fā)標(biāo)記期間因用戶線程繼續(xù)運行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,這一階段停頓時間一般比初始標(biāo)記階段稍長,但遠比并發(fā)標(biāo)記時間短

      整個過程中耗時最長的是并發(fā)標(biāo)記和標(biāo)記清理,不過這兩個階段用戶線程都可工作,所以不影響應(yīng)用的正常使用,所以總體上看,可以認(rèn)為 CMS 收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。

      但是 CMS 收集器遠達不到完美的程度,主要有以下三個缺點

      • CMS 收集器對 CPU 資源非常敏感 ?原因也可以理解,比如本來我本來可以有 10 個用戶線程處理請求,現(xiàn)在卻要分出 3 個作為回收線程,吞吐量下降了30%,CMS 默認(rèn)啟動的回收線程數(shù)是 (CPU數(shù)量+3)/ 4, 如果 CPU 數(shù)量只有一兩個,那吞吐量就直接下降 50%,顯然是不可接受的

      • CMS 無法處理浮動垃圾(Floating Garbage),可能出現(xiàn) 「Concurrent Mode Failure」而導(dǎo)致另一次 Full GC 的產(chǎn)生,由于在并發(fā)清理階段用戶線程還在運行,所以清理的同時新的垃圾也在不斷出現(xiàn),這部分垃圾只能在下一次 GC 時再清理掉(即浮云垃圾),同時在垃圾收集階段用戶線程也要繼續(xù)運行,就需要預(yù)留足夠多的空間要確保用戶線程正常執(zhí)行,這就意味著 CMS 收集器不能像其他收集器一樣等老年代滿了再使用,JDK 1.5 默認(rèn)當(dāng)老年代使用了68%空間后就會被激活,當(dāng)然這個比例可以通過 -XX:CMSInitiatingOccupancyFraction 來設(shè)置,但是如果設(shè)置地太高很容易導(dǎo)致在 CMS 運行期間預(yù)留的內(nèi)存無法滿足程序要求,會導(dǎo)致 Concurrent Mode Failure 失敗,這時會啟用 Serial Old 收集器來重新進行老年代的收集,而我們知道 Serial Old 收集器是單線程收集器,這樣就會導(dǎo)致 STW 更長了。

      • CMS 采用的是標(biāo)記清除法,上文我們已經(jīng)提到這種方法會產(chǎn)生大量的內(nèi)存碎片,這樣會給大內(nèi)存分配帶來很大的麻煩,如果無法找到足夠大的連續(xù)空間來分配對象,將會觸發(fā) Full GC,這會影響應(yīng)用的性能。當(dāng)然我們可以開啟 -XX:+UseCMSCompactAtFullCollection(默認(rèn)是開啟的),用于在 CMS 收集器頂不住要進行 FullGC 時開啟內(nèi)存碎片的合并整理過程,內(nèi)存整理會導(dǎo)致 STW,停頓時間會變長,還可以用另一個參數(shù) -XX:CMSFullGCsBeforeCompation 用來設(shè)置執(zhí)行多少次不壓縮的 Full GC 后跟著帶來一次帶壓縮的。

      G1(Garbage First) 收集器

      G1 收集器是面向服務(wù)端的垃圾收集器,被稱為駕馭一切的垃圾回收器,主要有以下幾個特點

      • 像 CMS 收集器一樣,能與應(yīng)用程序線程并發(fā)執(zhí)行。

      • 整理空閑空間更快。

      • 需要 GC 停頓時間更好預(yù)測。

      • 不會像 CMS 那樣犧牲大量的吞吐性能。

      • 不需要更大的 Java Heap

      與 CMS 相比,它在以下兩個方面表現(xiàn)更出色

    • 運作期間不會產(chǎn)生內(nèi)存碎片,G1 從整體上看采用的是標(biāo)記-整理法,局部(兩個 Region)上看是基于復(fù)制算法實現(xiàn)的,兩個算法都不會產(chǎn)生內(nèi)存碎片,收集后提供規(guī)整的可用內(nèi)存,這樣有利于程序的長時間運行。

    • 在 STW 上建立了可預(yù)測的停頓時間模型,用戶可以指定期望停頓時間,G1 會將停頓時間控制在用戶設(shè)定的停頓時間以內(nèi)。

    • 為什么G1能建立可預(yù)測的停頓模型呢,主要原因在于 G1 對堆空間的分配與傳統(tǒng)的垃圾收集器不一器,傳統(tǒng)的內(nèi)存分配就像我們前文所述,是連續(xù)的,分成新生代,老年代,新生代又分 Eden,S0,S1,如下

      而 G1 各代的存儲地址不是連續(xù)的,每一代都使用了 n 個不連續(xù)的大小相同的 Region,每個Region占有一塊連續(xù)的虛擬內(nèi)存地址,如圖示

      除了和傳統(tǒng)的新老生代,幸存區(qū)的空間區(qū)別,Region還多了一個H,它代表Humongous,這表示這些Region存儲的是巨大對象(humongous object,H-obj),即大小大于等于region一半的對象,這樣超大對象就直接分配到了老年代,防止了反復(fù)拷貝移動。那么 G1 分配成這樣有啥好處呢?

      傳統(tǒng)的收集器如果發(fā)生 Full GC 是對整個堆進行全區(qū)域的垃圾收集,而分配成各個 Region 的話,方便 G1 跟蹤各個 Region 里垃圾堆積的價值大小(回收所獲得的空間大小及回收所需經(jīng)驗值),這樣根據(jù)價值大小維護一個優(yōu)先列表,根據(jù)允許的收集時間,優(yōu)先收集回收價值最大的 Region,也就避免了整個老年代的回收,也就減少了 STW 造成的停頓時間。同時由于只收集部分 Region,可就做到了 STW 時間的可控。

      G1 收集器的工作步驟如下

    • 初始標(biāo)記

    • 并發(fā)標(biāo)記

    • 最終標(biāo)記

    • 篩選回收

    • 可以看到整體過程與 CMS 收集器非常類似,篩選階段會根據(jù)各個 Region 的回收價值和成本進行排序,根據(jù)用戶期望的 GC 停頓時間來制定回收計劃。

      總結(jié)

      本文簡述了垃圾回收的原理與垃圾收集器的種類,相信大家對開頭提的一些問題應(yīng)該有了更深刻的認(rèn)識,在生產(chǎn)環(huán)境中我們要根據(jù)不同的場景來選擇垃圾收集器組合,如果是運行在桌面環(huán)境處于 Client 模式的,則用 Serial + Serial Old 收集器綽綽有余,如果需要響應(yīng)時間快,用戶體驗好的,則用 ParNew + CMS 的搭配模式,即使是號稱是「駕馭一切」的 G1,也需要根據(jù)吞吐量等要求適當(dāng)調(diào)整相應(yīng)的 JVM 參數(shù),沒有最牛的技術(shù),只有最合適的使用場景,切記!

      理論有了,下一篇我們會進入手動操作環(huán)節(jié),我們會一起來動手操作一些 demo,做一些實驗,來驗證我們看到的一些現(xiàn)象,比如對象一般分配在新生代,什么情況下會直接到老年代,該怎么實驗?發(fā)生了OOM,該用哪些工具調(diào)試呢?等等,敬請期待!

      參考

      堆外內(nèi)存的回收機制分析 https://www.jianshu.com/p/35cf0f348275?

      java調(diào)用本地方法--jni簡介 https://blog.csdn.net/w1992wishes/article/details/80283403?

      咱們從頭到尾說一次 Java 垃圾回收 https://mp.weixin.qq.com/s/pR7U1OTwsNSg5fRyWafucA?

      深入理解 Java 虛擬機?

      Java Hotspot G1 GC的一些關(guān)鍵技術(shù)?https://tech.meituan.com/2016/09/23/g1.html

      往期推薦

      千萬不要這樣寫代碼!9種常見的OOM場景演示


      騰訊推出高性能 RPC 開發(fā)框架


      Java中竟有18種隊列?45張圖!安排


      關(guān)注我,每天陪你進步一點點!

    總結(jié)

    以上是生活随笔為你收集整理的看完这篇文章,我再也不怕面试官问「垃圾回收」了...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 最新中文字幕在线观看视频 | 国产老熟女一区二区三区 | 国产日韩一级 | 国产精品天美传媒入口 | 午夜视频 | 午夜精品极品粉嫩国产尤物 | 麻豆91精品 | 国产乱人伦app精品久久 | 亚洲日本国产精品 | 亚洲少妇网站 | 亚洲精品xxxx | 小柔的淫辱日记(h | 国产自偷自拍视频 | 欧美 国产 综合 | 美国一级大黄一片免费中文 | 精品香蕉99久久久久网站 | 高清乱码免费看污 | 176精品免费 | 91黄在线观看 | 日韩欧美一区视频 | 夜色成人 | 性视频网址| 538精品在线视频 | 国产精品久久久久不卡 | 激情a| 亚洲另类天堂 | 九九热在线视频免费观看 | 日本久久高清视频 | 精品人妻一区二区三区久久夜夜嗨 | 亚洲第一大网站 | 四虎在线免费观看 | 亚州av片 | 性歌舞团一区二区三区视频 | 最新地址在线观看 | 久久女同 | 97久久人澡人人添人人爽 | 女人色极品影院 | 久久精品国产电影 | www.色图| 小毛片网站 | 娇妻被老王脔到高潮失禁视频 | 精品久久久久成人码免费动漫 | 中文字幕第一区综合 | 国产第一页精品 | 国内成人自拍视频 | 亚洲国产一区二区在线观看 | 欧美大片一区二区三区 | 日韩欧美精品一区二区三区 | 看污片网站 | 少妇又紧又色 | 人妻一区二区视频 | 午夜精品剧场 | 亚洲综合专区 | 性色视频| 噼里啪啦国语高清 | 国产免费播放 | 四虎国产精品永久在线国在线 | 99久久综合国产精品二区 | 欧美破处大片 | 欧美黄色影院 | 日韩av片在线免费观看 | 91精品国产综合久久福利软件 | 精品久久二区 | 免费在线观看毛片视频 | 天天夜碰日日摸日日澡性色av | 天天上天天干 | 成人手机视频在线观看 | 日韩精品在线网站 | 亚洲涩涩视频 | 欧美亚洲综合在线 | 天堂中文视频 | 综合久久99 | 超碰狠狠 | 久久久成人免费视频 | 午夜精品影院 | 久久中字 | 小小姑娘电影大全免费播放 | av在线免费观看不卡 | 精品美女一区二区三区 | 97超碰资源站 | 好大好爽好舒服 | 久久久黄色片 | 亚洲第五页 | 337p粉嫩大胆噜噜噜噜69影视 | 亚洲第一网站 | 91视频免费在线观看 | 国产v在线 | 久久精品电影网 | 国产精品久久网站 | 国产精品女人精品久久久天天 | 免费看麻豆 | 亚洲一区二区av在线 | 国产一区二区三区免费观看 | 国产精品久久久影院 | 牛牛在线免费视频 | 深夜精品福利 | 日韩网| 久久大胆视频 | 黄色片18 |