Java對象的"後事處理"——垃圾回收(二)

来源:https://www.cnblogs.com/zhangweicheng/archive/2019/11/08/11809376.html
-Advertisement-
Play Games

1 先談Finalize() finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時,所以筆者建議大家完全可以忘掉Java語言中有這個方法的存在。 ——《深入理解JVM》 finalize()方法確實可以實現一次對象的自救,但是其不確定性和昂貴的運行代價都表 ...


1 先談Finalize()

finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、更及時,所以筆者建議大家完全可以忘掉Java語言中有這個方法的存在。

——《深入理解JVM》

  finalize()方法確實可以實現一次對象的自救,但是其不確定性昂貴的運行代價都表明這個方法的使用需要十分的慎重。那麼finalize()在什麼時期起作用又是如何實現對象的自救的呢?首先我們要理解虛擬機在掃描到死亡對象的時候並不是直接回收的,而是進行一次標記並且篩選,篩選的條件就是其對象的finalize方法是否有必要執行。如果當前對象沒有重寫finalize方法或者已經調用過一次finalize方法,那麼則視為沒有必要執行,此時便失去自救的機會,放入"即將回收"集合中。

  否則的話,則將對象放入一個叫F-Queue的隊列中,稍後虛擬機將一個個的執行隊列中對象的finalize方法(就是在此處對象可以在finalize方法中將自身關聯到引用鏈,從而暫時逃脫被回收的命運),需要註意的是虛擬機保證執行但不保證執行完finalize方法,原因是如果finalize方法執行時間過長或者陷入死迴圈,則可能讓系統奔潰。全部執行之後,虛擬機將對隊列的對象重新標記一次,如果還不在引用鏈中則GG,否則將其移出"即將回收"集合。下麵例子參考《深入理解JVM》實現自救並且驗證只能自救一次的過程。

public class TestForGc {

    /** 定義一個根節點的靜態變數 */
    public static TestForGc INSTANCE;

    /**
     * 重寫finalize方法,讓其被標記為有必要執行並且加入F-Q
     *
     * @throws Throwable
     */
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.err.println("finalize method in TestForGc Class invoked!");
        // 將自身關聯到根節點中,實現自救
        INSTANCE = this;
    }

    public static void main(String[] args) throws InterruptedException {
        INSTANCE = new TestForGc();

        INSTANCE = null;
        System.gc();
        // 睡眠1S,保證F-Q中的方法執行完畢
        TimeUnit.SECONDS.sleep(1);
        if (Objects.nonNull(INSTANCE)) {
            System.out.println("i successfully save myself by finalize method!");
        } else {
            System.out.println("i am dead :(");
        }

        /*
         * 下麵驗證finalize方法只能調用一次
         * 幾乎完全一樣的代碼,卻是不同的結局
         */
        INSTANCE = null;
        System.gc();
        // 睡眠1S
        TimeUnit.SECONDS.sleep(1);
        if (Objects.nonNull(INSTANCE)) {
            System.out.println("i successfully save myself by finalize method again!");
        } else {
            System.out.println("couldn't invoke finalize again, i am dead :(");
        }
    }
}
執行結果:

 2 垃圾回收器

  如果說回收演算法是介面,那麼垃圾回收器就是這些介面的實現類,共有7種回收器,接下來一一羅列。

2.1 Serial垃圾回收器

  Serial是一種單線程垃圾回收器,在工作的時候的時候會暫停所有的用戶線程,也就是"stop-the-world",雖然單線程代表了用戶線程的停頓,但是也意味著其不用進行線程的交互從而有更高的收集 效率。Serial採用複製演算法,是Client端新生代的預設垃圾回收器。其工作圖類似於:

2.2 ParNew垃圾回收器。

  ParNewSerial回收器的多線程版本,是Server端新生代的預設回收器,除了並行多線程之外,其他包括實現都是一模一樣,當然也是採用複製演算法。還有一點重要的是,新生代的收集器除了Serial之外,只有ParNew能跟年老代的CMS合作,其在低CPU的情況下效率比Serial低,但是在多個CPU的情況下要好的多。其工作圖:

2.3 Parallel Scavenge垃圾回收器

  跟ParNew類似,作用於新生代,並行多線程並且也是採用複製演算法。但是其關註的點卻不同,其著重的是一種叫做"吞吐量"的東西。所謂的"吞吐量"=運行用戶代碼的時間 / (運行用戶代碼的時間 + GC時間),也就是說其更加註重用戶代碼運行時間不是減少GC停頓時間。相對於其他收集器來說,可以更加高效的利用CPU,更加適合作為在後臺運算而不大需要交互的任務。Parallel收集器提供了兩個比較重要的參數。

-XX:MaxGCPauseMillis:表示收集器將儘可能的在這個參數設定的毫秒數內完成回收工作。但這並不代表其設置的越低越好,縮減回收時間是通過減少吞吐量換來的,如果設置得太低可能導致頻繁的GC。

-XX:GCTimeRatio:表示代碼運行時間和垃圾回收時間的比率,比如說設置為19,那麼則垃圾回收時間占比為 1 / (1+19) = 5%,預設是99。

2.4 Serial Old垃圾回收器

  Serial的年老代版本,同Serial基本相似,不同的是採用的是標記-整理演算法實現,作為Client端預設的年老代收集器。如果在Server端的話,那麼其主要作用有二:

 1、跟新生代的Parallel Scavenge收集器配合。

 2、做一個有價值的"備胎":當CMS垃圾回收器因為預留空間問題放不下對象而發生Concurrent Mode Fail時,作為其備選方案執行垃圾回收。

 

2.5 Parallel Old垃圾回收器

  Parallel Scavenge的年老代版本,多線程並行,同樣註重吞吐量,使用標記-整理演算法。這個收集器可以跟新生代的Parallel Svavenge一起搭配使用,在註重吞吐量和CPU資源敏感的場合中是一對很好的組合。

2.6 CMS垃圾回收器

  來了,它來了!CMS垃圾回收器被當做是具有劃時代意義的、真正實現併發的垃圾回收器,總而言之=》

  ,--^----------,--------,-----,-------^--,

  | ||||||||| `--------' | O

  `+---------------------------^----------|

  `\_,-------, _______________________強__|

  / XXXXXX /`| /

  / XXXXXX / `\ /

  / XXXXXX /\______(

  / XXXXXX /

  / XXXXXX /

  (________(

   `------'

  CMS是一款併發的垃圾回收器,但並不代表全程都不需要停頓,只是大部分時間是跟用戶線程一起執行的。其整個GC過程中總共有4個階段。

1、初始標記:簡單的標記所有的根節點,需要暫停所有的用戶線程,即"stop-the-world",耗時較短。關於GCRooots的過程可以看下另一篇文章——垃圾回收(一)

2、併發標記:跟用戶線程一起工作,尋找堆中的死亡對象,整個過程耗時最長。

3、重新標記:再次掃描,主要對象是併發標記過程中又新增的對象,也就是驗漏。多線程,需要STW,時間相對併發標記來說短。

4、併發清除:GC線程跟用戶線程一起執行,清除標記的死亡對象,"浮動垃圾"在此階段產生。

  然而,優秀如CMS也會有不足之處,總共四個階段的標記及清除演算法的實現必定為其帶來一些使用的麻煩。

缺點:

  1、占用一定CPU資源:其有兩個階段需要併發跟用戶線程一起執行,也就是說要跟用戶線程搶占CPU的時間片,會占用一定的CPU資源,如果CPU資源不太優質的情況下,可能會造成不小的影響。

  2、空間利用率不能達到最大:由於併發清除時用戶線程也在運行,那麼在GC結束前必定會產生一些額外的垃圾,那麼就必須給這些垃圾預留一定的空間,否則會導致記憶體不足從而報"Concurrent Mode Failure",此時虛擬機便啟用後備方案——使用Serial Old來進行垃圾回收,進而浪費更多的時間。

  3、記憶體碎片導致提前FullGC:CMS採用的是標記-清除演算法,也就是說會產生記憶體碎片,那麼可能出現大對象放不下的情況,進而不得不提前進行一次FullGC。為瞭解決這個問題,虛擬機提供了兩個參數-XX:+UseCMSCompactAtFullCollection-XX:CMSFullGCsBeforeCompaction,分別表示CMS頂不住要進行FullGC的時候進行記憶體的整理(整理的過程中無法併發,停頓時間不得不變長) 和進行多少次不壓縮的FullGC之後來一次整理的GC(預設0次,表示每次都進行記憶體整理)。

2.7 G1垃圾回收器

  G1是一個新秀垃圾回收器,被賦予了很大的使命——取代CMS。G1作為新時代的垃圾回收器,相對於其他垃圾回收器來說有許多優勢。

1、並行和併發:G1可以利用現在的硬體優勢,縮短GC時stop-the-world的停頓時間,並且GC的時候同時也能讓用戶線程執行。

2、分代收集:跟其他垃圾回收器不同,G1沒有物理上的年老代和新生代,其將記憶體分成了多個獨立的Region,每個Region都可能表示屬於新生代還是年老代,所以不需要一堆Region湊放在一起然後將這塊區域稱作新生代,它們之間並不需要連續,所以只有概念上的分代,也是這種分代方式使得G1可以獨立管理這個堆空間,不需要跟其他回收器合作。

3、空間整合:G1的演算法從Region層面看屬於複製演算法(從一個Region複製到另一個),但是從整體看又是標記-整理法。然而不管是哪種,都表示G1不會產生記憶體碎片,不會因為空間不連續放不下大對象而出現FullGC的情況。

  G1回收器將記憶體空間分成若幹個Region,並且這些Region之前相互獨立。但是我們都知道這並不能真正的獨立,因為一個Region中的對象不一定只會被當前Region的其他對象引用,而可能被堆中的其他對象引用,那G1是如何實現避免全堆掃描的呢?這個問題在分代的其他回收器中也有,但是在這裡突顯得更加明顯而已。再G1中,對象本身都會有一個Remembered Set,這個Set存放著當前對象被其他區域對象引用的信息,這樣子,在掃描引用的時候加上這個Set就可以避免全堆掃描了。

  具體實現大致為:虛擬機在發現程式正在進行對Reference類型的寫操作時,會暫時中斷寫操作,然後檢查Reference引用的對象是否處於不同的區域如果是分代,則只對年老代的對象進行檢查,檢查是否引用的對象在新生代),如果是的話則將引用信息記錄在被引用的Remembered Set中,這樣在GC的時候加上Remembered Set的掃描就可以避免全堆掃描了。

  跟CMS類型,G1也有四個階段(不算Remembered Set的掃描),雖然相似但是還是有些區別的。

1、初始標記:標記可達的根節點,STW,單線程,時間短。

2、併發標記:跟用戶線程同時執行,併發執行時對象可能會產生引用變化,其會將這些變化記錄在Remembered Set Logs中,待下個階段整合。

3、最終標記:驗漏,將併發標記階段的引用變化記錄Remembered Set Logs整合到Remembered Set中。

4、篩選回收:對各個Region中的回收價值進行排序,然後執行回收計劃。暫停用戶線程,並行執行。 

3 小結

  本文首先介紹了“對象自救”的方法——finalize,並且用一個小例子演示了對象如何實現自救。接著介紹了7種不同的垃圾回收器,新生代中有單線程的Serial可以作為Client端新生代的預設回收器,有多線程版本的Serial——ParNew,還有著重點不同(吞吐量)的Parallel Scavenge;年老代方面有單線程的Serial Old、跨時代意義的併發回收器——CMS,雖然優秀但不可避免的有三個缺點、還有吞吐量年老代版本——Parallel Old收集器,最後還簡單介紹了G1收集器的幾個過程還有獨立的Region間是如何實現避免堆掃描的。

  整體下來整篇行文還有些粗糙,日後會慢慢的圓潤,如果有關於這方面好的文章可以在下麵評論區分享學習一下,下方為各個垃圾回收器的搭配圖。

 

 

It helps me a lot if you could share your opinion with us.


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

-Advertisement-
Play Games
更多相關文章
  • CSS的引入 在早期,如果要去定義一個H1的標題的顏色、字體、大小和其他的顯示特征,就需要用到HTML中的font或其他樣式的指令,H1只是一個結構指令所以光有它是不夠的。因此如果有多個標簽要去進行處理,就會造成樣式的重覆,後期維護的困難。 那CSS的出現就解決了這一類的問題,CSS(Cascadi ...
  • GoF設計模式一共有23個。一般可以按目的和作用範圍來進行劃分,具體劃分方法如下: 第一,這些模式按目的(即完成什麼樣任務)來劃分為創建型、結構型和行為型這三種模式: 創建型:用來創建對象。單例、原型、抽象工廠、建造者、工廠方法這五個都屬於這一分類。這種類別起到了將對象的創建與其使用進行分離解耦。 ...
  • 重構改善既有代碼 第一次做某件事情的時候儘管去做,第二次做類似的事會產生反感,第三次再做類似的事,你就應該重構。 小型函數優美動人 一個類最好是常量類,任何的改變都是調用該類本身的介面實現。 0 壞代碼的味道 1、重覆代碼 Duplicated Code 同一類中的兩個函數含有相同的表達式,提取到方 ...
  • 0 簡單工廠模式 0.0 簡單工廠模式動機 考慮一個簡單的軟體應用場景,一個軟體系統可提供多個外觀不同按鈕(如圓形、矩形按、菱形按鈕等), 這些按鈕都源自同一個父類,不過在繼承父類後不同的子類修改了部分屬性從而使得它們可呈現不同外觀,如果希望在使用這些按鈕時,不需要知道這些具體按鈕類的名字,只需要知 ...
  • 要想理解持續集成和持續部署,先要瞭解它的部分組成,以及各個組成部分之間的關係。下麵這張圖是我見過的最簡潔、清晰的持續部署和集成的關係圖。 "圖片來源" 持續部署: 如圖所示,開發的流程是這樣的: 程式員從源碼庫(Source Control)中下載源代碼,編寫程式,完成後提交代碼到源碼庫,持續集成( ...
  • 本解決方案是一個Windows應用編程框架和UI庫,包括四個項目: Ligg.EasyWinForm是一個Winform應用編程框架和UI庫。通過這個該框架,不需任何代碼,通過XML配置文件,搭建任意複雜的Windows應用界面,以類似Execel公式的方式實現基本的過程式控制制(賦值、條件判斷、迴圈、 ...
  • (手機橫屏看源碼更方便) 註:java源碼分析部分如無特殊說明均基於 java8 版本。 註:本文基於ForkJoinPool分治線程池類。 簡介 隨著在硬體上多核處理器的發展和廣泛使用,併發編程成為程式員必須掌握的一門技術,在面試中也經常考查面試者併發相關的知識。 今天,我們就來看一道面試題: 如 ...
  • 題目: 鏈接:https://www.nowcoder.com/questionTerminal/6736cc3ffd1444a4a0057dee89be789b?orderByHotValue來源:牛客網牛牛舉辦了一次編程比賽,參加比賽的有3*n個選手,每個選手都有一個水平值a_i.現在要將這些選 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...