JVM垃圾回收機制


一、簡介

Java GC(Garbage Collection,垃圾回收)機制,是Java與C++/C的主要區別之一

  在C++/C語言中,程序員必須小心謹慎地處理每一項內存分配,且內存使用完后必須手工釋放曾經占用的內存空間。當內存釋放不夠完全時,即存在分配但永不釋放的內存塊,就會引起內存泄漏,嚴重時甚至導致程序癱瘓。

  Java 語言的一大特點就是可以進行自動垃圾回收處理,而無需開發人員過於關注系統資源。

  垃圾回收機制對JVM中的內存進行標記,並確定哪些內存需要回收,根據一定的回收策略,自動的回收內存,永不停息(Nerver Stop)的保證JVM中的內存空間,防止出現內存泄露和溢出問題。

問題:

  1、垃圾回收並不會按照程序員的要求,隨時進行GC。

  2、垃圾回收並不會及時的清理內存,盡管有時程序需要額外的內存。

  3、程序員不能對垃圾回收進行控制。

作為Java程序員我們很難去控制JVM的內存回收,只能根據它的原理去適應,盡量提高程序的性能。

 二、垃圾回收過程

Minor GC/Young GC:只收集新生代的GC  觸發條件:Eden區滿時

Major GC/Full GC:收集老年代、永久帶(方法區)(根據垃圾收集器不同可能收集新生代)

觸發條件: 

  (1)調用System.gc()時,系統建議執行Full GC,但是不必然執行

  (2)老年代空間不足

  (3)方法去空間不足

  (4)通過Minor GC后進入老年代的平均大小大於老年代的可用內存

  (5)由Eden區、From Space區向To Space區復制時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

新生代垃圾回收過程

算法:“停止-復制”算法進行清理

1.創建對象分配到Eden區

2.第一次Eden區滿,執行Minor GC,將存活對象復制到一個存活區Survivor0,清空Eden區

  *此時,Survivor1是空白的,兩個Survivor總有一個是空白的

  *Eden區是連續的內存空間,因此在其上分配內存極快

3.程序繼續創建對象分配到Eden區,Eden區滿執行Minor GC,Eden和Survivor0中存活的對象復制到Survivor1,清空Eden和Survivor0

  *如果對象比較大,比如長字符串或大數組,Young空間不足,則大對象會直接分配到老年代上

  *大對象可能觸發提前GC,應少用,更應避免使用短命的大對象

  *用-XX:PretenureSizeThreshold來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上

  *Eden和Survivor0存活對象占用空間超過Survivor1,多余對象進入老年代

  *由於絕大部分的對象都是短命的,甚至存活不到Survivor中,所以,Eden區與Survivor的比例較大,HotSpot默認是 8:1。

    如果一次回收中,Survivor+Eden中存活下來的內存超過了10%,則需要將一部分對象分配到 老年代。

    用-XX:SurvivorRatio參數來配置Eden區域Survivor區的容量比值,默認是8,代表Eden:Survivor1:Survivor2=8:1:1.

4.程序繼續創建對象分配到Eden區,Eden區滿執行Minor GC,Eden和Survivor1中存活的對象復制到Survivor0,清空Eden和Survivor1(反復3 4)

5.當兩個存活區切換了一定次數之后,仍然存活的對象,將被復制到老年代

  *HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制

老年代、永久代垃圾回收過程

算法:老年代存儲的對象比年輕代多得多,而且不乏大對象,對老年代進行內存清理時,如果使用停止-復制算法,則相當低效。

     一般,老年代用的算法是標記-整理算法,即:標記出仍然存活的對象(存在引用的),將所有存活的對象向一端移動,以保證內存的連續。

1.調用System.gc()  或老年代空間不足  或永久代空間不足時,執行Major GC

2.老年代對象引用新生代對象的情況,如果需要執行Young GC,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的。

  解決的方法是,年老代中維護一個512 byte的塊——”card table“,所有老年代對象引用新生代對象的記錄都記錄在這里。

  Young GC時,只要查這里即可,不用再去查全部老年代,因此性能大大提高。

3.Minor GC與Full GC:

Minor GC時,檢查進入老年代的大小是否大於老年代的剩余空間大小

大於:直接觸發一次Full GC

不大於:查看是否設置了-XX:+HandlePromotionFailure(允許擔保失敗)

    允許:只會進行MinorGC,此時可以容忍內存分配失敗

    不允許:進行Full GC

(如果設置-XX:+Handle PromotionFailure不允許擔保失敗,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多內存,所以,最好不要這樣做)

4.方法區(永久代)回收:

  常量池中的常量:沒有引用了就可以被回收。(JDK7+以后,字符串常量被移動到堆中)

  無用的類信息:

    (1)類的所有實例都已經被回收

    (2)加載類的ClassLoader已經被回收

    (3)類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)

永久代的回收並不是必須的,可以通過參數來設置是否對類進行回收。
HotSpot提供-Xnoclassgc進行控制,使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看類加載和卸載信息
-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;-XX:+TraceClassUnLoading需要fastdebug版HotSpot支持

三、判斷對象是否存活

通過引用計數法/可達性分析法來判定對象是否存活

1、引用計數法

給對象中添加一個引用計數器,每當一個地方引用這個對象時,計數器值+1;當引用失效時,計數器值-1。任何時刻計數值為0的對象就是不可能再被使用的。

Java中卻沒有使用這種算法,因為這種算法很難解決對象之間相互引用的情況。

2、可達性分析法

這個算法的基本思想是通過一系列稱為“GC Roots”的對象作為起始點,從這些節點向下搜索,搜索所走過的路徑稱為引用鏈,

當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證明此對象是不可用的。

可以作為GCRoots的對象包括下面幾種:

  (1) 虛擬機棧(棧幀中的局部變量區,也叫做局部變量表)中引用的對象。

  (2) 方法區中的類靜態屬性引用的對象。

  (3) 方法區中常量引用的對象。

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

如圖:obj8、obj9、obj10都沒有到GCRoots對象的引用鏈,即便obj9和obj10之間有引用鏈,他們還是會被當成垃圾處理,可以進行回收。

3、finalize方法

對於可達性分析算法而言,若要判斷一個對象死亡,需要經歷兩次標記階段。

第一次標記:對象在進行可達性分析后發現沒有與GCRoots相連的引用鏈,則該對象被第一次標記並進行一次篩選,篩選條件為是否有必要執行該對象的finalize方法

  若對象沒有覆蓋finalize方法或者該finalize方法是否已經被虛擬機執行過了,則均視作不必要執行該對象的finalize方法,即該對象將會被回收。

  若對象覆蓋了finalize方法並且該finalize方法並沒有被執行過,這個對象會被放置在一個叫F-Queue的隊列中,之后會由虛擬機自動建立的、優先級低的Finalizer線程去執行

第二次標記:對F-Queue中對象進行第二次標記

  如果對象在finalize方法中拯救了自己,即關聯上了GCRoots引用鏈,那么在第二次標記的時候該對象將從“即將回收”的集合中移除

  如果對象還是沒有拯救自己,那就會被回收

如下代碼演示了一個對象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。具體代碼如下:

 

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、強引用

代碼中普遍存在的類似"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

2、軟引用

描述有些還有用但並非必需的對象。在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍進行二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。Java中的類SoftReference表示軟引用。

3、弱引用

描述非必需對象。被弱引用關聯的對象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。Java中的類WeakReference表示弱引用。

4、虛引用

這個引用存在的唯一目的就是在這個對象被收集器回收時收到一個系統通知,被虛引用關聯的對象,和其生存時間完全沒關系。Java中的類PhantomReference表示虛引用。

ThreadLocal中有強引用和弱引用的應用,並且有內存泄漏風險。Java並發(二十):線程本地變量ThreadLocal

五、垃圾收集算法

1、標記-清除(Mark-Sweep)算法

首先標記出所有需要回收的對象,標記完成后統一回收所有被標記的對象。

不足:

  從效率的角度講,標記和清除兩個過程的效率都不高;

  從空間的角度講,標記清除后會產生大量不連續的內存碎片。

  (內存碎片太多可能會導致以后程序運行過程中在需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發一次垃圾收集動作)

 

2、復制(Copying)算法

將可用的內存分為兩塊,每次只用其中一塊,當這一塊內存用完了,就將還存活着的對象復制到另外一塊上面,然后再把已經使用過的內存空間一次性清理掉。

優點:這樣每次只需要對整個半區進行內存回收,內存分配時也不需要考慮內存碎片等復雜情況,只需要移動指針,按照順序分配即可。

缺點:對象存活率較高的場景下要進行大量的復制操作,效率很低。萬一對象100%存活,那么需要有額外的空間進行分配擔保。(因此不適合老年代)

HotSpot 虛擬機采用這種算法來回收新生代。

3、標記-整理(Mark-Compact)算法

過程與標記-清除算法一樣,不過不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然后直接清理掉邊界以外的內存。

 

 

 總結:大批對象死去、少量對象存活的(新生代),使用復制算法,復制成本低;對象存活率高、沒有額外空間進行分配擔保的(老年代),采用標記-清理算法或者標記-整理算法。

六、垃圾收集器

HotSpo虛擬機垃圾收集器如圖:

1.如果兩個收集器之間存在連線,那說明它們可以搭配使用。

2.虛擬機所處的區域說明它是屬於新生代收集器還是老年代收集器。

3.沒有最好的垃圾收集器,更加沒有萬能的收集器,只能選擇對具體應用最合適的收集器。

1、Serial收集器

新生代收集器,使用停止復制算法,使用一個線程進行GC,串行,其它工作線程暫停。

單線程串行:進行垃圾收集時必須暫停其他線程的所有工作,直到它收集結束為止。

在用戶不可見的情況下要把用戶正常工作的線程全部停掉(Stop The World)。

不過實際上到目前為止,Serial收集器依然是虛擬機運行在Client模式下的默認新生代收集器,因為它簡單而高效。用戶桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆的新生代停頓時間在幾十毫秒最多一百毫秒,只要不是頻繁發生,這點停頓是完全可以接受的。

使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行內存回收(這也是虛擬機在Client模式下運行的默認值)

2、ParNew收集器

新生代收集器,使用停止復制算法,用多個線程進行GC(Serial收集器的多線程版),並行,其它工作線程暫停,關注縮短垃圾收集時用戶線程的停頓時間。

Server模式下的虛擬機首選的新生代收集器,因為除了Serial收集器外,目前只有它能與CMS收集器配合工作(看圖)。

使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集內存;使用-XX:ParallelGCThreads來設置執行內存回收的線程數。

3、Parallel Scavenge收集器

新生代收集器,使用停止復制算法,多線程,並行,關注CPU吞吐量。

吞吐量=運行用戶代碼的時間/總時間,比如:JVM運行100分鍾,其中運行用戶代碼99分鍾,垃圾收集1分鍾,則吞吐量是99%。反映CPU使用效率。

CMS等收集器的關注點是盡可能縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是打到一個可控制的吞吐量。

停頓時間短適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗;高吞吐量則可以高效率利用CPU時間,盡快完成運算任務,主要適合在后台運算而不需要太多交互的任務。

使用-XX:+UseParallelGC開關控制使用Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認值);使用-XX:GCTimeRatio來設置用戶執行時間占總時間的比例,默認99,即1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設置GC的最大停頓時間(這個參數只對Parallel Scavenge有效)

用開關參數-XX:+UseAdaptiveSizePolicy可以進行動態控制,如自動調整Eden/Survivor比例,老年代對象年齡,新生代大小等,這個參數在ParNew下沒有。

4、Serial Old收集器——Serial收集器的老年代版本

5、Parallel Old收集器——Parallel Scavenge收集器的老年代版本

使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。

6、CMS收集器

老年代收集器,使用標記清除算法,多線程,優點是並發收集(用戶線程可以和GC線程同時工作),停頓小。以獲取最短回收停頓時間為目標

使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行內存回收,優先使用ParNew+CMS,當用戶線程內存不足時,采用備用方案Serial Old收集(悲觀full gc)。

過程:

(1)初始標記,標記GCRoots能直接關聯到的對象,stop the world,時間很短。

(2)並發標記,標記GCRoots可達的對象,和應用線程並發執行,不需要用戶停頓,時間很長。

(3)重新標記,修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,stop the world,時間較初始標記階段長。

(4)並發清除,回收內存空間,和應用線程並發執行,時間很長。

缺點:

(1)需要消耗額外的CPU和內存資源,在CPU和內存資源緊張,CPU較少時,會加重系統負擔(CMS默認啟動線程數為(CPU數量+3)/4)。

(2)在並發收集過程中,用戶線程仍然在運行,所以可能產生“浮動垃圾”,本次無法清理,只能下一次Full GC才清理。

    因此在GC期間,需要預留足夠的內存給用戶線程使用。所以使用CMS的收集器並不是老年代滿了才觸發Full GC,而是在使用了一大半的時候就要進行Full GC。

  (默認68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction來設置)

   如果預留的用戶線程內存不夠,則會觸發Concurrent Mode Failure,此時將觸發備用方案:使用Serial Old 收集器進行收集,但這樣停頓時間就長了。

   如果用戶線程消耗內存不是特別大,可以適當調高-XX:CMSInitiatingOccupancyFraction以降低GC次數,提高性能。

(3)CMS采用的是標記清除算法,會導致內存碎片的產生

   可以使用-XX:+UseCMSCompactAtFullCollection來設置是否在Full GC之后進行碎片整理

   用-XX:CMSFullGCsBeforeCompaction來設置在執行多少次不壓縮的Full GC之后,來一次帶壓縮的Full GC

Java系列筆記(3) - Java 內存區域和GC機制中對CMS做了詳細介紹

7、G1收集器

G1是目前技術發展的最前沿成果之一,HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中發布的CMS收集器。與其他GC收集器相比,G1收集器有以下特點:

(1)並行+並發。使用多個CPU來縮短Stop The World停頓時間,與用戶線程並發執行。

(2)分代收集。獨立管理整個堆,但是能夠采用不同的方式去處理新創建對象和已經存活了一段時間、熬過多次GC的舊對象,以獲取更好的收集效果。

(3)空間整合。基於標記 - 整理算法,無內存碎片產生。

(4)可預測的停頓。能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

  在G1之前的垃圾收集器,收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存布局與其他收集器有很大差別,它將整個Java堆划分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分(可以不連續)Region的集合。

七、GC日志

[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K), [Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000, 0x0000000006ea0000)
  eden space 2176K,   2% used [0x00000000052a0000, 0x00000000052aaeb8, 0x00000000054c0000)
  from space 256K,   0% used [0x00000000054c0000, 0x00000000054c0000, 0x0000000005500000)
  to   space 256K,   0% used [0x0000000005500000, 0x0000000005500000, 0x0000000005540000)
 tenured generation   total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000, 0x000000000a6a0000)
   the space 5312K,   3% used [0x0000000006ea0000, 0x0000000006ed0730, 0x0000000006ed0800, 0x00000000073d0000)
 compacting perm gen  total 21248K, used 2982K [0x000000000a6a0000, 0x000000000bb60000, 0x000000000faa0000)
   the space 21248K,  14% used [0x000000000a6a0000, 0x000000000a989980, 0x000000000a989a00, 0x000000000bb60000)
No shared spaces configured.

1、日志的開頭“GC”、“Full GC”表示這次垃圾收集的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有Full,則說明本次GC停止了其他所有工作線程(Stop-The-World)。看到Full GC的寫法是“Full GC(System)”,這說明是調用System.gc()方法所觸發的GC。

2、“GC”中接下來的“[DefNew”表示GC發生的區域,這里顯示的區域名稱與使用的GC收集器是密切相關的,例如上面樣例所使用的Serial收集器中的新生代名為“Default New Generation”,所以顯示的是“[DefNew”。如果是ParNew收集器,新生代名稱就會變為“[ParNew”,意為“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代稱為“PSYoungGen”,老年代和永久代同理,名稱也是由收集器決定的。

3、后面方括號內部的“310K->194K(2368K)”、“2242K->0K(2368K)”,指的是該區域已使用的容量->GC后該內存區域已使用的容量(該內存區總容量)。方括號外面的“310K->194K(7680K)”、“2242K->2241K(7680K)”則指的是GC前Java堆已使用的容量->GC后Java堆已使用的容量(Java堆總容量)

4、再往后“0.0269163 secs”表示該內存區域GC所占用的時間,單位是秒。最后的“[Times: user=0.00 sys=0.00 real=0.03 secs]”則更具體了,user表示用戶態消耗的CPU時間、內核態消耗的CPU時間、操作從開始到結束經過的牆鍾時間。后面兩個的區別是,牆鍾時間包括各種非運算的等待消耗,比如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多線程操作會疊加這些CPU時間,所以如果看到user或sys時間超過real時間是完全正常的。

5、“Heap”后面就列舉出堆內存目前各個年代的區域的內存情況。

 

 

參考:

1、《JAVA編程思想》,第5章;

2、《Java深度歷險》,Java垃圾回收機制與引用類型;

3、《深入理解Java虛擬機:JVM高級特效與最佳實現》,第2-3章;

4、Java之美[從菜鳥到高手演變]之JVM內存管理及垃圾回收

5、Java垃圾回收(GC)機制詳解

6、Java系列筆記(3) - Java 內存區域和GC機制


免責聲明!

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



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