一篇帶你入門Java垃圾回收器

第一階段:串行垃圾回收器:jdk1.3.1之前Java虛擬機僅僅隻支持Serial收集器

第二階段:並行垃圾回收器:隨著多核的出現,Java引入瞭並行垃圾回收器,充分利用多核性能提升垃圾回收效率

第三階段:並發標記清理回收器CMS:垃圾回收器可以和應用程序同時運行,降低暫停用戶線程執行的時間

第四階段:G1(並發)回收器:初衷是在清理非常大的堆空間的時候能滿足特定的暫停應用程序的時間,與CMS相比會有更少的內存碎片

1 垃圾回收算法

1-1 標記清除算法

算法概述

優點:回收速度快

缺點:造成內存碎片,無法分配大的連續空間。

算法思想

在Java9之前,Java默認使用的垃圾回收器是ParallelGC,從Java9開始G1作為瞭默認的垃圾回收器

  • step1: 第一次掃描,通過GC root對象判斷堆內存中哪些對象可以進行垃圾回收,進行標記。
  • step2: 第二次掃描, 將那些標記的GC root對象進行垃圾回收,隻需要將起始內存地址與終止內存地址放入空閑內存區就行。

1-2 標記整理算法

第一個依舊是標記,第二步會進行一個空間整理,從而不產生碎片。

優點:避免瞭內存碎片

缺點:對空間的整理使得效率比較低下。

1-3 復制算法

特點

將管理的內存分為2塊區域,from區域與to區域,將那些不需要回收的對象從from區域拷貝到to區域。復制的過程中完成內存區域的整理。之後交換from和to的指向。

優點:不會產生內存碎片

缺點:需要雙倍的內存空間,內存利用率不高,而且拷貝也需要時間。



1-4 三種垃圾回收算法總結

垃圾回收算法 優點 缺點
標記清除算法(Mark Sweep) 速度較快 產生內存碎片
標記整理算法(Mark Compact) 沒有內存碎片 速度慢
復制算法(Copy) 沒有內存碎片 需要占用雙倍內存空間

註意:實際的JVM垃圾回收算法中上面的三種算法是綜合使用的。

2 JVM分代回收算法

2-1 概述

Garden of Eden:伊甸園 garbage:垃圾

新生代主要由三部分內容組成,分別是Eden區,幸存區from,幸存區to。 通常情況下隻有Eden區與幸存區from會存放數目,幸存區to隻有垃圾回收時,復制對象會用到。堆內存的新生代進行一次垃圾回收(Minor GC),大部分對象都會都會被回收。

老年代通常存放一些經常被使用的對象,一個對象如果經歷多次垃圾回收仍然幸存,那麼該對象會從新生代放入老年代。隻有新生代內存不足並且老年代內存也不足的時候才會觸發full GC對老年代的對象進行垃圾回收。

為什麼需要進行劃分?

實際環境中,對象的生命周期是不同的,老年代的對象生命周期比較長,可能很長時間才進行一次垃圾回收。新生代的對象生命周期比較短,垃圾回收比較頻繁。這種分區法方便采用不同的垃圾回收算法更加有效的進行垃圾回收。

2-2 分代垃圾回收示例

step1:程序剛剛開始運行,產生的對象先放入Eden區,當Eden區放不下的時候。

step2:對Eden區進行Minor GC,並將沒有被垃圾回收的對象復制的幸存區To,然後交換幸存區To和幸存區From,第一次垃圾回收的最終的效果如下圖所示:

step3: 第一次Minor GC, Eden區又有空間可以分配給新的對象使用,經過一段時間Eden又不夠用瞭,觸發第二次Minor GC, 這次垃圾會檢查Eden區以及幸存區From哪些對象可以存活,並將這些對象復制到幸存區To,然後交換幸存區To和幸存區From,這個時候Eden區又空瞭出來,可以放置新的對象。

實際垃圾回收過程中,JVM會對每個對象經過垃圾回收幸存下來的次數進行記錄,比如上圖中,幸存區的2個對象經過垃圾回收的次數分別是1和2。

step4: 當一些對象經過垃圾回收的次數仍然幸存的次數達到一個閾值(說明這個對象價值比較高),那麼這個對象會被移動到老年代。

極端情況考慮:Eden區,from區,老年區都已經滿瞭?

此時會觸發Full GC(優先Minor GC,Minor GC依舊內存不夠)

2-3 分代垃圾回收的總結

對象首先分配在伊甸園區域

新生代空間不足時,觸發 minor gc,伊甸園和 from 存活的對象使用 copy復制到 to 中,存活的對象年齡加 1並且交換from to

minor gc 會引發stop the world,暫停其它用戶的線程,等垃圾回收結束,用戶線程才恢復運行暫停時間較短,由於新生代大部分對象都是垃圾,復制的對象很少,所以效率較高

當對象壽命超過閾值時,會晉升至老年代,最大壽命是15(4bit,對象頭存儲)

當老年代空間不足,會先嘗試觸發 minor gc,如果之後空間仍不足,那麼觸發full gc,STW的時間更長

Full GC 的stop the world的時間要比MInor GC時間長,老年代存活對象較多加上空間整理時間,所以停止時間會較長。如果Full GC後,空間仍然不足會觸發內存不足的異常。

2-4 垃圾回收相關的虛擬機參數

垃圾回收器概述

參數含義 參數 備註
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) NewSize是初始大小,MaxNewSize是最大大小。
幸存區比例(動態) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy 幸存區的比例,默認是8,假設新生代10M內存,8M劃分給Eden區,剩下的二等分,一份from,一份to。
幸存區比例 -XX:SurvivorRatio=ratio 動態調整幸存區比例
晉升閾值 -XX:MaxTenuringThreshold=threshold 用於動態調整幸存區比例
晉升詳情 -XX:+PrintTenuringDistribution 用於動態調整幸存區比例
GC詳情 -XX:+PrintGCDetails -verbose:gc 打印詳情信息
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC 默認在Full GC 前進行一次Minor GC

2-5 垃圾回收案例分析

情況1:什麼都不放的情況

new generation:新生代 tenured generation:老年代

package cn.itcast.jvm.t2;
import java.util.ArrayList;
/**
 *  演示內存的分配策略
 */
public class Demo2_1 {
    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;
//加入Java開發交流君樣:756584822一起吹水聊天
    // -Xms20M -Xmx20M -Xmn10M : 堆初始與最大大小都是20M,新生代的大小為10M.
    // -XX:+UseSerialGC : 為瞭學習方便,采用這個垃圾回收器,默認的垃圾回收器並不是這個。
    // -XX:+PrintGCDetails -verbose:gc :打印詳細信息
    // -XX:-ScavengeBeforeFullGC :在Full GC 前進行 Minor GC.
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
            list.add(new byte[_8MB]);
        }).start();
        System.out.println("sleep....");
        Thread.sleep(1000L);
    }
}

情況1執行結果

可以看到即使用戶沒有創建對象,系統對象也要占據一部分堆內存空間。

Heap
 def new generation   total 9216K, used 2341K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
// 新生代的空間總的大小為9216K,這裡沒有把To空間給計算進去,系統任務To的空間是分配是不可用的,所以不是10M,已經使用瞭2341K,[]內部則是內存地址范圍。                              
  eden space 8192K,  28% used [0x00000000fec00000, 0x00000000fee49420, 0x00000000ff400000)
//                                                          
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
// 老年代大小為10M,可以看到沒有任何空間使用                                             
                                          //加入Java開發交流君樣:756584822一起吹水聊天   
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3394K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 378K, capacity 388K, committed 512K, reserved 1048576K

Java的內存對象都是分配在堆上嗎

情況2:新生代堆空間放滿,觸發GC

    public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);      // 系統類占用2341K,加上new的7MB觸發垃圾回收
    }

情況2執行結果

[GC (Allocation Failure) [DefNew: 2342K->696K(9216K), 0.0029193 secs] 2342K->696K(19456K), 0.0029867 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
// GC:minor GC(新生代垃圾回收)    FUll GC  (老年代垃圾回收)
// [Times: user=0.00 sys=0.00, real=0.00 secs] 垃圾回收執行時間
Heap
 def new generation   total 9216K, used 8110K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  90% used [0x00000000fec00000, 0x00000000ff33d8c0, 0x00000000ff400000)
  from space 1024K,  67% used [0x00000000ff500000, 0x00000000ff5ae100, 0x00000000ff600000)
  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3539K, capacity 4536K, committed 4864K, reserved 1056768K
 //加入Java開發交流君樣:756584822一起吹水聊天
  class space    used 395K, capacity 428K, committed 512K, reserved 1048576K

情況三: 新生代內存隨著對象的增多放不下瞭

    public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_7MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_512KB]);
    }

執行結果

新生代放不下,將新生代的對象放置到老年代。

[GC (Allocation Failure) [DefNew: 2342K->670K(9216K), 0.0022591 secs] 2342K->670K(19456K), 0.0023131 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8678K->538K(9216K), 0.0061246 secs] 8678K->8354K(19456K), 0.0061637 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 1132K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,   7% used [0x00000000fec00000, 0x00000000fec94930, 0x00000000ff400000)
  from space 1024K,  52% used [0x00000000ff400000, 0x00000000ff486a00, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 7815K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  76% used [0x00000000ff600000, 0x00000000ffda1f80, 0x00000000ffda2000, 0x0000000100000000)
 Metaspace       used 3539K, capacity 4536K, committed 4864K, reserved 1056768K
 //加入Java開發交流君樣:756584822一起吹水聊天
  class space    used 395K, capacity 428K, committed 512K, reserveed 1048576K

情況四:一開始直接分配大於新生代的內存,如果老年代放的下,則直接放到老年代

    public static void main(String[] args) throws InterruptedException {
        ArrayList<byte[]> list = new ArrayList<>();
        list.add(new byte[_8MB]);
    }

執行結果

Heap
 def new generation   total 9216K, used 2507K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  30% used [0x00000000fec00000, 0x00000000fee72ca8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
 Metaspace       used 3539K, capacity 4536K, committed 4864K, reserved 1056768K
  class space    used 395K, capacity 428K, committed 512K, reserved 1048576K

小結

當內存比較緊張的時候,即新生代內存放不下的時候,有時候會直接將對象分配到老年代,或者直接在回收次數較少(未達到15次)的情況下,直接將新生代對象弄到老年代。【資料獲取】

2 垃圾回收器

2-1 垃圾回收器概述

名稱 特點 適合場景 目標 新生代 老年代
串行垃圾回收器 單線程 堆內存小,適合個人電腦(cpu個數) 采用復制的垃圾回收算法 采用標記+整理的垃圾回收算法
吞吐量優先垃圾回收器 多線程 堆內存大,多核CPU 並行,讓單位時間內STW的時間最短 復制算法 標記+拷貝
響應時間優先的垃圾回收器(簡稱CMS) 多線程 堆內存大,多核CPU 並發,盡可能讓單次STW最短 復制算法 標記清除算法產生內存碎片需要退化成單線程的垃圾整理回收器

CMS垃圾回收器後來被G1垃圾回收器取代。

2-2 串行垃圾回收器

開啟串行垃圾回收器的JVM參數

-XX:+UseSerialGC    = Serial + SerialOld
// Serial:工作在新生代,采用復制的垃圾回收算法
// SerialOld:工作在老生代,采用標記+整理的垃圾回收算法

總結:觸發垃圾回收時,讓多個線程在一個安全點停下來,然後使用單線程的垃圾回收器去進行垃圾回收,垃圾回收完成後,再讓其他線程運行。

2-3 吞吐量優先的垃圾回收器

開啟吞吐量優先的垃圾回收器的JVM參數

開啟/關閉的參數

默認的多線程垃圾回收器,前者是開啟新生代回收器,采用復制算法,後者是開啟老年代回收器,采用標記+拷貝算法。下面選項隻要開啟一個,那麼另外一個也會開啟。

-XX:+UseParallelGC , -XX:+UseParallelOldGC 

開啟自適應動態調整新生代的大小,晉升閾值

-XX:+UseAdaptiveSizePolicy 

二個指標調整的參數(ParallelGC會根據設定的指標去調整堆的大小到達下面期望設定的目標)
指標1)1/(1+ratio) = 垃圾回收的時間/總的運行時間

ratio默認值時99,即垃圾回收的時間不超過總時間1%。但一般設為19。【資料獲取】
如果達不到目標,ParallelGC會調整堆內存大小來達到這個目標,通常是調大,這樣垃圾回收的次數會減少,從而提高吞吐量

-XX:GCTimeRatio=ratio          

指標2)每次垃圾回收的時間限制( 最大暫停的毫秒數)

默認值是200ms
顯然將堆內存空間變小有助於減少每次垃圾回收的時間

-XX:MaxGCPauseMillis=ms   

總結:顯然指標1)與指標2)是有沖突的。

-XX:ParallelGCThreads=n //垃圾回收並行的線程數目

小結

采用多線程方式進行垃圾回收,垃圾回收的線程數目通常根據CPU的核數進行設置。在垃圾回收階段,並行的垃圾回收線程會充分占用CPU。在非垃圾回收階段,用戶線程會充分利用CPU資源。

2-4 響應時間優先的垃圾回收器(CMS垃圾回收器)

缺點:采用的標記清除算法產生內存碎片需要退化成單線程的垃圾整理回收器,造成響應時間變長。

開啟的JVM參數

註意這個是並發的采用標記清除算法的垃圾回收,這裡區別於之前的垃圾回收器,該垃圾回收器能夠在進行垃圾回收的同時運行其他非垃圾回收線程(也存在時間階段需要停止,但不是所有階段停止)。

老年代並發的垃圾回收器會出現失敗的情況,這時老年代垃圾回收器會退化成單線程的垃圾回收器(SerialOld)

-XX:+UseConcMarkSweepGC // use concurrent mark sweep(會產生垃圾碎片) 工作在老年代的垃圾回收器
-XX:+UseParNewGC        // 工作在新生代的垃圾回收器

重要的初始參數

-XX:ParallelGCThreads=n        // 並行的垃圾回收線程數,通常等於CPU的核心數(垃圾回收並行階段)
-XX:ConcGCThreads=threads      // 並發的線程數目,通常設為並行垃圾回收線程數的1/4(垃圾回收並發階段)

其他參數

-XX:CMSInitiatingOccupancyFraction=percent // 執行垃圾回收的內存占比,預留空間給浮動垃圾
-XX:+CMSScavengeBeforeRemark
// 在重新標記前,對新生代進行垃圾回收,減少並發清理的垃圾對象,+開啟,-關閉

浮動垃圾是指並發清理過程中用戶線程新產生的垃圾,需要等待下次並發清理。

並發工作流程概述:

  • step1:老年代發生內存不存的現象。
  • step2:ConcMarkSweepGC會進行一個初始標記動作(初始標記需要STW即阻塞非垃圾回收線程),初始標記隻標記根對象,所以速度非常快,暫停時間也非常短。
  • step3:完成初始標記後,之前阻塞的線程又可以運行瞭,這個時候垃圾回收線程進行並發標記。
  • step4:並發標記結束後,需要再次阻塞非垃圾回收線程,進行一個所謂的重新標記,
  • step5:重新標記完成後,阻塞的線程又可以運行瞭。垃圾回收線程也並發的清理垃圾對象。

總結:

初始標記與重新標記需要阻塞線程。 在並發階段,由於垃圾回收線程占用資源,所以系統的吞吐量會受到一定的影響,但是系統的響應速度由於並發執行不會受到垃圾回收的明顯影響(相比較其他垃圾回收器,STW時間隻需要進行初始標記與重新標記,並且能夠不阻塞其他線程進行垃圾的標記與清除)。

最後,祝大傢早日學有所成,拿到滿意offer,快速升職加薪,也請多多關註WalkonNet的其他文章!

推薦閱讀: