淺析大促備戰過程中出現的fullGc,我們能做什麼?

京東雲開發者發表於2023-03-08

作者:京東科技 白洋

前言:

背景:
為應對618、雙11大促,消費金融側會根據零售側大促節奏進行整體系統備戰。對核心流量入口承載的系統進行加固最佳化,排除系統風險,保證大促期間系統穩定。
由於大促期間,消費金融業務承擔著直面使用者,高併發,系統風險大機率直接造成資損風險等問題。 

在日常壓測和大促期間,經常會發生Jvm出現大量young Gc 和 部分full GC的情況,導致效能下降,可用率降低等情況。

之前對Jvm的垃圾回收機制不是很熟,如何避免和如何調優,基本上一竅不通,本文也是對自己學到的知識的一個鞏固~

一、什麼是JVM的GC?

JVM(Java Virtual Machine)。JVM 是 Java 程式的虛擬機器,是一種實現 Java 語言的直譯器。

它提供了一種獨立於作業系統的執行環境,使得 Java 程式在任何支援 JVM 的計算機上都可以執行。JVM 負責載入、驗證、解釋、執行和垃圾回收 Java 位元組程式碼,併為 Java 程式提供記憶體管理、執行緒管理和安全控制等服務。

JVM 中的 GC(Garbage Collection)是垃圾回收的縮寫,是 JVM 的記憶體管理機制。

Young GC 和 Full GC 是兩種不同的 GC 演算法。

Young GC:針對新生代物件的回收演算法,通常使用的是複製演算法或者標記整理演算法。因為新生代中的物件生命週期短,所以 Young GC 速度要比 Full GC 快得多。

Full GC:針對整個堆記憶體的回收演算法,用於回收那些在 Young GC 中沒有回收的存活物件。Full GC 速度比較慢,因為它需要掃描整個堆記憶體,因此對系統的效能影響較大。

所以在設計 Java 應用時,需要儘量減少 Full GC 的次數,以保證系統的效能。常見的方法包括擴大新生代的記憶體空間,減少陣列長度等。

以上基本是通用的對 Jvm 和 Gc 的解釋。但是可以明顯看出缺少一些細節,對我們來說還是沒什麼用,測試同學該如何理解具體的場景呢??

我們首先來理解young GC 的誕生過程:

首先, 理解複製演算法和標記整理演算法,它們是兩種不同的 Young GC 回收演算法。

複製演算法:將新生代記憶體分成兩個等大的部分,新建立的物件儲存在一個部分,而另一個部分用於儲存存活的物件。當新生代記憶體不夠用時,Young GC 會發生,將存活的物件複製到另一個記憶體區域。複製演算法不會導致記憶體碎片,但是會消耗一定的記憶體空間。

標記整理演算法:每次 Young GC 時,會先標記所有存活的物件,然後再將所有不存活的物件整理到一起。因此,記憶體碎片可能會導致空間浪費。標記整理演算法適用於需要保持記憶體空間整潔的應用,比如那些需要長時間執行的伺服器應用。

這個看看就好,本質上Young Gc可以理解成jvm正常的掃垃圾過程

根據上述的解釋,相信聰明的小夥伴可以清晰的看到,young Gc 有著更高的回收效率,對業務側的影響要小的多~因此,我們進一步來看看頭痛的full Gc,是怎麼來的?

Full GC 是 Full Garbage Collection 的縮寫,是指把整個堆記憶體掃描一遍,回收不再使用的物件並且整理記憶體的過程。由於堆記憶體的整體回收過程非常慢,因此,Full GC 可能導致應用程式的暫停。

如上所述,只有更合理的記憶體分配,避免不被使用的物件頻繁出現,調整堆記憶體的掃描時間。

full GC, 即全垃圾回收,是一種垃圾回收的過程 它會暫停所有的應用程式執行緒,對整個堆進行回收。 (這個太可怕了。。)

初始標記:首先,垃圾回收器標記出哪些物件是需要被回收的。

併發標記:然後,垃圾回收器將標記任務分配給多個執行緒,併發地執行標記任務。

重新標記:在併發標記的過程中,如果有新的物件被建立,需要對這些物件進行重新標記。

整理:接下來,垃圾回收器將沒有被標記的物件整理到記憶體的一端。

回收:最後,垃圾回收器回收被標記的物件,釋放記憶體。

來個圖大家看的明白一些~ Full Gc 的生命流程~ 本質上就是,垃圾太多,正常的活兒幹不了了,記憶體空間不夠了,得停下所有的事情,來一次大掃除

二、寫程式碼的時候能做什麼?

上述可得,fullGc是很可怕的,由於堆記憶體的整體回收過程非常慢,因此,Full GC 可能導致應用程式的暫停,直接就崩掉了。。。



要避免 Full GC 發生,本質上就需要對系統堆記憶體大小進行適當設定以及對程式碼進行最佳化,基本上有以下這些技巧:



•調整堆記憶體大小:確定合適的堆記憶體大小是避免 Full GC 發生的關鍵。

•對程式碼進行記憶體最佳化:使用不同的資料結構,避免記憶體洩漏,使用物件池等技巧。

•使用較大的新生代:新生代是儲存短生命週期物件的記憶體區域,更大的新生代可以減少 Full GC 的頻率。

•設定適當的垃圾回收演算法:使用 G1 GC 演算法等技術可以提高系統效能並減少 Full GC 的頻率。

•這些是避免 Full GC 發生的一些常見建議。請注意,每種情況都不同,所以要根據具體情況選擇適當的方法。

這些方法,看起來還是很抽象...我們來說點具體例子



首先,堆記憶體大小和垃圾回收演算法,不是我們能操作和關心的,業務側也一般不怎麼會調,交給運維同學了。 淺提一下,調整記憶體大小:透過調整 JVM 引數,如 -Xms、-Xmx 來適當增大記憶體。



具體我們能做到的,最主要的就是減少資料物件的生命週期:

透過使用弱引用、軟引用、虛引用等引用型別,可以在不需要資料物件時直接回收,從而避免 Full GC 。

減少資料物件的生命週期是指在程式中使用物件時,儘可能地縮短物件的存活時間。

這樣可以減少垃圾物件數量,降低Full GC的頻率。這是我們重點需要關注的!!

以下是一些具體的例子:

1. 避免使用不必要的臨時物件:

如果程式中有大量臨時物件,它們可能很快就會被垃圾回收器清理掉。因此,應該避免建立不必要的臨時物件,以減少物件的生命週期。

eg:

double average(double[] values) {
    double sum = 0;
    for (double value : values) {
        sum += value;
    }
    return sum / values.length;
}

在這個例子中,陣列values是臨時物件,在函式結束時會被銷燬。這樣,不必考慮如何刪除集合,以避免記憶體洩漏的風險。

還有,

String concatenate(List<String> strings) {
    String result = "";
    for (String str : strings) {
        result += str;
    }
    return result;
}

在這個例子中,每次迴圈都會建立一個臨時的字串物件,並將其附加到result中。隨著迴圈的進行,這些臨時物件可能會堆積,導致頻繁的GC操作。為了避免這個問題,可以使用Java中的StringBuilder來構建字串

String concatenate(List<String> strings) {
    StringBuilder result = new StringBuilder();
    for (String str : strings) {
        result.append(str);
    }
    return result.toString();
}

這樣的話,不再需要建立臨時字串物件,從而減少GC的次數。

2. 儘早釋放物件:當物件不再需要時,應該儘早將其釋放,以便及時回收它。例如,在程式完成處理後立即釋放物件,而不是等到下一次需要使用它之前。

比如我們日常最常用的for迴圈就很棒,

for (int i = 0; i < data.length; i++) {
  // do something with data[i]
}

在這個例子中,迴圈變數 i 只在迴圈中使用,並在迴圈結束後釋放。這樣做可以減少不必要的記憶體使用,從而減少全垃圾回收的次數。

另一個具體的例子是使用 try-with-resources 語句,這可以確保流等資源在不再使用後自動關閉,例如:

try (FileInputStream in = new FileInputStream("file.txt")) {
  // use the input stream
} catch (IOException e) {
  // handle exception
}

在這個例子中,檔案輸入流在不再使用後會被自動關閉,就不用手動關,這樣也會更合理~

3. 重複使用物件:如果可以,可以嘗試重複使用同一個物件,而不是頻繁地建立和銷燬新的物件。

這個比較好理解,比如同樣的事務流程,沒必要搞兩個變數 ~ 最少的變數幹最多的活兒是最理想的~

4. 使用物件池:

可以使用物件池,重複使用固定數量的物件,而不是不斷建立新的物件。這樣可以減少物件的生命週期,並降低Full GC的頻率。

使用物件池是一種常用的避免Full GC的方式。它的核心思想是重複利用已經建立好的物件,而不是每次都建立新的物件。

以下是一個簡單的物件池的程式碼例子:

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

public class ObjectPool {
  private static final int POOL_SIZE = 100;
  private static final List<Object> pool = new ArrayList<>(POOL_SIZE);
  static {
    for (int i = 0; i < POOL_SIZE; i++) {
      pool.add(new Object());
    }
  }
  public static Object getObject() {
    if (pool.isEmpty()) {
      return new Object();
    }
    return pool.remove(0);
  }
  public static void returnObject(Object object) {
    pool.add(object);
  }
}

在程式碼中,我們建立了一個大小為100的物件池,並在靜態程式碼塊中初始化了100個物件。當我們需要使用物件時,可以呼叫getObject方法,如果物件池中有剩餘的物件,就從物件池中取出一個物件;如果沒有剩餘物件,就新建一個物件。當不需要這個物件了,就可以呼叫returnObject方法,將物件放回物件池中。

這樣,我們可以重複利用已經建立好的物件,減少了物件的建立和銷燬的頻率,從而減少了Full GC的機率。

5. 使用弱引用:
在程式中,如果有大量物件不會再使用,可以使用弱引用來引用它們。 這個最多應用在型別快取這樣的場景,它們不是必須的物件,因此有些時候可以直接幹掉

這是一個弱引用的例子,這玩意兒還是比較抽象的。。。

import java.lang.ref.WeakReference;

public class WeakReferenceExample {
    public static void main(String[] args) {
        Object obj = new Object();
        WeakReference<Object> weakRef = new WeakReference<>(obj);

        obj = null;
        System.gc();
        if (weakRef.get() != null) {
            System.out.println("Object is still alive");
        } else {
            System.out.println("Object has been garbage collected");
        }
    }
}

舉個更具體點的:

import java.lang.ref.WeakReference;
import java.util.HashMap;

public class WeakReferenceExample {
  public static void main(String[] args) {
    HashMap<String, WeakReference<MyObject>> cache = new HashMap<>();
    MyObject obj = new MyObject("example");
    cache.put("example", new WeakReference<>(obj));
    obj = null;
    System.gc();
    System.out.println(cache.get("example").get());
  }

  static class MyObject {
    String name;

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

    @Override
    public String toString() {
      return "MyObject{" + "name='" + name + ''' + '}';
    }
  }
}

這個例子中,我們將一個MyObject物件封裝在弱引用中,並儲存在HashMap快取中,當我們顯式呼叫System.gc()方法時,JVM會嘗試回收這些不再使用的物件,如果記憶體不足,則會回收MyObject物件,那麼cache.get("example").get()返回的將是null。

三、測試能做啥

回顧全文,其實我們能做的真不多,只能在業務程式碼測試的過程中,關注物件的使用頻次,拒絕無效的引用或new一大堆沒必要的物件。

具體手段

定期監測 GC 日誌:透過我們的jvm關注,大專案上線後,或程式碼改動特別大的專案上線後,做一下讀寫壓測的操作~ 關注我們的jvm, jvm監測地址

資料結構最佳化: 根據上述的手段,測試開發工程師可以透過上述手段,來最佳化資料結構來減小資料物件的生命週期,從而避免 Full GC。在測試過程中,關注一下資料結構的合理性~

關注單元測試:透過執行研發的單元測試,或自己手動寫一個,模擬實際的記憶體使用情況,來評估記憶體的使用情況(基本上,目前的業務程式碼能跑起來,大機率是沒問題的,,)

總結:

日常的業務程式碼測試,對記憶體的敏感度要高一些,沒bug不一定不會出問題,現在我們的系統是成熟的可靠的,但是面對大促的壓力,如果能提前解決隱患,幹掉有風險的記憶體使用,也是節省我們壓測時的工作量嘛~

相關文章