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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

垃圾回收器算法

發布時間:2023/12/10 编程问答 19 豆豆
生活随笔 收集整理的這篇文章主要介紹了 垃圾回收器算法 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

GC

垃圾回收概述

  • Java 和 C語言最大的區別,就在于,垃圾回收和內存動態分配上。C語言沒有垃圾回收技術,需要程序員手動回收。
  • 1960年,第一門開始使用內存動態分配和垃圾收集技術的Lisp語言誕生。
  • 關于垃圾收集有三個經典問題:
    • 哪些內存需要回收?
    • 什么時候回收?
    • 如何回收?
  • 大廠面試題

    螞蟻金服

  • 你知道哪幾種垃圾回收器,各自的優缺點,重點講一下CMS和G1?
  • JVM GC算法有哪些,目前的JDK版本采用什么回收算法?
  • G1回收器講下回收過程GC是什么?為什么要有GC?
  • GC的兩種判定方法?CMS收集器與G1收集器的特點
  • 百度

  • 說一下GC算法,分代回收說下
  • 垃圾收集策略和算法
  • 天貓

  • JVM GC原理,JVM怎么回收內存
  • CMS特點,垃圾回收算法有哪些?各自的優缺點,他們共同的缺點是什么?
  • 滴滴

  • Java的垃圾回收器都有哪些,說下G1的應用場景,平時你是如何搭配使用垃圾回收器的
  • 京東

  • 你知道哪幾種垃圾收集器,各自的優缺點,重點講下CMS和G1,
  • 包括原理,流程,優缺點。垃圾回收算法的實現原理
  • 阿里

  • 講一講垃圾回收算法。
  • 什么情況下觸發垃圾回收?
  • 如何選擇合適的垃圾收集算法?
  • JVM有哪三種垃圾回收器?
  • 字節跳動

  • 常見的垃圾回收器算法有哪些,各有什么優劣?
  • System.gc()和Runtime.gc()會做什么事情?
  • Java GC機制?GC Roots有哪些?
  • Java對象的回收方式,回收算法。
  • CMS和G1了解么,CMS解決什么問題,說一下回收的過程。
  • CMS回收停頓了幾次,為什么要停頓兩次?
  • 什么是垃圾?

  • 垃圾是指在運行程序中沒有任何指針指向的對象
  • 如果不及時回收,垃圾一直占用內存空間,可能導致內存溢出錯誤
  • 為什么需要GC?

    想要學習GC,首先需要理解為什么需要GC?

  • 對于高級語言來說,一個基本認知是如果不進行垃圾回收,內存遲早都會被消耗完,因為不斷地分配內存空間而不進行回收,就好像不停地生產生活垃圾而從來不打掃一樣。
  • 除了釋放沒用的對象,垃圾回收也可以清除內存里的記錄碎片。碎片整理將所占用的堆內存移到堆的一端,以便JVM將整理出的內存分配給新的對象。
  • 隨著應用程序所應付的業務越來越龐大、復雜,用戶越來越多,沒有GC就不能保證應用程序的正常進行。而經常造成STW的GC又跟不上實際的需求,所以才會不斷地嘗試對GC進行優化。
  • 早期垃圾回收

  • 在早期的C/C++時代,垃圾回收基本上是手工進行的。開發人員可以使用new關鍵字進行內存申請,并使用delete關鍵字進行內存釋放。比如以下代碼:
  • MibBridge *pBridge= new cmBaseGroupBridge();//如果注冊失敗,使用Delete釋放該對象所占內存區域if(pBridge->Register(kDestroy)!=NO ERROR)delete pBridge;
  • 這種方式可以靈活控制內存釋放的時間,但是會給開發人員帶來頻繁申請和釋放內存的管理負擔。倘若有一處內存區間由于程序員編碼的問題忘記被回收,那么就會產生內存泄漏,垃圾對象永遠無法被清除,隨著系統運行時間的不斷增長,垃圾對象所耗內存可能持續上升,直到出現內存溢出并造成應用程序崩潰。
  • 有了垃圾回收機制后,上述代碼極有可能變成這樣
  • MibBridge *pBridge=new cmBaseGroupBridge(); pBridge->Register(kDestroy);
  • 現在,除了Java以外,C#、Python、Ruby等語言都使用了自動垃圾回收的思想,也是未來發展趨勢,可以說這種自動化的內存分配和來及回收方式已經成為了現代開發語言必備的標準。
  • Java 垃圾回收機制

    自動內存管理

    官網介紹:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html

    自動內存管理的優點

  • 自動內存管理,無需開發人員手動參與內存的分配與回收,這樣降低內存泄漏和內存溢出的風險
  • 沒有垃圾回收器,java也會和cpp一樣,各種懸垂指針,野指針,泄露問題讓你頭疼不已。
  • 自動內存管理機制,將程序員從繁重的內存管理中釋放出來,可以更專心地專注于業務開發
  • 關于自動內存管理的擔憂

  • 對于Java開發人員而言,自動內存管理就像是一個黑匣子,如果過度依賴于“自動”,那么這將會是一場災難,最嚴重的就會弱化Java開發人員在程序出現內存溢出時定位問題和解決問題的能力。
  • 此時,了解JVM的自動內存分配和內存回收原理就顯得非常重要,只有在真正了解JVM是如何管理內存后,我們才能夠在遇見OutofMemoryError時,快速地根據錯誤異常日志定位問題和解決問題。
  • 當需要排查各種內存溢出、內存泄漏問題時,當垃圾收集成為系統達到更高并發量的瓶頸時,我們就必須對這些“自動化”的技術實施必要的監控和調節
  • 應該關心哪些區域的回收?

    ![](https://img-blog.csdnimg.cn/img_convert/09570fc1add1736d472f7705fe5fcba2.png#align=left&display=inline&height=548&margin=[object Object]&originHeight=548&originWidth=739&size=0&status=done&style=none&width=739)

  • 垃圾收集器可以對年輕代回收,也可以對老年代回收,甚至是全棧和方法區的回收,
  • 其中,Java堆是垃圾收集器的工作重點
  • 從次數上講:
  • 頻繁收集Young區
  • 較少收集Old區
  • 基本不收集Perm區(元空間)
  • 垃圾回收相關算法

    標記階段:引用計數算法

    標記階段的目的

    垃圾標記階段:主要是為了判斷對象是否存活

  • 在堆里存放著幾乎所有的Java對象實例,在GC執行垃圾回收之前,首先需要區分出內存中哪些是存活對象,哪些是已經死亡的對象。只有被標記為己經死亡的對象,GC才會在執行垃圾回收時,釋放掉其所占用的內存空間,因此這個過程我們可以稱為垃圾標記階段。
  • 那么在JVM中究竟是如何標記一個死亡對象呢?簡單來說,當一個對象已經不再被任何的存活對象繼續引用時,就可以宣判為已經死亡。
  • 判斷對象存活一般有兩種方式:引用計數算法可達性分析算法
  • 引用計數算法

  • 引用計數算法(Reference Counting)比較簡單,對每個對象保存一個整型的引用計數器屬性。用于記錄對象被引用的情況。
  • 對于一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要對象A的引用計數器的值為0,即表示對象A不可能再被使用,可進行回收。
  • 優點:實現簡單,垃圾對象便于辨識;判定效率高,回收沒有延遲性。
  • 缺點:
  • 它需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷。
  • 每次賦值都需要更新計數器,伴隨著加法和減法操作,這增加了時間開銷。
  • 引用計數器有一個嚴重的問題,即無法處理循環引用的情況。這是一條致命缺陷,導致在Java的垃圾回收器中沒有使用這類算法。
  • 循環引用

    ![](https://img-blog.csdnimg.cn/img_convert/7f2a76a4eb39e1fcbc3220445ac17b46.png#align=left&display=inline&height=611&margin=[object Object]&originHeight=611&originWidth=976&size=0&status=done&style=none&width=976)

    當p的指針斷開的時候,內部的引用形成一個循環,計數器都還算1,無法被回收,這就是循環引用,從而造成內存泄漏

    證明:java使用的不是引用計數算法

    /*** -XX:+PrintGCDetails* 證明:java使用的不是引用計數算法*/ public class RefCountGC {//這個成員屬性唯一的作用就是占用一點內存private byte[] bigSize = new byte[5 * 1024 * 1024];//5MBObject reference = null;public static void main(String[] args) {RefCountGC obj1 = new RefCountGC();RefCountGC obj2 = new RefCountGC();obj1.reference = obj2;obj2.reference = obj1;obj1 = null;obj2 = null;//顯式的執行垃圾回收行為//這里發生GC,obj1和obj2能否被回收?System.gc();} }

    ![](https://img-blog.csdnimg.cn/img_convert/35579d8a0631a646661e89cab4f55f4e.png#align=left&display=inline&height=606&margin=[object Object]&originHeight=606&originWidth=841&size=0&status=done&style=none&width=841)

    • 如果不小心直接把obj1.reference和obj2.reference置為null。則在Java堆中的兩塊內存依然保持著互相引用,無法被回收

    沒有進行GC時

    把下面的幾行代碼注釋掉,讓它來不及

    System.gc();//把這行代碼注釋掉 HeapPSYoungGen total 38400K, used 14234K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)eden space 33280K, 42% used [0x00000000d5f80000,0x00000000d6d66be8,0x00000000d8000000)from space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)to space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)ParOldGen total 87552K, used 0K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)object space 87552K, 0% used [0x0000000081e00000,0x0000000081e00000,0x0000000087380000)Metaspace used 3496K, capacity 4498K, committed 4864K, reserved 1056768Kclass space used 387K, capacity 390K, committed 512K, reserved 1048576KProcess finished with exit code 0

    進行GC

    打開那行代碼的注釋

    [GC (System.gc()) [PSYoungGen: 13569K->808K(38400K)] 13569K->816K(125952K), 0.0012717 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 808K->0K(38400K)] [ParOldGen: 8K->670K(87552K)] 816K->670K(125952K), [Metaspace: 3491K->3491K(1056768K)], 0.0051769 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] HeapPSYoungGen total 38400K, used 333K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)eden space 33280K, 1% used [0x00000000d5f80000,0x00000000d5fd34a8,0x00000000d8000000)from space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)to space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)ParOldGen total 87552K, used 670K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)object space 87552K, 0% used [0x0000000081e00000,0x0000000081ea7990,0x0000000087380000)Metaspace used 3498K, capacity 4498K, committed 4864K, reserved 1056768Kclass space used 387K, capacity 390K, committed 512K, reserved 1048576KProcess finished with exit code 0

    1、從打印日志就可以明顯看出來,已經進行了GC

    2、如果使用引用計數算法,那么這兩個對象將會無法回收。而現在兩個對象被回收了,說明Java使用的不是引用計數算法來進行標記的。

    小結

  • 引用計數算法,是很多語言的資源回收選擇,例如因人工智能而更加火熱的Python,它更是同時支持引用計數和垃圾收集機制。
  • 具體哪種最優是要看場景的,業界有大規模實踐中僅保留引用計數機制,以提高吞吐量的嘗試。
  • Java并沒有選擇引用計數,是因為其存在一個基本的難題,也就是很難處理循環引用關系。
  • Python如何解決循環引用?
    • 手動解除:很好理解,就是在合適的時機,解除引用關系。
    • 使用弱引用weakref,weakref是Python提供的標準庫,旨在解決循環引用。
  • 標記階段:可達性分析算法

    可達性分析算法:也可以稱為根搜索算法、追蹤性垃圾收集

  • 相對于引用計數算法而言,可達性分析算法不僅同樣具備實現簡單和執行高效等特點,更重要的是該算法可以有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生。
  • 相較于引用計數算法,這里的可達性分析就是Java、C#選擇的。這種類型的垃圾收集通常也叫作追蹤性垃圾收集(Tracing Garbage Collection)
  • 可達性分析實現思路

    • 所謂"GCRoots”根集合就是一組必須活躍的引用
    • 其基本思路如下:
  • 可達性分析算法是以根對象集合(GCRoots)為起始點,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達。
  • 使用可達性分析算法后,內存中的存活對象都會被根對象集合直接或間接連接著,搜索所走過的路徑稱為引用鏈(Reference Chain)
  • 如果目標對象沒有任何引用鏈相連,則是不可達的,就意味著該對象己經死亡,可以標記為垃圾對象。
  • 在可達性分析算法中,只有能夠被根對象集合直接或者間接連接的對象才是存活對象。
  • ![](https://img-blog.csdnimg.cn/img_convert/71a1c097478cde41d8949caa6944abf6.png#align=left&display=inline&height=681&margin=[object Object]&originHeight=681&originWidth=854&size=0&status=done&style=none&width=854)

    GC Roots可以是哪些元素?

  • 虛擬機棧中引用的對象
    • 比如:各個線程被調用的方法中使用到的參數、局部變量等。
  • 本地方法棧內JNI(通常說的本地方法)引用的對象
  • 方法區中類靜態屬性引用的對象
    • 比如:Java類的引用類型靜態變量
  • 方法區中常量引用的對象
    • 比如:字符串常量池(StringTable)里的引用
  • 所有被同步鎖synchronized持有的對象
  • Java虛擬機內部的引用。
    • 基本數據類型對應的Class對象,一些常駐的異常對象(如:NullPointerException、OutofMemoryError),系統類加載器。
  • 反映java虛擬機內部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等。
  • ![](https://img-blog.csdnimg.cn/img_convert/667057cccc49b38d5362f2d23bbc426a.png#align=left&display=inline&height=602&margin=[object Object]&originHeight=602&originWidth=802&size=0&status=done&style=none&width=802)

  • 總結一句話就是,除了堆空間的周邊,比如:虛擬機棧、本地方法棧、方法區、字符串常量池等地方對堆空間進行引用的,都可以作為GC Roots進行可達性分析
  • 除了這些固定的GC Roots集合以外,根據用戶所選用的垃圾收集器以及當前回收的內存區域不同,還可以有其他對象“臨時性”地加入,共同構成完整GC Roots集合。比如:分代收集和局部回收(PartialGC)。
    • 如果只針對Java堆中的某一塊區域進行垃圾回收(比如:典型的只針對新生代),必須考慮到內存區域是虛擬機自己的實現細節,更不是孤立封閉的,這個區域的對象完全有可能被其他區域的對象所引用,這時候就需要一并將關聯的區域對象也加入GC Roots集合中去考慮,才能保證可達性分析的準確性。
  • 小技巧

    由于Root采用棧方式存放變量和指針,所以如果一個指針,它保存了堆內存里面的對象,但是自己又不存放在堆內存里面,那它就是一個Root。

    注意

  • 如果要使用可達性分析算法來判斷內存是否可回收,那么分析工作必須在一個能保障一致性的快照中進行。這點不滿足的話分析結果的準確性就無法保證。
  • 這點也是導致GC進行時必須“Stop The World”的一個重要原因。即使是號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必須要停頓的。
  • 對象的 finalization 機制

    finalize() 方法機制

    對象銷毀前的回調函數:finalize()

  • Java語言提供了對象終止(finalization)機制來允許開發人員提供對象被銷毀之前的自定義處理邏輯
  • 當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象之前,總會先調用這個對象的finalize()方法。
  • finalize() 方法允許在子類中被重寫,用于在對象被回收時進行資源釋放。通常在這個方法中進行一些資源釋放和清理的工作,比如關閉文件、套接字和數據庫連接等。
  • Object 類中 finalize() 源碼

    // 等待被重寫 protected void finalize() throws Throwable { }
  • 永遠不要主動調用某個對象的finalize()方法,應該交給垃圾回收機制調用。理由包括下面三點:
  • 在finalize()時可能會導致對象復活。
  • finalize()方法的執行時間是沒有保障的,它完全由GC線程決定,極端情況下,若不發生GC,則finalize()方法將沒有執行機會。
  • 一個糟糕的finalize()會嚴重影響GC的性能。比如finalize是個死循環
  • 從功能上來說,finalize()方法與C中的析構函數比較相似,但是Java采用的是基于垃圾回收器的自動內存管理機制,所以finalize()方法在本質上不同于C中的析構函數。
  • finalize()方法對應了一個finalize線程,因為優先級比較低,即使主動調用該方法,也不會因此就直接進行回收
  • 生存還是死亡?

    由于finalize()方法的存在,虛擬機中的對象一般處于三種可能的狀態。

  • 如果從所有的根節點都無法訪問到某個對象,說明對象己經不再使用了。一般來說,此對象需要被回收。但事實上,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段。一個無法觸及的對象有可能在某一個條件下“復活”自己,如果這樣,那么對它立即進行回收就是不合理的。為此,定義虛擬機中的對象可能的三種狀態。如下:
  • 可觸及的:從根節點開始,可以到達這個對象。
  • 可復活的:對象的所有引用都被釋放,但是對象有可能在finalize()中復活。
  • 不可觸及的:對象的finalize()被調用,并且沒有復活,那么就會進入不可觸及狀態。不可觸及的對象不可能被復活,因為finalize()只會被調用一次
  • 以上3種狀態中,是由于finalize()方法的存在,進行的區分。只有在對象不可觸及時才可以被回收。
  • 具體過程

    判定一個對象objA是否可回收,至少要經歷兩次標記過程:

  • 如果對象objA到GC Roots沒有引用鏈,則進行第一次標記。
  • 進行篩選,判斷此對象是否有必要執行finalize()方法
  • 如果對象objA沒有重寫finalize()方法,或者finalize()方法已經被虛擬機調用過,則虛擬機視為“沒有必要執行”,objA被判定為不可觸及的。
  • 如果對象objA重寫了finalize()方法,且還未執行過,那么objA會被插入到F-Queue隊列中,由一個虛擬機自動創建的、低優先級的Finalizer線程觸發其finalize()方法執行。
  • finalize()方法是對象逃脫死亡的最后機會,稍后GC會對F-Queue隊列中的對象進行第二次標記。如果objA在finalize()方法中與引用鏈上的任何一個對象建立了聯系,那么在第二次標記時,objA會被移出“即將回收”集合。之后,對象會再次出現沒有引用存在的情況。在這個情況下,finalize()方法不會被再次調用,對象會直接變成不可觸及的狀態,也就是說,一個對象的finalize()方法只會被調用一次。
  • 通過 JVisual VM 查看 Finalizer 線程

    ![](https://img-blog.csdnimg.cn/img_convert/74210d540633d0cf5d6c3dd664197d82.png#align=left&display=inline&height=751&margin=[object Object]&originHeight=751&originWidth=1813&size=0&status=done&style=none&width=1813)

    代碼演示 finalize() 方法可復活對象

    我們重寫 CanReliveObj 類的 finalize()方法,在調用其 finalize()方法時,將 obj 指向當前類對象 this

    /*** 測試Object類中finalize()方法,即對象的finalization機制。**/ public class CanReliveObj {public static CanReliveObj obj;//類變量,屬于 GC Root//此方法只能被調用一次@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("調用當前類重寫的finalize()方法");obj = this;//當前待回收的對象在finalize()方法中與引用鏈上的一個對象obj建立了聯系}public static void main(String[] args) {try {obj = new CanReliveObj();// 對象第一次成功拯救自己obj = null;System.gc();//調用垃圾回收器System.out.println("第1次 gc");// 因為Finalizer線程優先級很低,暫停2秒,以等待它Thread.sleep(2000);if (obj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}System.out.println("第2次 gc");// 下面這段代碼與上面的完全相同,但是這次自救卻失敗了obj = null;System.gc();// 因為Finalizer線程優先級很低,暫停2秒,以等待它Thread.sleep(2000);if (obj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}} catch (InterruptedException e) {e.printStackTrace();}} }

    如果注釋掉finalize()方法

    //此方法只能被調用一次@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("調用當前類重寫的finalize()方法");obj = this;//當前待回收的對象在finalize()方法中與引用鏈上的一個對象obj建立了聯系}

    輸出結果:

    1次 gc obj is dead 第2次 gc obj is dead

    放開finalize()方法

    輸出結果:

    1次 gc 調用當前類重寫的finalize()方法 obj is still alive 第2次 gc obj is dead

    第一次自救成功,但由于 finalize() 方法只會執行一次,所以第二次自救失敗

    MAT與JProfiler的GC Roots溯源

    MAT 介紹

  • MAT是Memory Analyzer的簡稱,它是一款功能強大的Java堆內存分析器。用于查找內存泄漏以及查看內存消耗情況。
  • MAT是基于Eclipse開發的,是一款免費的性能分析工具。
  • 大家可以在http://www.eclipse.org/mat/下載并使用MAT
  • 1、雖然Jvisualvm很強大,但是在內存分析方面,還是MAT更好用一些
    2、此小節主要是為了實時分析GC Roots是哪些東西,中間需要用到一個dump的文件

    獲取 dump 文件方式

    方式一:命令行使用 jmap

    ![](https://img-blog.csdnimg.cn/img_convert/c75eb12521c86a6861f6071229a2681f.png#align=left&display=inline&height=412&margin=[object Object]&originHeight=412&originWidth=1025&size=0&status=done&style=none&width=1025)

    方式二:使用JVisualVM

  • 捕獲的heap dump文件是一個臨時文件,關閉JVisualVM后自動刪除,若要保留,需要將其另存為文件。可通過以下方法捕獲heap dump:
  • 操作步驟下面演示
  • 捕捉 dump 示例

    使用JVisualVM捕捉 heap dump

    代碼:

    • numList 和 birth 在第一次捕捉內存快照的時候,為 GC Roots
    • 之后 numList 和 birth 置為 null ,對應的引用對象被回收,在第二次捕捉內存快照的時候,就不再是 GC Roots
    public class GCRootsTest {public static void main(String[] args) {List<Object> numList = new ArrayList<>();Date birth = new Date();for (int i = 0; i < 100; i++) {numList.add(String.valueOf(i));try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("數據添加完畢,請操作:");new Scanner(System.in).next();numList = null;birth = null;System.out.println("numList、birth已置空,請操作:");new Scanner(System.in).next();System.out.println("結束");} }

    如何捕捉堆內存快照

    1、先執行第一步,然后停下來,去生成此步驟dump文件

    ![](https://img-blog.csdnimg.cn/img_convert/f64d2da235618d07f5b224da445fb241.png#align=left&display=inline&height=631&margin=[object Object]&originHeight=631&originWidth=1460&size=0&status=done&style=none&width=1460)

    2、 點擊【堆 Dump】

    ![](https://img-blog.csdnimg.cn/img_convert/f56f36380654fb7c2a75acf9aef4b951.png#align=left&display=inline&height=621&margin=[object Object]&originHeight=621&originWidth=1941&size=0&status=done&style=none&width=1941)

    3、右鍵 --> 另存為即可

    ![](https://img-blog.csdnimg.cn/img_convert/974c21c2d86fe92545891925375dfdb7.png#align=left&display=inline&height=884&margin=[object Object]&originHeight=884&originWidth=1461&size=0&status=done&style=none&width=1461)

    4、輸入命令,繼續執行程序

    ![](https://img-blog.csdnimg.cn/img_convert/199183ac1cda35b31cd34c94fddec323.png#align=left&display=inline&height=513&margin=[object Object]&originHeight=513&originWidth=1246&size=0&status=done&style=none&width=1246)

    5、我們接著捕獲第二張堆內存快照

    ![](https://img-blog.csdnimg.cn/img_convert/b9cdbaa215a0bc0eb079b87bf3e07404.png#align=left&display=inline&height=872&margin=[object Object]&originHeight=872&originWidth=1486&size=0&status=done&style=none&width=1486)

    使用 MAT 查看堆內存快照

    1、打開 MAT ,選擇File --> Open File,打開剛剛的兩個dump文件,我們先打開第一個dump文件

    點擊Open Heap Dump也行

    ![](https://img-blog.csdnimg.cn/img_convert/2114e4851852d5144bac54f0ea151b98.png#align=left&display=inline&height=686&margin=[object Object]&originHeight=686&originWidth=1196&size=0&status=done&style=none&width=1196)

    2、選擇Java Basics --> GC Roots

    ![](https://img-blog.csdnimg.cn/img_convert/3956170a73c48dd9abe0acc68da22443.png#align=left&display=inline&height=817&margin=[object Object]&originHeight=817&originWidth=1645&size=0&status=done&style=none&width=1645)

    3、第一次捕捉堆內存快照時,GC Roots 中包含我們定義的兩個局部變量,類型分別為 ArrayList 和 Date,Total:21

    ![](https://img-blog.csdnimg.cn/img_convert/e58b865997c7a03aeaa6536fe2dbab7e.png#align=left&display=inline&height=740&margin=[object Object]&originHeight=740&originWidth=1300&size=0&status=done&style=none&width=1300)

    4、打開第二個dump文件,第二次捕獲內存快照時,由于兩個局部變量引用的對象被釋放,所以這兩個局部變量不再作為 GC Roots ,從 Total Entries = 19 也可以看出(少了兩個 GC Roots)

    ![](https://img-blog.csdnimg.cn/img_convert/b6343228d2acf437158c8cf104bcc55c.png#align=left&display=inline&height=750&margin=[object Object]&originHeight=750&originWidth=1040&size=0&status=done&style=none&width=1040)

    JProfiler GC Roots 溯源

    1、在實際開發中,我們很少會查看所有的GC Roots。一般都是查看某一個或幾個對象的GC Root是哪個,這個過程叫GC Roots 溯源

    2、下面我們使用使用 JProfiler 進行 GC Roots 溯源演示

    依然用下面這個代碼

    public class GCRootsTest {public static void main(String[] args) {List<Object> numList = new ArrayList<>();Date birth = new Date();for (int i = 0; i < 100; i++) {numList.add(String.valueOf(i));try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("數據添加完畢,請操作:");new Scanner(System.in).next();numList = null;birth = null;System.out.println("numList、birth已置空,請操作:");new Scanner(System.in).next();System.out.println("結束");} }

    1、

    ![](https://img-blog.csdnimg.cn/img_convert/7ec14b58646976c9e9acc986cb70e6c1.png#align=left&display=inline&height=712&margin=[object Object]&originHeight=712&originWidth=1300&size=0&status=done&style=none&width=1300)

    2、

    ![](https://img-blog.csdnimg.cn/img_convert/0c7aea3ce6321cc3d08d64aee0389abb.png#align=left&display=inline&height=626&margin=[object Object]&originHeight=626&originWidth=681&size=0&status=done&style=none&width=681)

    ![](https://img-blog.csdnimg.cn/img_convert/0cd4fe5bf1d7ec2f17cd2a031329d30a.png#align=left&display=inline&height=587&margin=[object Object]&originHeight=587&originWidth=1300&size=0&status=done&style=none&width=1300)

    可以發現顏色變綠了,可以動態的看變化

    3、右擊對象,選擇 Show Selection In Heap Walker,單獨的查看某個對象

    ![](https://img-blog.csdnimg.cn/img_convert/8ff60124fb311d4c6ae2550b3e06ac54.png#align=left&display=inline&height=590&margin=[object Object]&originHeight=590&originWidth=1385&size=0&status=done&style=none&width=1385)

    ![](https://img-blog.csdnimg.cn/img_convert/38efd0a20d873ac71e652b87eb8e2a3e.png#align=left&display=inline&height=613&margin=[object Object]&originHeight=613&originWidth=1659&size=0&status=done&style=none&width=1659)

    4、選擇Incoming References,表示追尋 GC Roots 的源頭

    點擊Show Paths To GC Roots,在彈出界面中選擇默認設置即可

    ![](https://img-blog.csdnimg.cn/img_convert/e47be2d812ccc4d54821239931ff53e5.png#align=left&display=inline&height=524&margin=[object Object]&originHeight=524&originWidth=1300&size=0&status=done&style=none&width=1300)

    ![](https://img-blog.csdnimg.cn/img_convert/9def31a491af412ccc83ee469c30f46f.png#align=left&display=inline&height=623&margin=[object Object]&originHeight=623&originWidth=899&size=0&status=done&style=none&width=899)

    ![](https://img-blog.csdnimg.cn/img_convert/031af9eda82b04d9e83c068fb5d7cfde.png#align=left&display=inline&height=469&margin=[object Object]&originHeight=469&originWidth=1155&size=0&status=done&style=none&width=1155)

    JProfiler 分析 OOM

    這里是簡單的講一下,后面篇章會詳解

    /*** -Xms8m -Xmx8m * -XX:+HeapDumpOnOutOfMemoryError 這個參數的意思是當程序出現OOM的時候就會在當前工程目錄生成一個dump文件*/ public class HeapOOM {byte[] buffer = new byte[1 * 1024 * 1024];//1MBpublic static void main(String[] args) {ArrayList<HeapOOM> list = new ArrayList<>();int count = 0;try{while(true){list.add(new HeapOOM());count++;}}catch (Throwable e){System.out.println("count = " + count);e.printStackTrace();}} }

    程序輸出日志

    com.atguigu.java.HeapOOM java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid14608.hprof ... java.lang.OutOfMemoryError: Java heap spaceat com.atguigu.java.HeapOOM.<init>(HeapOOM.java:12)at com.atguigu.java.HeapOOM.main(HeapOOM.java:20) Heap dump file created [7797849 bytes in 0.010 secs] count = 6

    打開這個dump文件

    1、看這個超大對象

    ![](https://img-blog.csdnimg.cn/img_convert/9515f9c5b8ef3e2ff0793440410600c5.png#align=left&display=inline&height=475&margin=[object Object]&originHeight=475&originWidth=1525&size=0&status=done&style=none&width=1525)

    2、揪出 main() 線程中出問題的代碼

    ![](https://img-blog.csdnimg.cn/img_convert/7361a37fb48c06ef1916fc15f79647f4.png#align=left&display=inline&height=689&margin=[object Object]&originHeight=689&originWidth=1277&size=0&status=done&style=none&width=1277)

    清除階段:標記-清除算法

    垃圾清除階段

    • 當成功區分出內存中存活對象和死亡對象后,GC接下來的任務就是執行垃圾回收,釋放掉無用對象所占用的內存空間,以便有足夠的可用內存空間為新對象分配內存。目前在JVM中比較常見的三種垃圾收集算法是
  • 標記-清除算法(Mark-Sweep)
  • 復制算法(Copying)
  • 標記-壓縮算法(Mark-Compact)
  • 背景

    標記-清除算法(Mark-Sweep)是一種非?;A和常見的垃圾收集算法,該算法被J.McCarthy等人在1960年提出并并應用于Lisp語言。

    執行過程

    當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也被稱為stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除

  • 標記:Collector從引用根節點開始遍歷,標記所有被引用的對象。一般是在對象的Header中記錄為可達對象。
    • 注意:標記的是被引用的對象,也就是可達對象,并非標記的是即將被清除的垃圾對象
  • 清除:Collector對堆內存從頭到尾進行線性的遍歷,如果發現某個對象在其Header中沒有標記為可達對象,則將其回收
  • ![](https://img-blog.csdnimg.cn/img_convert/2ea57636ae34b75538e75ff765fbd08f.png#align=left&display=inline&height=785&margin=[object Object]&originHeight=785&originWidth=972&size=0&status=done&style=none&width=972)

    標記-清除算法的缺點

  • 標記清除算法的效率不算高
  • 在進行GC的時候,需要停止整個應用程序,用戶體驗較差
  • 這種方式清理出來的空閑內存是不連續的,產生內碎片,需要維護一個空閑列表
  • 注意:何為清除?

    這里所謂的清除并不是真的置空,而是把需要清除的對象地址保存在空閑的地址列表里。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放(也就是覆蓋原有的地址)。

    關于空閑列表是在為對象分配內存的時候提過:

  • 如果內存規整
    • 采用指針碰撞的方式進行內存分配
  • 如果內存不規整
    • 虛擬機需要維護一個空閑列表
    • 采用空閑列表分配內存
  • 清除階段:復制算法

    背景

  • 為了解決標記-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年發表了著名的論文,“使用雙存儲區的Lisp語言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky在該論文中描述的算法被人們稱為復制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp語言的一個實現版本中。
  • 核心思想

    將活著的內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中,之后清除正在使用的內存塊中的所有對象,交換兩個內存的角色,最后完成垃圾回收

    ![](https://img-blog.csdnimg.cn/img_convert/0f4b338962a307c2b4d0a0b9f7fce434.png#align=left&display=inline&height=638&margin=[object Object]&originHeight=638&originWidth=1078&size=0&status=done&style=none&width=1078)

    新生代里面就用到了復制算法,Eden區和S0區存活對象整體復制到S1區

    復制算法的優缺點

    優點

  • 沒有標記和清除過程,實現簡單,運行高效
  • 復制過去以后保證空間的連續性,不會出現“碎片”問題。
  • 缺點

  • 此算法的缺點也是很明顯的,就是需要兩倍的內存空間。
  • 對于G1這種分拆成為大量region的GC,復制而不是移動,意味著GC需要維護region之間對象引用關系,不管是內存占用或者時間開銷也不小
  • 復制算法的應用場景

  • 如果系統中的垃圾對象很多,復制算法需要復制的存活對象數量并不會太大,效率較高
  • 老年代大量的對象存活,那么復制的對象將會有很多,效率會很低
  • 在新生代,對常規應用的垃圾回收,一次通??梢曰厥?0% - 99% 的內存空間?;厥招詢r比很高。所以現在的商業虛擬機都是用這種收集算法回收新生代。
  • ![](https://img-blog.csdnimg.cn/img_convert/130e89c0cf1e378b6f002f939a69639e.png#align=left&display=inline&height=458&margin=[object Object]&originHeight=458&originWidth=957&size=0&status=done&style=none&width=957)

    清除階段:標記-壓縮算法

    標記-壓縮(或標記-整理、Mark - Compact)算法

    背景

  • 復制算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在新生代經常發生,但是在老年代,更常見的情況是大部分對象都是存活對象。如果依然使用復制算法,由于存活對象較多,復制的成本也將很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
  • 標記-清除算法的確可以應用在老年代中,但是該算法不僅執行效率低下,而且在執行完內存回收后還會產生內存碎片,所以JVM的設計者需要在此基礎之上進行改進。標記-壓縮(Mark-Compact)算法由此誕生。
  • 1970年前后,G.L.Steele、C.J.Chene和D.s.Wise等研究者發布標記-壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記-壓縮算法或其改進版本。
  • 執行過程

  • 第一階段和標記清除算法一樣,從根節點開始標記所有被引用對象
  • 第二階段將所有的存活對象壓縮到內存的一端,按順序排放。之后,清理邊界外所有的空間。
  • ![](https://img-blog.csdnimg.cn/img_convert/e6bf06f0bfcb1fb13a9e83a497a48e52.png#align=left&display=inline&height=629&margin=[object Object]&originHeight=629&originWidth=770&size=0&status=done&style=none&width=770)

    標記-壓縮算法與標記-清除算法的比較

  • 標記-壓縮算法的最終效果等同于標記-清除算法執行完成后,再進行一次內存碎片整理,因此,也可以把它稱為標記-清除-壓縮(Mark-Sweep-Compact)算法。
  • 二者的本質差異在于標記-清除算法是一種非移動式的回收算法,標記-壓縮是移動式的。是否移動回收后的存活對象是一項優缺點并存的風險決策。
  • 可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閑列表顯然少了許多開銷。
  • 標記-壓縮算法的優缺點

    優點

  • 消除了標記-清除算法當中,內存區域分散的缺點,我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可。
  • 消除了復制算法當中,內存減半的高額代價。
  • 缺點

  • 從效率上來說,標記-整理算法要低于復制算法。
  • 移動對象的同時,如果對象被其他對象引用,則還需要調整引用的地址(因為HotSpot虛擬機采用的不是句柄池的方式,而是直接指針)
  • 移動過程中,需要全程暫停用戶應用程序。即:STW
  • 垃圾回收算法小結

    對比三種清除階段的算法

  • 效率上來說,復制算法是當之無愧的老大,但是卻浪費了太多內存。
  • 而為了盡量兼顧上面提到的三個指標,標記-整理算法相對來說更平滑一些,但是效率上不盡如人意,它比復制算法多了一個標記的階段,比標記-清除多了一個整理內存的階段。
    | | 標記清除 | 標記整理 | 復制 |
    | — | — | — | — |
    | 速率 | 中等 | 最慢 | 最快 |
    | 空間開銷 | 少(但會堆積碎片) | 少(不堆積碎片) | 通常需要活對象的2倍空間(不堆積碎片) |
    | 移動對象 | 否 | 是 | 是 |
  • 分代收集算法

    Q:難道就沒有一種最優的算法嗎?

    A:無,沒有最好的算法,只有最合適的算法

    為什么要使用分代收集算法

  • 前面所有這些算法中,并沒有一種算法可以完全替代其他算法,它們都具有自己獨特的優勢和特點。分代收集算法應運而生。
  • 分代收集算法,是基于這樣一個事實:**不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。**一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點使用不同的回收算法,以提高垃圾回收的效率。
  • 在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關:
    • 比如Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鉤,因此生命周期比較長。
    • 但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,比如:String對象,由于其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。
  • 目前幾乎所有的GC都采用分代手機算法執行垃圾回收的

    在HotSpot中,基于分代的概念,GC所使用的內存回收算法必須結合年輕代和老年代各自的特點。

  • 年輕代(Young Gen)
    • 年輕代特點:區域相對老年代較小,對象生命周期短、存活率低,回收頻繁。
    • 這種情況復制算法的回收整理,速度是最快的。復制算法的效率只和當前存活對象大小有關,因此很適用于年輕代的回收。而復制算法內存利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。
  • 老年代(Tenured Gen)
    • 老年代特點:區域較大,對象生命周期長、存活率高,回收不及年輕代頻繁。
    • 這種情況存在大量存活率高的對象,復制算法明顯變得不合適。一般是由標記-清除或者是標記-清除與標記-整理的混合實現。
      • Mark階段的開銷與存活對象的數量成正比。
      • Sweep階段的開銷與所管理區域的大小成正相關。
      • Compact階段的開銷與存活對象的數據成正比。
  • 以HotSpot中的CMS回收器為例,CMS是基于Mark-Sweep實現的,對于對象的回收效率很高。對于碎片問題,CMS采用基于Mark-Compact算法的Serial Old回收器作為補償措施:當內存回收不佳(碎片導致的Concurrent Mode Failure時),將采用Serial Old執行Full GC以達到對老年代內存的整理。
  • 分代的思想被現有的虛擬機廣泛使用。幾乎所有的垃圾回收器都區分新生代和老年代
  • 增量收集算法和分區算法

    增量收集算法

    上述現有的算法,在垃圾回收過程中,應用軟件將處于一種Stop the World的狀態。在Stop the World狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程序會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。為了解決這個問題,即對實時垃圾收集算法的研究直接導致了增量收集(Incremental Collecting)算法的誕生。

    增量收集算法基本思想

  • 如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接著切換到應用程序線程。依次反復,直到垃圾收集完成。
  • 總的來說,增量收集算法的基礎仍是傳統的標記-清除和復制算法。增量收集算法通過對線程間沖突的妥善處理,允許垃圾收集線程以分階段的方式完成標記、清理或復制工作
  • 增量收集算法的缺點

    使用這種方式,由于在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。

    分區算法

    主要針對G1收集器來說的

  • 一般來說,在相同條件下,堆空間越大,一次GC時所需要的時間就越長,有關GC產生的停頓也越長。為了更好地控制GC產生的停頓時間,將一塊大的內存區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減少一次GC所產生的停頓。
  • 分代算法將按照對象的生命周期長短劃分成兩個部分,分區算法將整個堆空間劃分成連續的不同小區間。每一個小區間都獨立使用,獨立回收。這種算法的好處是可以控制一次回收多少個小區間。
  • ![](https://img-blog.csdnimg.cn/img_convert/713eb5d31743b3f92a5a6b44b5a454fe.png#align=left&display=inline&height=618&margin=[object Object]&originHeight=618&originWidth=907&size=0&status=done&style=none&width=907)

    寫在最后

    注意,這些只是基本的算法思路,實際GC實現過程要復雜的多,目前還在發展中的前沿GC都是復合算法,并且并行和并發兼備。

    總結

    以上是生活随笔為你收集整理的垃圾回收器算法的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。