jvm - 垃圾回收

来源:http://www.cnblogs.com/itkk/archive/2017/11/01/7765016.html
-Advertisement-
Play Games

jvm 垃圾回收 註意 : 本系列文章為學習系列,部分內容會取自相關書籍或者網路資源,在文章中間和末尾處會有標註 垃圾回收的意義 它使得java程式員不再時時刻刻的關註記憶體管理方面的工作. 垃圾回收機制會自動的管理jvm記憶體空間,將那些已經不會被使用到了的"垃圾對象"清理掉",釋放出更多的空間給其他 ...


jvm - 垃圾回收

註意 : 本系列文章為學習系列,部分內容會取自相關書籍或者網路資源,在文章中間和末尾處會有標註

垃圾回收的意義

它使得java程式員不再時時刻刻的關註記憶體管理方面的工作.

垃圾回收機制會自動的管理jvm記憶體空間,將那些已經不會被使用到了的"垃圾對象"清理掉",釋放出更多的空間給其他對象使用.

何為對象的引用?

Java中的垃圾回收一般是在Java堆中進行,因為堆中幾乎存放了Java中所有的對象實例

在java中,對引用的概念簡述如下(引用強度依次減弱) :

  • 強引用 : 這類引用是Java程式中最普遍的,只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象

  • 軟引用 : 用來描述一些非必須的對象,在系統記憶體不夠使用時,這類對象會被垃圾收集器回收,JDK提供了SoftReference類來實現軟引用

  • 弱引用 : 用來描述一些非必須的對象,只要發生GC,無論但是記憶體是否夠用,這類對象就會被垃圾收集器回收,JDK提供了WeakReference類來實現弱引用

  • 虛引用 : 與其他幾種引用不同,它不影響對象的生命周期,如果這個對象是虛運用,則就跟沒有引用一樣,在任何時刻都可能會回收,JDK提供了PhantomReference類來實現虛引用

如下為相關示例代碼

public class ReferenceDemo {
    public static void main(String[] arge) {
        //強引用
        Object object = new Object();
        Object[] objects = new Object[100];

        //軟引用
        SoftReference<String> stringSoftReference = new SoftReference<>(new String("SoftReference"));
        System.out.println(stringSoftReference.get());
        System.gc();
        System.out.println(stringSoftReference.get()); //手動GC,這時記憶體充足,對象沒有被回收

        System.out.println();

        //弱引用
        WeakReference<String> stringWeakReference = new WeakReference<>(new String("WeakReference"));
        System.out.println(stringWeakReference.get());
        System.gc();
        System.out.println(stringWeakReference.get()); //手動gc,這時,返回null,對象已經被回收

        System.out.println();

        //虛引用
        //虛引用主要用來跟蹤對象被垃圾回收器回收的活動。
        //虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列 (ReferenceQueue)聯合使用。
        //當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之 關聯的引用隊列中
        ReferenceQueue<String> stringReferenceQueue = new ReferenceQueue<>();
        PhantomReference<String> stringPhantomReference = new PhantomReference<>(new String("PhantomReference"), stringReferenceQueue);
        System.out.println(stringPhantomReference.get());
    }
}

當然,關於這幾種引用還有很多知識點,本文只做簡單的介紹,後續有機會再單獨的文章詳細介紹.

如何確定需要回收的垃圾對象?

引用計數器

每個對象都有一個引用計數器 , 新增一個引用的時候就+1,引用釋放的時候就-1,當計數器為0的時候,就表示可以回收

引用計數演算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的選擇,當Java語言並沒有選擇這種演算法來進行垃圾回收,主要原因是它很難解決對象之間的相互迴圈引用問題

public class LoopReferenceDemo {
    
    public static void main(String[] args) {
        TestA a = new TestA(); //1
        TestB b = new TestB(); //2
        a.b = b; //3
        b.a = a; //4
        a = null; //5
        b = null; //6
    }
    
}

class TestA {
    public TestB b;

}

class TestB {
    public TestA a;
}

雖然a和b都為null,但是a和b存在迴圈引用,這樣a和b就永遠不會被回收

如果你在互聯網上搜索"引用計數器"這個關鍵字,通常都會得到以上這一個結論,但是究竟為什麼a和b不會被回,收其實還是沒有說清楚的,下麵簡單說明一下 :

  • 第一行 : TestA的引用計數器加1,TestA的引用數量為1

  • 第二行 : TestB的引用計數器加1,TestB的引用數量為1

  • 第三行 : TestB的引用計數器加1,TestB的引用數量為2

  • 第四行 : TestA的引用計數器加1,TestA的引用數量為2

記憶體分佈如下圖

引用計數器示例-1

  • 第五行 : 將a變數設置為null,不再指向堆中的引用,所以TestA的引用計數器減1,TestA的引用數量為1

  • 第六行 : 將b變數設置為null,不再指向堆中的引用,所以TestB的引用計數器減1,TestB的引用數量為1

記憶體分佈如下圖

引用計數器示例-2

  • 結論 : 雖然上面程式將a和b設置為null了,但是在堆中,TestA和TestB還是互相持有對方的引用,引用計數器依然不等於0,這個就稱為迴圈引用,所以說"引用計數器"會存在這個問題,導致這類對象無法被清理掉.

以上的知識點參考 : https://www.zhihu.com/question/21539353

可達性分析

雖然以上的"引用計數器"演算法存在"迴圈引用"的問題,不過目前主流的虛擬機都採用"可達性分析(GC Roots Tracing)"演算法來標記那些對象是可以被回收的.

該演算法是從GC Roots開始向下搜索,搜索走過的路徑稱之為引用鏈.當一個對象到GC Roots沒有任何引用鏈相連時,就代表這個對象是不可用的.稱為"不可達對象"

GC Roots包括:

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

  • 方法區中的靜態屬性實體引用的對象

  • 方法區中常量引用的對象

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

實際上,在根搜索演算法中,要真正宣告一個對象死亡,至少要經歷兩次標記過程 :

  • 如果對象在進行根搜索後發現沒有與GC Roots相連接的引用鏈,那它會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法

  • 當對象沒有覆蓋finalize()方法,或finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為沒有必要執行

  • 如果該對象被判定為有必要執行finalize()方法,那麼這個對象將會被放置在一個名為F-Queue隊列中,併在稍後由一條由虛擬機自動建立的、低優先順序的Finalizer線程去執行finalize()方法

  • finalize()方法是對象逃脫死亡命運的最後一次機會(因為一個對象的finalize()方法最多只會被系統自動調用一次), 稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中讓該對象重引用鏈上的任何一個對象建立關聯即可

  • 而如果對象這時還沒有關聯到任何鏈上的引用,那它就會被回收掉

如下圖所示

可達性分析

從上圖上看,reference1,2,3都是gc roots

reference1指向instance1,reference2指向instance4,並且instance4又指向了instance6,reference3則指向了instance2

所以說instance1,2,4,6都具有gc roots可達性,是存活著的對象,不會被垃圾回收器回收掉

而instance3,5則不具備gc roots可達性,是不可用對象,將會被垃圾回收器回收掉

從上圖描述"引用計數器"的圖例場景來看,TestA和TestB雖然互相有持有引用,但是並不具備gc roots可達性,所以,在"可達性分析"演算法下,是會被垃圾回收器回收掉的

垃圾收集的演算法

標記-清除 演算法

演算法分為"標記"和"清除"兩個階段,首先標記出需要回收的對象,在標記完成後,統一回收掉之前被標記的所有對象. 它是最基礎的收集演算法 . 後續的收集演算法都是基於這種思想,並且對其缺點進行改進而產生的

標記-清除

主要缺點:

  • 效率問題 : 需要標記和清除兩次掃描

  • 空間問題 : 標記和清除之後會產生大量的不連續的記憶體碎片,可能會導致,當程式需要分配一個較大記憶體空間的時候,無法找到足夠的連續記憶體,從而不得不提前出發另外一次垃圾回收動作

複製 演算法

將可用記憶體按容量劃分為兩塊,每次只使用其中的一塊,當記憶體使用完了後,就將還存活著的對象複製到另外一塊上面,然後在把前面一塊記憶體一次性清理掉

複製

優點 :

  • 每次只操作一塊記憶體,分配記憶體的時候無需考慮記憶體碎片的情況,只需要移動對象的指針,按順序分配記憶體即可,實現簡單,運行高效

缺點 :

  • 會將記憶體縮小為原來的一半

  • 持續複製長生期的對象則導致效率降低 (沒理解) (對於存活率較高的對象,就會對其進行多次複製,從而導致效率降低)

標記-壓縮 演算法

和標記-清除演算法一樣,只不過標記後的動作不是清除,而是將所有對象向一端移動,然後直接清理掉邊界以外的對象(被標記的對象)

標記-壓縮

特點 :

  • 複製演算法比較適合於新生代,在老年代中,對象存活率比較高,如果執行較多的複製操作,效率將會變低,所以老年代一般會選用其他演算法,如標記—清理演算法

  • 該演算法標記的過程與標記—清除演算法中的標記過程一樣,但對標記後出的垃圾對象的處理情況有所不同,它不是直接對可回收對象進行清理,而是讓所有的對象都向一端移動,然後直接清理掉端邊界以外的記憶體

分代收集 演算法

把java的堆分為"新生代"和"老年代",對於不同的年代採用不同演算法

分代管理

在新生代中,由於對象生命周期非常短暫,所以每次垃圾回收的時候都會有大量的對象死去,只有少量存活,這樣,採用"複製演算法",就只需要付出少量存活對象的複製成本,就能完成回收

在老年代中,由於對象生命周期比較長,存活率較高,沒有額外的空間對它進行分配和擔保,那就必須使用"標記-清除演算法"或者"標記-壓縮演算法"來進行回收

Minor GC: 從年輕代空間(包括Eden和Survivor區域)回收記憶體被稱為Minor GC

Major GC: 清理老年代

Full GC: 清理整個堆空間—包括年輕代和老年代

年輕代: 是所有新對象產生的地方.年輕代被分為3個部分(Enden區和兩個Survivor區,也叫From和To),當Eden區被對象填滿時,就會執行Minor GC,並把所有存活下來的對象轉移到其中一個survivor區(Form),Minor GC同樣會檢查存活下來的對象,並把它們轉移到另一個survivor區(To),這樣在一段時間內,總會有一個空的survivor區,經過多次GC周期後,仍然存活下來的對象會被轉移到年老代記憶體空間,常這是在年輕代有資格提升到年老代前通過設定年齡閾值來完成的,需要註意,Survivor的兩個區是對稱的,沒先後關係,from和to是相對的.

老年代: 在年輕代中經歷了N次回收後仍然沒有被清除的對象,就會被放到年老代中,都是生命周期較長的對象.對於年老代,則會執行Major GC,來清理.在某些情況下,則會觸發Full GC,來清理整個堆記憶體

元空間: 堆外的一部分記憶體,通常直接使用的是系統記憶體,用於存放運行時常量池,等內容,垃圾回收對應元空間來說沒有明顯的影響

垃圾收集器

垃圾收集器是記憶體回收演算法的具體實現,Java虛擬機規範中對垃圾收集器應該如何實現並沒有任何規定,因此不同廠商、不同版本的虛擬機所提供的垃圾收集器都可能會有很大的差別

Sun HotSpot虛擬機1.6版包含瞭如下收集器:Serial、ParNew、Parallel Scavenge、CMS、Serial Old、Parallel Old

這些收集器以不同的組合形式配合工作來完成不同分代區的垃圾收集工作,如下是垃圾收集器簡單介紹 :

Serial收集器

串列收集器,最古老,最穩定,以及效率高的收集器,但是可能會造成程式較長時間的停頓,只使用一個線程去回收.新生代,老年代使用串列回收

新生代使用"複製演算法"

老年代使用"標記壓縮演算法"

垃圾回收的過程中會"程式暫停"(Stop the world)

ParNew收集器

是Serial收集器的多線程版,新生代並行,老年代串列

新生代使用"複製演算法"

老年代使用"標記壓縮演算法"

垃圾回收的過程中會"程式暫停"(Stop the world)

Paralle收集器

類似於ParNew收集器,但是更關註系統的吞吐量.

可以通過參數來打開"自適應調節策略",虛擬機會根據系統當前的運行情況收集性能監控信息,動態調整這些參數以便提供最合適的停頓時間和最大的吞吐量

也可以通過參數控制GC的時間不大於多少毫秒或者比例

新生代使用"複製演算法"

老年代使用"標記壓縮演算法"

Parallel Old收集器

是Paralle收集器的老年代版本 , 使用多線程和"標記-整理演算法",這個收集器在JDK1.6中才開始使用

CMS收集器

是基於"標記-清除"演算法實現的,它的運作過程相對於前面的其中收集器要複雜一些,整個過程分為4個步驟,包括 :

  • 初始標記(CMS initial mark)

  • 併發標記(CMS concurrent mark)

  • 重新標記(CMS remark)

  • 併發清除(CMS concurrent sweep)

初始標記和併發標記仍需要Stop the World.

初始標記僅僅只是標記一下GC Root能直接關聯到的對象,速度很快.

併發標記階段就是進行GC Root Tracing的過程.

重新標記這是為了修正併發標記期間,因用戶程式繼續運作而導致標記變動的那一部分的標記記錄,這一階段的停頓時間會比初始標記階段的時間稍長一些,但遠比併發標記時間短

整個過程中耗時最長的併發標記和併發清除過程中,收集器線程可以與用戶線程一起工作,所以總體來說,CMS收集器的記憶體回收是與用戶線程一起併發執行的

優點 : 併發收集,低停頓

缺點 : 產生大量的空間碎片,併發階段會降低吞吐量

G1收集器

與CMS收集器項目,G1收集器有以下特點 :

  • 空間整合 :

    • G1收集器採用標記-整理演算法,不會產生空間碎片.分配大對象時不會應為找不到連續的空間而提前觸發下一次GC
  • 可預測停頓 :

    • 降低停頓時間是G1和CMS的共同關註點

    • G1除了追求低停頓外,還能建立可預測的停頓時間模型.

    • 能讓使用者明確指定在一個長度為N毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,幾乎已經是實時java(RTSJ)垃圾回收的特征了

上面提到的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1不在是這樣.

使用G1收集器的時候,JAVA堆的記憶體佈局與其他的收集器有很大的差別,它將這個java堆劃分為多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,他們都是一部分(可以不連續)Region的集合

G1的新生代收集器跟ParNew類似,當新生代占用達到一定的比例的時候,開始觸發收集

和CMS類似,G1收集器收集老年代對象的時候會有短暫停頓

收集步驟如下 :

  • 標記階段 :

    • 首先初始標記(initial mark),這個階段是停頓的(Stop the world event),並且會觸發一次普通的Mintor GC(從年輕代空間回收)
  • Root Region Scanning :

    • 運行程式過程中會回收survivor區(存活到老年代),這一過程必須在young GC之前完成
  • Concurrent Marking :

    • 在整個java堆中進行併發標記(和應用程式併發執行),此過程可能會被young GC中斷

    • 若發現區域對象中的所有對象都是垃圾,那個區域就會被立即回收

    • 同時,併發標記過程中,會去計算每個區域的對象活性(區域中存活對象的比例)

  • Remark :

    • 再標記,會有短暫停頓(STW)

    • 是用來收集併發標記階段,產生新的垃圾(併發階段和應用程式一同執行)

    • G1中採用了比CMS更快的初始快照演算法 : snapshot-at-the-beginning (SATB)

  • Copy / Clean up :

    • 多線程清除失活對象,會有STW

    • G1將回收區域的存活對象拷貝到新的區域,清除Remember Sets,併發清空回收區域,並把它返回到空閑的區域鏈表中

  • 複製/清除過程後 :

    • 回收區域的活性對象已經被收集器回收到"最近複製的年輕代"(recently copied in young generation)和"最近複製的老年代"(recently copied in old generation)區域中了

參考文獻

<<深入理解JVM虛擬機>>

結束

本文提到的點很多,有對象引用,如何定義垃圾對象,gc演算法,現有的垃圾收集器,等.

由於篇幅和時間原因,每個點都提及的不深入(當然,本篇文章的每個點深入的聊起來,都夠寫本書的了,呵呵).

後續會找機會逐個的將這些點跟大家深入的討論.

總之 "學無止境" , 與大家共勉 .

代碼倉庫 (博客配套代碼)


想獲得最快更新,請關註公眾號

想獲得最快更新,請關註公眾號


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 如果你是程式員,那麼肯定會在周末給自己充下電 有的人選擇看書學習 有的人選擇看視頻學習 如果你是一個老鳥,那麼看書,看博客,看官方文檔,這些是老鳥的學習路線 如果對上面都不感冒,可能你會在網上找一些視頻教程來去學習 如果你是看視頻學習的話, 下麵的這些意見可能對你會有用 一、視頻學習 看視頻的過程中 ...
  • 本文是基於嵌入式的C語言 首先弄明白程式是什麼?電腦為什麼需要編程? 編寫程式的目的是為了去運行,從而得到一些結果。顧名思義電腦就是用來計算的,所以電腦的所有程式就是在計算,那麼電腦在計算什麼呢?當然是在計算數據,因此,數據是程式的重要組成。推出:電腦程式 = 代碼 + 數據。那麼程式運行 ...
  • import java.util.HashMap;import java.util.Map;import java.util.Map.Entry;import java.util.Set; import javax.servlet.http.HttpServletRequest; public st ...
  • 點擊試聽課程 前言 點擊試聽課程 前言 很多自學編程的同學經常和我說想學一門語言自己到網上找一些教程看到一半就像背單詞背到ambulance一樣堅持不下去了....究其原因基本上都是:內容太多,太枯燥,專業術語聽不懂,學的不知道能幹嘛,學到一半就放棄了...確實從零學習編程是一個很枯燥的事,所以在想 ...
  • 用到查看當前目錄的完整路徑使用:pwd 物理路徑和連接路徑什麼鬼?沒明白暫時借鑒別人的記錄下 顯示當前目錄的物理路徑 pwd –P 1: [root@DB-Server init.d]# cd /etc/init.d 2: [root@DB-Server init.d]# pwd -P 3: /et ...
  • JSTL簡介 用的時候: ...
  • 我們可以通過這樣子的方式去理解apache的工作原理 1 單進程TCP服務(堵塞式) 這是最原始的服務,也就是說只能處理個客戶端的連接,等當前客戶端關閉後,才能處理下個客戶端,是屬於阻塞式等待 這種阻塞型自然不適合處理多客戶端的請求,於是有了改版 2 多進程服務 採取多進程處理多客戶端連接請求,對單 ...
  • 一、引用類型有哪些 類 介面 數組 枚舉 註解 以上五種為引用數據類型, 我們現在學了其中的兩種 使用new關鍵字創建出來的類型都是引用數據類型 二、什麼是地址傳遞 引用數據類型有兩塊存儲空間 一個在棧(Stack)中,一個在堆(heap)中。 棧中存放的是堆中的地址 當我們把引用類型當作參數傳遞時 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...