Java中的GC回收機制


為什么要進行GC回收?

當我們新建一個對象時,系統就會為其分配一定的內存空間,而有時候新建的對象沒有去使用時,不回收的話會極大浪費內存空間,造成系統效率低下。

 

什么時候進行GC回收?

  • 1、當CPU空閑的時候
  • 2、執行System.gc()方法的時候
  • 3、堆內存滿了以后

 

GC的算法有引用計數法和可達性分析的算法進行回收

引用計數法:當新建對象就創建一個與之對應的計數器,當對象被使用時計數器就加一,而當不執行此對象時計數器就減一,最終計數器為0的對象將會被回收。

  • 優點:執行速度快
  • 缺點:當存在相互引用時會造成內存泄漏

 

相互引用

public class test{
       public static void main(String []args){
              public Object instance=null;
              test test1 = new test();
              test test2 = new test();
              test1.instance = test2;
              test.instance = test1;
test1=null;
test2=null; } }

  內存泄漏:內存泄漏是指無用對象(不再引用對象)持續占有內存,得不到及時的釋放,從而造成的內存空間的浪費。

 

 

可達性分析法

通過一系列名為"GC Roots"的對象作為起點,從這些節點往下搜尋,搜索所經過的路徑稱為引用鏈,當某個對象沒有任何引用鏈相連時,證明此對象無用,回收(回收不是立即進行的,Java對象在回收前調用finalize()方法——finalize方法通常交給線程優先級較低的線程去處理,有些本應該被回收的對象還會“再生”原因是它復制給了可達對象去引用“借屍還魂法”)。

 

 

如何回收?

回收算法有四種:標記清除法、復制算法、標記整理法、分代回收法

標記清除法:

對內存中無用的對象進行標記,然后回收,缺點是會造成內存不連續,空間的浪費。

 

 

復制算法:

將內存分成兩塊,每次只使用其中的一塊,當這一塊使用完之后就將這一塊中存活的對象復制到另一塊內存中,已經使用完的內存空間清除。缺點是將內存縮成原來的一半。對於存活較多的對象,要進行復制,效率較低。

 

 

標記整理法:

與標記清除法有一定的類似,不過標記整理法僅對存活的對象進行處理,對不調用的對象不進行處理,將活的對象復制到另一半內存中並整理,不會產生內存碎片

 

 

分代回收法:(目前大部分JVM所采用的方法

1、年輕代:對象被創建時,內存的分配首先在年輕代(大的對象可以直接創建在老年代),大部分的對象在創建后就不在使用了,因此很快變得不可達,於是被年輕代的GC機制清理掉,這個GC機制被稱為Minor GC或者叫Young GC。年輕代分為三個區,Eden和兩個活區(survivor0和survivor1)分別占80%、10%、10%。

將Eden中和survivor中存活的對象拷貝到另一個survivor中(使用的是stop-and-copy的方法)並且清空Eden和survivor中的對象,當Eden滿了以后執行minorGC,並將剩余存活的對象放到survivor0中,回收Eden中沒有存活的對象。當survivor0滿了以后,就將存活的對象復制到survivor1中,不存活的對象回收。依次去存。當兩個存活區切換了15次后(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代),仍然存活的對象將被存放近老年代區。

2、老年代:(存放較大的實例化對象和在新生代中存活很久的對象)

使用的是標記整理算法,即標記存活對象,向一端移動,保證內存完整性,然后將未標記的清理掉。當老年代不夠用,也會執行majorGC,FullGC.

3、永久代:永久代的回收有兩種,一種是常量池中的常量、二是無用的類信息

對無用的類信息進行回收必須保證以下幾點:

  • 類的所有實例已經被回收
  • 加載類的ClassLoader已經被回收
  • 類對象的Class對象沒有被引用

垃圾收集器

1、Serial收集器

  • 串行收集器是最古老、最高效、最穩定的收集器。
  • 可能產生較長的停頓。
  • -XX:UseSerialGC
  • -XX:UseSerial Old
  • 新生代收集器,使用停止-復制算法,使用一個線程進行GC,串行,其他工作線程停止

 

 

 

2、並行收集器

  • -XX:UseParNewGC(new表示新生代適用)
  • 新生代並行
  • Serial收集器新生代並行版本
  • 在新生代回收時使用停止復制算法
  • 多線程,需要多核支持
  • ParallelGCThreads 限制線程數量

 

 

 3、parallel收集器

  • 類似ParNew
  • 新生代復制算法
  • 老年代標記整理算法
  • 更關注吞吐量

UseParallelGC  :使用parallel收集器+老年代串行

UseParallelOldGC  :使用parallel收集器+老年代並行

其他GC參數

MaxGCPausemills

最大停頓時間,單位毫秒

GCTimeRatio

0-100范圍

垃圾收集時間占總時間比

默認99,即最大允許1%的時間做GC

4、CMS收集器

 

  • Concurrent Mark Sweep  並發標記清除(應用程序線程與GC線程交替執行)
  • 使用標記清除算法
  • 並發階段會降低吞吐量
  • 老年代收集器
  • UseConcMarkSweepGC

資料來源:https://www.cnblogs.com/bluemapleman/p/9277064.html

                https://blog.csdn.net/laomo_bible/article/details/83112622

              https://www.cnblogs.com/xiaoxi/p/6486852.html

CMS運行的標記過程主要為

1、初始標記(會產生全局停頓)

  • 根可以直接關聯到的對象
  • 速度快

2、並發標記(和用戶線程一起)

主要的標記過程,標記全部對象

3、重新標記(會產生全局停頓)

由於並發標記時,線程正在運行,因此在正式清理前,再做修正。

4、並發清理(和用戶線程一起)

基於標記結果,直接清理對象

 

 

 CMS收集器的特點:

盡可能降低停頓

會影響系統整體吞吐量和性能

清理不徹底

finalize()方法詳解

1、finalize的作用

(1)finalized是Object類中protected的方法,子類可以覆蓋該方法以實現資源清理工作,GC在回收對象前使用該方法。

(2)finalized()方法與c++中的析構函數不是對應的。c++中的析構函數調用的時機是確定的(對象離開作用域或delete掉),但Java中finalized()調用有不確定性。

(3)不建議用finalized()對“非內存資源”的清理工作,建議用於:1、清理本地對象 ;2、作為某些非內存資源(如Socket、文件)釋放的補充:在finalized方法中顯式調用其他其他資源釋放方法

2、finalized的生命周期

當對象不可達時,GC判斷對象是否覆蓋了finalized()方法,若未覆蓋則直接將對象回收。否則對象未執行finalized()方法,將其放入F-Queue隊列,由一低優先級線程執行該隊列中對象的finalized()方法。執行完畢后,GC再判斷對象是否可達,若不可達則回收,否則對象“復活”。

代碼例子

package com.demo;

/*
 * 此代碼演示了兩點:
 * 1.對象可以再被GC時自我拯救
 * 2.這種自救的機會只有一次,因為一個對象的finalize()方法最多只會被系統自動調用一次
 * */
public class FinalizeEscapeGC {
    
    public String name;
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public FinalizeEscapeGC(String name) {
        this.name = name;
    }

    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        System.out.println(this);
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    @Override
    public String toString() {
        return name;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC("leesf");
        System.out.println(SAVE_HOOK);
        // 對象第一次拯救自己
        SAVE_HOOK = null;
        System.out.println(SAVE_HOOK);
        System.gc();
        // 因為finalize方法優先級很低,所以暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }

        // 下面這段代碼與上面的完全相同,但是這一次自救卻失敗了
        // 一個對象的finalize方法只會被調用一次
        SAVE_HOOK = null;
        System.gc();
        // 因為finalize方法優先級很低,所以暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead : (");
        }
    }
}

  運行結果

leesf
null
finalize method executed!
leesf
yes, i am still alive :)
no, i am dead : (

  

 

一些疑問

1、GC是怎么判斷對象是被標記的

通過枚舉根節點的方式,通過jvm提供的一種oopMap的數據結構,簡單來說就是不要再通過遍歷內存里的東西,而是通過OOPMap的數據結構去記錄該記錄的信息,比如說它可以不用去遍歷整個棧,而是掃描棧上面引用的信息並記錄下來。

2、什么時候觸發GC

minor GC(young GC):當年輕代中eden區分配滿的時候觸發【young GC后部分存活的對象會已到老年代(比如對象熬過15輪),所以old gen的占用量通常會變高】

full GC:

  • 1)手動調用system.gc()方法【增加full GC的頻率,不建議使用而是讓jvm自己管理內存,可以設置-XX:DisableExplicitGC來禁止RMI調用system.gc】
  • 2)發現perm gen(如果存在永久代的話)需分配空間但已經沒有足夠的空間
  • 3)老年代空間不足,比如說新生代的大對象數組晉升到老年代可能導致老年代空間不足。
  • 4)CMS GC時出現promotion Faield【pf】
  • 5)統計的到的minor GC晉升到舊生代的平均大小大於老年代的剩余空間。

full gc導致了concurrent mode failure,而不是concurrent mode failure錯誤導致觸發full gc,真正觸發full gc 可能是promotion failure。

3、cms收集器是否會掃描年輕代

會,在初始標記的時候會掃描新生代。

雖然cms是老年代收集器,但是我們 知道年輕代對象是可以晉升老年代的,為了空間分配擔保,還是有必要掃描年輕代。

4、什么是空間分配擔保

在minor gc前,jvm會檢查年老代最大可用空間是否大於新生代所有對象的總空間,如果是的話,則minor gc可以確保安全

 

如果擔保失敗,會檢查一個配置(handlepromotionfailire),即是否允許擔保失敗。

如果允許:繼續檢查老年代最大可用的連續空間是否大於之前的晉升的平均大仙,比如說剩10m,之前每次都有9M左右的新生到老年代,那么嘗試一次minor gc,這會比較冒險。如果不允許,而且還小於的情況,則會觸發full gc.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM