天天看點

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

1. 垃圾回收基礎

1.1 什麼是GC?

GC(Garbage Collection): 垃圾回收器

Java是由C++發展來的。

在老式的C/C++程式中,程式員定義了一個變量,就是在記憶體中開辟了一段相應的空間來存值。由于記憶體是有限的,是以當程式不再需要使用某個變量的時候,就需要銷毀該對象并釋放其所占用的記憶體資源,好重新利用這段空間。在C/C++中,釋放無用變量記憶體空間的事情需要由程式員自己來處理。就是說當程式員認為變量沒用了,就手動地釋放其占用的記憶體。但是這樣顯然非常繁瑣,如果有所遺漏,就可能造成資源浪費甚至記憶體洩露。當軟體系統比較複雜,變量多的時候程式員往往就忘記釋放記憶體或者在不該釋放的時候釋放記憶體了。

由于我大學四年都在實驗室做嵌入式開發,最早用的開發語言就是C和C++。特别能了解需要在代碼裡面主動釋放記憶體的痛苦。C++要十年磨一劍也是因為需要更多的經驗來在大型複雜的軟體系統中用好指針和垃圾回收,避免記憶體溢出或洩露。

有了GC,程式員就不需要再手動的去控制記憶體的釋放。當Java虛拟機(VM)發覺記憶體資源緊張的時候,就會自動地去清理無用對象(沒有被引用到的對象)所占用的記憶體空間。如果需要,可以在程式中顯式地使用System.gc() / System.GC.Collect()來強制進行一次立即的記憶體清理。Java提供的GC功能可以自動監測對象是否超過了作用域,進而達到自動回收記憶體的目的,Java的GC會自動進行管理,調用方法:System.gc() 或者Runtime.getRuntime().gc();

1.2 分代回收理論

目前商業虛拟機的垃圾回收器,大多遵循“分代收集”的理論來進行設計,這個理論大體上是這麼描述的:

1、 絕大部分的對象都是朝生夕死。

2、 熬過多次垃圾回收的對象就越難回收。

根據以上兩個理論,朝生夕死的對象放一個區域,難回收的對象放另外一個區域,這個就構成了新生代和老年代。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

1.3 GC分類

市面上發生垃圾回收的叫法很多,定義也比較混亂,以下概念總結簡單直接:

  • 1、 新生代回收(Minor GC/Young GC):指隻是進行新生代的回收。Young GC的觸發原因都是eden區滿了。
  • 2、 老年代回收(Major GC/Old GC):指隻是進行老年代的回收。目前隻有 CMS 垃圾回收器會有這個單獨的回收老年代的行為。

    (Major GC 定義是比較混亂,有說指是老年代,有的說是做整個堆的收集,這個需要你根據别人的場景來定,沒有固定的說法)

  • 3、 整堆回收(Full GC):收集整個 Java 堆和方法區(注意包含方法區)。定義是相對明确的,就是針對整個新生代、老生代、元空間(metaspace,java8以上版本取代perm gen)的全局範圍的GC。

觸發條件就是某GC算法對應區域滿了,或是預測快滿了

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器
JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

2. 垃圾回收算法

垃圾回收算法的實作設計到大量的程式細節,并且每一個平台的虛拟機操作記憶體的方式都有不同,是以不需要去了解算法的實作,我們重點講解 3 種算法的思想。

2.1 複制算法(Copying)

将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使 用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要按順序配置設定記憶體即可, 實作簡單,運作高效。隻是這種算法的代價是将記憶體縮小為了原來的一半。

但是要注意:記憶體移動是必須實打實的移動(複制),是以對應的引用(直接指針)需要調整。

複制回收算法适合于新生代,因為大部分對象朝生夕死,那麼複制過去的對象比較少,效率自然就高,另外一半的一次性清理是很快的。

Appel 式回收:

一種更加優化的複制回收分代政策:具體做法是配置設定一塊較大的 Eden 區和兩塊較小的 Survivor 空間(你可以叫做 From 或者 To,也可以叫做 Survivor1 和 Survivor2)

專門研究表明,新生代中的對象 98%是“朝生夕死”的,是以并不需要按照 1:1 的比例來劃分記憶體空間,而是将記憶體分為一塊較大的 Eden 空間和兩塊較 小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor[1]。當回收時,将 Eden 和 Survivor 中還存活着的對象一次性地複制到另外一塊 Survivor 空間上, 最後清理掉 Eden 和剛才用過的 Survivor 空間。

HotSpot 虛拟機預設 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的 90%(80%+10%),隻有 10%的記憶體會被 “浪費”。當然,98%的對象可回收隻是一般場景下的資料,我們沒有辦法保證每次回收都隻有不多于 10%的對象存活,當 Survivor 空間不夠用時,需要 依賴其他記憶體(這裡指老年代)進行配置設定擔保(Handle Promotion)

2.2 标記-清除算法(Mark-Sweep)

算法分為“标記”和“清除”兩個階段:首先掃描所有對象标記出需要回收的對象,在标記完成後掃描回收所有被标記的對象,是以需要掃描兩遍。 回收效率略低,如果大部分對象是朝生夕死,那麼回收效率降低,因為需要大量标記對象和回收對象,對比複制回收效率要低。

它的主要問題,标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻以後在程式運作過程中需要配置設定較大對象時,無法找到足夠的連 續記憶體而不得不提前觸發另一次垃圾回收動作。

回收的時候如果需要回收的對象越多,需要做的标記和清除的工作越多,是以标記清除算法适用于老年代。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

2.3 标記-整理算法(Mark-Compact)

首先标記出所有需要回收的對象,在标記完成後,後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端 邊界以外的記憶體。标記整理算法雖然沒有記憶體碎片,但是效率偏低。

我們看到标記整理與标記清除算法的差別主要在于對象的移動。對象移動不單單會加重系統負擔,同時需要全程暫停使用者線程才能進行,同時所有引用 對象的地方都需要更新(直接指針需要調整)。

是以看到,老年代采用的标記整理算法與标記清除算法,各有優點,各有缺點。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

3. JVM 中常見的垃圾回收器

在新生代中,每次垃圾回收時都發現有大批對象死去,隻有少量存活,那就選用複制算法,隻需要付出少量存活對象的複制成本就可以完成回收。 而老年代中因為對象存活率高、沒有額外空間對它進行配置設定擔保,就必須使用“标記—清理”或者“标記—整理”算法來進行回收。

3.1 Serial/Serial Old

JVM 剛誕生就隻有這種,最古老的,單線程,獨占式,成熟,适合單 CPU,一般用在用戶端模式下。

這種垃圾回收器隻适合幾十兆到一兩百兆的堆空間進行垃圾回收(可以控制停頓時間再 100ms 左右),但是對于超過這個大小的記憶體回收速度很慢,是以對于現在來說這個垃圾回收器已經是一個雞肋。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

VM options

-XX:+UseSerialGC 新生代和老年代都用串行收集器

Stop The World(STW):

單線程進行垃圾回收時,必須暫停所有的工作線程,直到它回收結束。這個暫停稱之為“Stop The World”,但是這種 STW 帶來了惡劣的使用者體驗,例如:應用每運作一個小時就需要暫停響應 5 分。這個也是早期 JVM 和 java 被 C/C++語言诟病性能差的一個重要原因。是以 JVM 開發團隊一直努力消除或降低 STW 的時間。

執行個體:

import java.util.List;

/**
 * @author 公衆号:IT三明治
 * @date 2021/5/2
 * VM options:
 * -XX:+PrintGCDetails -XX:+UseSerialGC
 * -XX:+PrintGCDetails
 * -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC
 * -XX:+PrintGCDetails -XX:+UseG1GC
 */
public class TestGC {
    /**
     * 不停往list中填充資料
     * 就使用不斷的填充 堆 -- 觸發GC
     */
    public static class FillListThread extends Thread {
        List<Object> list = new LinkedList<>();

        @Override
        public void run() {
            try {
                while (true) {
                    if (list.size() * 512 / 1024 / 1024 >= 990) {
                        list.clear();
                        System.out.println("list is clear");
                    }
                    byte[] bl;
                    for (int i = 0; i < 100; i++) {
                        bl = new byte[512];
                        list.add(bl);
                    }
                    Thread.sleep(1);
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        FillListThread thread = new FillListThread();
        thread.start();
    }
}
           

設定VM options

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

執行後檢視GC日志:

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

DefNew: 新生代回收

Tenured: 老年代回收

Metabase: 方法區(元空間)回收

3.2 Parallel Scavenge(ParallerGC)/Parallel Old

為了提高回收效率,從 JDK1.3 開始,JVM 使用了多線程的垃圾回收機制,關注吞吐量的垃圾收集器,高吞吐量則可以高效率地利用 CPU 時間,盡快完成 程式的運算任務,主要适合在背景運算而不需要太多互動的任務。

所謂吞吐量就是 CPU 用于運作使用者代碼的時間與 CPU 總消耗時間的比值,即吞吐量=運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間),虛拟機總 共運作了 100 分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是 99%。

該垃圾回收器适合回收堆空間 上百兆~幾個 G。

VM options (JDK1.8 預設就是以下組合)

-XX:+UseParallelGC 新生代使用 Parallel Scavenge,老年代使用 Parallel Old

收集器提供了兩個參數用于精确控制吞吐量,分别控制的停頓時間的-XX:MaxGCPauseMillis 參數以及直接設定吞吐量大小的-XX:GCTimeRatio 參數。

-XX:MaxGCPauseMillis

-XX:MaxGCPauseMillis=time
Sets a target for the maximum GC pause time (in milliseconds). This is a soft goal, and the JVM will make its best effort to achieve it. By default, there is no maximum pause time value.
The following example shows how to set the maximum target pause time to 500 ms:
-XX:MaxGCPauseMillis=500
           

不過大家不要異想天開地認為如果把這個參數的值設定得更小一點就能使得系統的垃圾收集速度變得更快,垃圾收集停頓時間縮短是以犧牲吞吐量和新生代空間為代價換取的:系統把新生代調得小一些,收集 300MB 新生代肯定比收集 500MB 快,但這也直接導緻垃圾收集發生得更頻繁,原來 10 秒 收集一次、每次停頓 100 毫秒,現在變成 5 秒收集一次、 每次停頓 70 毫秒。停頓時間的确在下降,但吞吐量也降下來了。

-XX:GCTimeRatio

-XX:GCTimeRatio 參數的值則應當是一個大于 0 小于100的整數,也就是垃圾收集時間占總時間的比率,相當于吞吐量的倒數。

例如:把此參數設定為 19, 那允許的最大垃圾收集時占用總時間的 5% (即 1/(1+19)), 預設值為 99,即允許最大 1% (即 1/(1+99))的垃圾收集時間 由于與吞吐量關系密切,ParallelScavenge 是“吞吐量優先垃圾回收器”。

-XX:+UseAdaptiveSizePolicy

這是一個開關參數(預設開啟), 當這個參數被激活之後,就不需要人工指定新生代的大小(-Xmn)、Eden 與 Survivor 區的 比例(-XX:SurvivorRatio)、 晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數了,虛拟機會根據目前系統的運作情況收集性能監控資訊,動态調 整這些參數以提供最合适的停頓時間或者最大的吞吐量。

3.1的執行個體改回預設的垃圾回收器-XX:(+UseParallelGC):

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

執行代碼,再看看他的GC日志跟3.1有什麼不同

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

我這裡給出不同的日志,是想大家熟悉一下不同垃圾回收器的GC日志,以後看到GC log, 自然就知道當時用的是什麼GC

3.3 ParNew

多線程垃圾回收器,與 CMS 進行配合,對于 CMS(CMS 隻回收老年代),新生代垃圾回收器隻有 Serial 與 ParNew 可以選。和 Serial 基本沒差別,唯一的區 别:多線程,多 CPU 的,停頓時間比 Serial 少。(在 JDK9 以後,把 ParNew 合并到了 CMS 了)

大緻了解下搭配關系即可,後續版本已經接近淘汰。

3.4 Concurrent Mark Sweep(CMS)

收集器是一種以擷取最短回收停頓時間為目标的收集器。目前很大一部分的 Java 應用集中在網際網路站或者 B/S 系統的服務端上,這類應用尤其重視服務 的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS 收集器就非常符合這類應用的需求。

3.4.1 運作過程

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

從名字(包含“Mark Sweep”)上就可以看出,CMS 收集器是基于“标記—清除”算法實作的,它的運作過程相對于前面幾種收集器來說更複雜一些, 整個過程分為 4 個步驟,包括:

  • 初始标記-短暫,僅僅隻是标記一下 GC Roots 能直接關聯到的對象,速度很快。
  • 并發标記-和使用者的應用程式同時進行,進行 GC Roots 追蹤的過程,标記從 GCRoots 開始關聯的所有對象開始周遊整個可達分析路徑的對象。這個時間比較長,是以采用并發處理(垃圾回收器線程和使用者線程同時工作)
  • 并發清除-由于整個過程中耗時最長的并發标記和并發清除過程收集器線程都可以與使用者線程一起工作,是以,從總體上來說,CMS 收集器的記憶體回收過程是與用 戶線程一起并發執行的。
-XX:+UseConcMarkSweepGC ,表示新生代使用 ParNew,老年代用 CMS

3.4.2 執行個體分析

把3.1的執行個體改造成如下

package sandwich.test4;

import java.util.LinkedList;
import java.util.List;

/**
 * @author 公衆号:IT三明治
 * @date 2021/5/2
 * VM options:
 * -XX:+PrintGCDetails -XX:+UseSerialGC
 * -XX:+PrintGCDetails
 * -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC
 * -XX:+PrintGCDetails -XX:+UseG1GC
 */
public class TestGC {
    /**
     * 不停往list中填充資料
     * 就使用不斷的填充 堆 -- 觸發GC
     */
    public static class FillListThread extends Thread {
        List<Object> list = new LinkedList<>();

        @Override
        public void run() {
            try {
                while (true) {
                    if (list.size() * 512 / 1024 / 1024 >= 990) {
                        list.clear();
                        System.out.println("list is clear");
                    }
                    byte[] bl;
                    for (int i = 0; i < 100; i++) {
                        bl = new byte[512];
                        list.add(bl);
                    }
                    Thread.sleep(1);
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 每1ms定時列印
     */
    public static class TimerThread extends Thread {
        public final static long startTime = System.currentTimeMillis();

        @Override
        public void run() {
            try {
                while (true) {
                    long t = System.currentTimeMillis() - startTime;
                    System.out.println(t / 1000 + "." + t % 1000);
                    Thread.sleep(1);
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        FillListThread thread = new FillListThread();
        TimerThread timerThread = new TimerThread();
        thread.start();
        timerThread.start();
    }
}
           

把GC改成CMS+ParNew

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

再執行代碼執行個體,看看GC log,這裡我想看看GC log跟使用者線程的關系,我需要分開截圖。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

由上圖可見初始化标記需要暫停所有使用者線程的時間并不長,這個測試沒有達到毫秒級别。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

由上圖可見,并發标記的過程還是有點複雜的,包括:

  • 并發标記
  • 并發預清理
  • 并發可放棄的預清理。

但是這些過程不會暫停其他使用者線程。你可以看到使用者線程還是不斷打穿插執行。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

由上圖可見重新标記雖然會暫停所有使用者線程,但是這裡的STW時間才1.4ms,相對于并發可放棄的預清除處理耗時110ms,最終标記的耗時可以說微乎其微了。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

由上圖可見,并發清除耗時50ms,并發清除一結束,馬上就開始并發重置了。

3.4.3 CMS的缺點

  • CPU敏感:CMS 對處理器資源敏感,畢竟采用了并發的收集、當處理核心數不足 4 個時,CMS對使用者的影響較大。
  • 浮動垃圾:由于 CMS 并發清理階段使用者線程還在運作着,伴随程式運作自然就還會有新的垃圾不斷産生,這一部分垃圾出現在标記過程之後,CMS 無法 在當次收集中處理掉它們,隻好留待下一次 GC 時再清理掉。這一部分垃圾就稱為“浮動垃圾”。

    由于浮動垃圾的存在,是以需要預留出一部分記憶體,意味着CMS收集不能像其它收集器那樣等待老年代快滿的時候再回收。 在 1.6 的版本中老年代空間使用率門檻值(92%)。

    如果預留的記憶體不夠存放浮動垃圾,就會出現Concurrent Mode Failure,這時虛拟機将臨時啟用Serial Old來替代CMS。

  • 會産生空間碎片:标記-清除算法會導緻産生不連續的空間碎片。

    總體來說,CMS 是 JVM 推出了第一款并發垃圾收集器,是以還是非常有代表性。

    但是最大的問題是 CMS 采用了标記清除算法,是以會有記憶體碎片,當碎片較多時,給大對象的配置設定帶來很大的麻煩,為了解決這個問題,CMS提供一個 參數:-XX:+UseCMSCompactAtFullCollection,一般是開啟的,如果配置設定不了大對象,就進行記憶體碎片的整理過程。

    這個地方一般會使用Serial Old,因為Serial Old是一個單線程,是以如果記憶體空間很大、且對象較多時, CMS發生這樣情況會很卡。

3.4.4 CMS總結

CMS問題比較多,是以現在沒有一個版本預設是CMS,隻能手工指定。但是它畢竟是第一個并發垃圾回收器,對于了解并發垃圾回收具有一定意義,是以我們必須了解。

為什麼CMS采用标記-清除?

在實作并發的垃圾回收時,如果采用标記整理算法,那麼還涉及到對象的移動(對象的移動必定涉及到引用的變化,這個需要暫停業務線程來處理棧資訊,這樣使得并發收集的暫停時間更長),是以使用簡單的标記-清除算法才可以降低CMS的STW的時間。該垃圾回收器适合回收堆空間幾個G~ 20G左右。

3.5 Garbage First(G1)

随着 JVM 中記憶體的增大,STW 的時間成為 JVM 急迫解決的問題,但是如果按照傳統的分代模型,總跳不出 STW 時間不可預測這點。

為了實作 STW 的時間可預測,首先要有一個思想上的改變。G1 将堆記憶體“化整為零”,将堆記憶體劃分成多個大小相等獨立區域(Region),每一個 Region 都可以根據需要,扮演新生代的 Eden 空間、Survivor 空間,或者老年代空間。回收器能夠對扮演不同角色的 Region 采用不同的政策去處理,這樣無論是 新建立的對象還是已經存活了一段時間、熬過多次收集的舊對象都能擷取很好的收集效果。

3.5.1 Region

Region 可能是 Eden,也有可能是 Survivor,也有可能是 Old,另外 Region 中還有一類特殊的 Humongous 區域,專門用來存儲大對象。 G1 認為隻要大小超過 了一個 Region 容量一半的對象即可判定為大對象。每個 Region 的大小可以通過參數-XX:G1HeapRegionSize 設定,取值範圍為 1MB~32MB,且應為 2 的 N 次 幂。而對于那些超過了整個 Region 容量的超級大對象,将會被存放在 N 個連續的 Humongous Region 之中,G1 的進行回收大多數情況下都把 Humongous Region 作為老年代的一部分來進行看待。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

開啟G1的參數:

-XX:+UseG1GC

分區大小

-XX:G1HeapRegionSize=size
Sets the size of the regions into which the Java heap is subdivided when using the garbage-first (G1) collector. The value can be between 1 MB and 32 MB. The default region size is determined ergonomically based on the heap size.
The following example shows how to set the size of the subdivisions to 16 MB:

-XX:G1HeapRegionSize=16m
           

最大 GC 暫停時間

-XX:MaxGCPauseMillis=time
Sets a target for the maximum GC pause time (in milliseconds). This is a soft goal, and the JVM will make its best effort to achieve it. By default, there is no maximum pause time value.

The following example shows how to set the maximum target pause time to 500 ms:

-XX:MaxGCPauseMillis=500
           

3.5.2 運作過程

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

G1 的運作過程大緻可劃分為以下四個步驟:

  • 初始标記( Initial Marking)

    僅僅隻是标記一下 GC Roots 能直接關聯到的對象,并且修改 TAMS 指針的值,讓下一階段使用者線程并發運作時,能正确地在可用的 Region 中配置設定新對象。 這個階段需要停頓線程,但耗時很短,而且是借用進行 Minor GC 的時候同步完成的,是以 G1 收集器在這個階段實際并沒有額外的停頓。

    TAMS 是什麼?

    要達到 GC 與使用者線程并發運作,必須要解決回收過程中新對象的配置設定,是以 G1 為每一個 Region 區域設計了兩個名為 TAMS(Top at Mark Start)的指針, 從 Region 區域劃出一部分空間用于記錄并發回收過程中的新對象。這樣的對象認為它們是存活的,不納入垃圾回收範圍。

  • 并發标記( Concurrent Marking)

    從 GC Root 開始對堆中對象進行可達性分析,遞歸掃描整個堆裡的對象圖,找出要回收的對象,這階段耗時較長,但可與使用者程式并發執行。當對象圖掃描完成以後,并發時有引用變動的對象,這些對象會漏标(後續再講三色标記的時候會細講這個問題),漏标的對象會被一個叫做 SATB(snapshot-at-the-beginning)算法來解決

  • 最終标記( Final Marking)

    對使用者線程做另一個短暫的暫停,用于處理并發階段結後仍遺留下來的最後那少量的 SATB 記錄(漏标對象)。

  • 篩選回收( Live Data Counting and Evacuation)

    負責更新 Region 的統計資料,對各個 Region 的回收價值和成本進行排序,根據使用者所期望的停頓時間來制定回收計劃,可以自由選擇任意多個 Region 構成回收集,然後把決定回收的那一部分 Region 的存活對象複制到空的 Region 中,再清理掉整個舊 Region 的全部空間。這裡的操作涉及存活對象的移動, 是必須暫停使用者線程,由多條收集器線程并行完成的。

3.5.3 執行個體分析

繼續采用3.4的執行個體

把vm options改為

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

執行代碼并分析日志,我想看看G1的GC日志和其他使用者線程的關系,是以采取分段截圖

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

由上圖可見young區初始化标記在這個執行個體都可以花費近30ms,這個過程是需要暫停其他使用者線程的。 root region掃描也需要幾ms。不過root region的掃描是可以并發執行的。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

由上圖可見,并發标記用了54ms。 最終标記花費了0.48ms, 這個過程會暫停其他使用者線程,但是耗時很短。回收耗時1.5ms。

JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

mixed的時候GC pause時間是18ms。由此可見mixed的GC pause不一定比剛才隻是young區的GC pause時間長。

由于初始标志和并發标記的耗時并不小,是以G1在小堆記憶體項目中并沒有什麼優勢。

3.5.4 G1特點

  • 并行與并發:G1 能充分利用多 CPU、多核環境下的硬體優勢,使用多個 CPU(CPU 或者 CPU 核心)來縮短 Stop-The-World 停頓的時間,部分其他收集器原本需要停頓Java 線程執行的GC動作,G1收集器仍然可以通過并發的方式讓 Java 程式繼續執行。
  • 分代收集:與其他收集器一樣,分代概念在 G1 中依然得以保留。雖然 G1 可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新建立的對象和已經存活了一段時間、熬過多次 GC 的舊對象以擷取更好的收集效果。
  • 空間整合:與 CMS 的“标記—清理”算法不同,G1從整體來看是基于“标記—整理”算法實作的收集器,從局部(兩個 Region 之間)上來看是基于“複 制”算法實作的,但無論如何,這兩種算法都意味着G1運作期間不會産生記憶體空間碎片,收集後能提供規整的可用記憶體。這種特性有利于程式長時間運作,配置設定大對象時不會因為無法找到連續記憶體空間而提前觸發下一次 GC。
  • 追求停頓時間:

    -XX:MaxGCPauseMillis 指定目标的最大停頓時間,G1 嘗試調整新生代和老年代的比例,堆大小,晉升年齡來達到這個目标時間。

怎麼玩?

該垃圾回收器适合回收堆空間上百 G。一般在G1和CMS中間選擇的話平衡點在6~8G,隻有記憶體比較大G1才能發揮優勢。

3.6 垃圾回收器總結

根據以上的垃圾回收器,我們分為以下幾種

  • 1.單線程垃圾回收器
  • 2.多線程并行垃圾回收器
  • 3.并發垃圾回收器
    JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器

    由上圖可見單線程垃圾回收器雖然落後,但是在特殊情況,ParNew,CMS和Parallel Scavenge這幾個垃圾回收器還是會自動跟他們配合使用。是以并沒有被淘汰。

    G1是可以同時回收新生代和老年代的垃圾回收器。

    JVM垃圾回收機制及算法1. 垃圾回收基礎2. 垃圾回收算法3. JVM 中常見的垃圾回收器