iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
自我挑戰組

我的Java自學之路:一個轉職者的30篇技術統整系列 第 18

Java虛擬機器:垃圾回收機制與演算法

  • 分享至 

  • xImage
  •  

1. 引言

垃圾回收機制自動管理Java程式的記憶體,釋放開發者處理記憶體分配和回收的負擔,大幅提升了開發效率和程式的穩定性。然而,要充分發揮Java的效能優勢,深入理解GC的運作原理和各種演算法就顯得尤為重要。

2. 垃圾回收基本概念

垃圾回收(Garbage Collection,GC)是Java虛擬機器中自動管理記憶體的機制,主要任務是識別並刪除不再被程式使用的物件,釋放這些物件佔用的記憶體空間。

GC的主要目標包括:

  1. 自動記憶體管理:開發者無需手動分配和釋放記憶體。
  2. 提高記憶體使用效率:及時回收不再使用的物件,避免記憶體洩漏。
  3. 簡化程式開發:減少因記憶體管理不當導致的錯誤。

GC的運作基於一個重要概念:GC Root。GC Root是一組特殊的參考,被視為程式執行的起點。常見的GC Root包括:

  • 執行中的執行緒堆疊中的區域變數和參數
  • 靜態變數
  • JNI(Java Native Interface)參考

GC通過追蹤這些Root,找出所有可達(reachable)的物件,其餘不可達的物件則被視為垃圾,可以被回收。

物件如何成為垃圾:

public class GCDemo {
    public static void main(String[] args) {
        Object obj1 = new Object();
        Object obj2 = new Object();
        
        obj1 = null; // obj1指向的物件現在成為垃圾
        obj2 = obj1; // obj2指向的物件也成為垃圾
    }
}

3. Java記憶體區域與GC

在Java虛擬機器中,記憶體被劃分為幾個不同的區域,其中與垃圾回收最密切相關的是堆(Heap)。
堆是Java程式執行期間最大的一塊記憶體,幾乎所有的物件實例都在這裡分配。
為了提高GC效率,堆通常被劃分為幾個部分:

  1. 新生代(Young Generation):

    • Eden區:大多數新建立的物件首先被分配在這裡。
    • Survivor區:分為From和To兩個區域,用於存放經過垃圾回收後仍然存活的物件。
  2. 老年代(Old Generation):

    • 存放長期存活的物件和大型物件。
    • 當物件在新生代中經過多次GC仍然存活,就會被移到老年代。
  3. 永久代(PermGen,Java 8之前)/ 元空間(Metaspace,Java 8及之後):

    • 存放類別資訊、常量、靜態變數等。
    • Java 8將永久代移除,改用本地記憶體實現的元空間。

不同物件可能被分配到不同的記憶體區域:

public class MemoryAllocationDemo {
    public static void main(String[] args) {
        // 小物件,可能在Eden區分配
        Object smallObject = new Object();

        // 大型陣列,可能直接在老年代分配
        byte[] largeArray = new byte[1024 * 1024 * 10]; // 10MB

        // 類別資訊存儲在元空間
        Class<?> clazz = MemoryAllocationDemo.class;
    }
}

不同的記憶體區域有不同的GC策略:

  • 新生代:頻繁進行GC,主要使用複製算法。
  • 老年代:GC頻率較低,主要使用標記-清除或標記-壓縮算法。
  • 元空間:通常不需要經常GC,但如果元空間耗盡,也會觸發完整GC。

4. 垃圾回收演算法

Java虛擬機器中的垃圾回收演算法經過多年發展,形成幾種主要的策略。每種演算法都有其特點和適用場景,了解這些演算法有助於我們更好地理解GC的工作原理。

  1. 標記-清除(Mark-Sweep)演算法:

    • 標記階段:從GC Root開始遍歷所有可達的物件,並進行標記。
    • 清除階段:遍歷整個堆,回收未被標記的物件。
    • 優點:實現簡單。
    • 缺點:效率不高,可能產生大量記憶體碎片。
  2. 複製(Copying)演算法:

    • 將可用記憶體劃分為兩塊,每次只使用其中一塊。
    • GC時,將存活物件複製到另一塊,然後清理當前使用的記憶體區域。
    • 優點:效率高,沒有碎片。
    • 缺點:可用記憶體減半。
  3. 標記-壓縮(Mark-Compact)演算法:

    • 標記階段:與標記-清除相同。
    • 壓縮階段:將所有存活的物件向一端移動,然後清理邊界以外的記憶體。
    • 優點:沒有碎片,可以充分利用記憶體。
    • 缺點:效率較低,需要移動物件。
  4. 分代收集(Generational Collection)演算法:

    • 基於大多數物件都是短命的觀察結果。
    • 將堆分為新生代和老年代,對不同代採用不同的回收算法。
    • 新生代:使用複製算法,因為大部分物件會死亡。
    • 老年代:使用標記-清除或標記-壓縮算法。

不同生命週期的物件如何影響GC策略:

import java.util.ArrayList;
import java.util.List;

public class GCAlgorithmDemo {
    public static void main(String[] args) {
        // 短命物件,可能在新生代中被快速回收
        for (int i = 0; i < 1000000; i++) {
            Object obj = new Object();
        }

        // 長期存活的物件,可能被移到老年代
        List<String> longLivedList = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            longLivedList.add("長期存活的物件 " + i);
        }

        // 觸發GC
        System.gc();
    }
}

在這個例子中,大量短命的Object實例可能會觸發新生代的GC,而長期存活的ArrayList可能會被移到老年代。

5. Java垃圾收集器類型

Java虛擬機器提供了多種垃圾收集器,每種都有其特定的使用場景和優勢。以下是幾種主要的垃圾收集器:

  1. Serial收集器:

    • 單執行緒收集器,適用於單CPU環境。
    • 在進行垃圾收集時,會暫停所有的應用執行緒(Stop The World,STW)。
    • 簡單高效,適合客戶端應用程式。
  2. Parallel收集器:

    • 多執行緒收集器,適用於多核心處理器。
    • 新生代使用複製算法,老年代使用標記-壓縮算法。
    • 目標是達到可控制的吞吐量。
  3. CMS(Concurrent Mark Sweep)收集器:

    • 以獲取最短回收停頓時間為目標。
    • 採用標記-清除算法。
    • 分為初始標記、並發標記、重新標記、並發清除四個階段。
    • 適合對回應時間要求較高的應用。
  4. G1(Garbage First)收集器:

    • 面向服務端應用的收集器。
    • 將堆劃分為多個大小相等的區域(Region)。
    • 優先收集垃圾最多的區域,實現高效回收。
    • 可預測的停頓時間模型。
  5. ZGC(Z Garbage Collector):

    • Java 11中引入的低延遲垃圾收集器。
    • 目標是將STW時間控制在10ms以內。
    • 使用著色指針和讀屏障技術。
    • 適合大記憶體、低延遲應用。

在Java程式中指定使用特定的垃圾收集器:

public class GCTypeDemo {
    public static void main(String[] args) {
        // 使用G1收集器
        // -XX:+UseG1GC
        
        // 使用CMS收集器
        // -XX:+UseConcMarkSweepGC
        
        // 使用Parallel收集器
        // -XX:+UseParallelGC
        
        // 使用ZGC(Java 11+)
        // -XX:+UseZGC

        // 創建大量物件以觸發GC
        for (int i = 0; i < 1000000; i++) {
            new Object();
        }

        // 手動觸發GC(僅用於演示,實際應用中應避免)
        System.gc();
    }
}

要使用特定的收集器,可以在啟動Java應用程式時加上相應的JVM參數。

在實際應用中,應根據應用程式的特性(如記憶體大小、延遲要求、吞吐量需求等)來選擇最適合的收集器。

6. GC調校與最佳實踐

垃圾回收的效能對Java應用程式的整體效能有重大影響。因此,了解如何監控、分析和調校GC是非常重要的。以下是一些GC調校的關鍵點和最佳實踐:

  1. GC監控與分析工具:

    • jstat:JVM統計監控工具
    • jconsole:Java監控和管理控制台
    • VisualVM:視覺化監控、分析工具
    • GC日誌:使用-Xloggc參數啟用
  2. 常見GC調校參數:

    • -Xms和-Xmx:設置堆的初始和最大大小
    • -XX:NewRatio:新生代和老年代的比例
    • -XX:SurvivorRatio:Eden區和Survivor區的比例
    • -XX:MaxGCPauseMillis:設置最大GC停頓時間
  3. GC最佳實踐建議:

    • 適當設置堆大小:過大或過小都可能影響效能
    • 選擇合適的GC收集器:根據應用特性選擇
    • 減少物件創建:重用物件,使用物件池
    • 及時釋放不用的物件:將引用設為null
    • 使用弱引用或軟引用:對於可有可無的緩存數據
    • 避免使用終結器(finalizers):會延遲對象回收

如何在程式中使用軟引用來實現一個簡單的緩存:

import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;

public class SoftReferenceCache<K, V> {
    private final Map<K, SoftReference<V>> cache = new HashMap<>();

    public V get(K key) {
        SoftReference<V> ref = cache.get(key);
        if (ref != null) {
            V value = ref.get();
            if (value != null) {
                return value;
            } else {
                cache.remove(key);
            }
        }
        return null;
    }

    public void put(K key, V value) {
        cache.put(key, new SoftReference<>(value));
    }
}

在進行GC調校時,重要的是要根據應用程式的具體需求和運行環境來進行優化。同時,應該謹慎進行調校,每次修改後都要進行充分的測試和監控,以確保調校確實帶來了效能改善。

本篇文章同步刊載: JYI.TW
筆者個人的網站: JUNYI


上一篇
Java虛擬機器:JVM類別載入機制
下一篇
Java虛擬機器:JVM調校與效能最佳化
系列文
我的Java自學之路:一個轉職者的30篇技術統整30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言