Posted by Adam on August 24, 2022
這是一個非常經典的效能優化問題。當多個 Thread 同時向 JVM 請求大量記憶體來存放臨時物件(例如 `List<Entity>`)時,會迅速填滿 **Young Generation** 並觸發頻繁的 **Minor GC**;如果物件太大或存活時間稍長,進入 **Old Generation** 後觸發的 **Full GC** 就會導致嚴重的 **Stop-The-World (STW)**,讓系統卡頓。 針對您的情況,建議從「減少記憶體分配」與「優化回收策略」兩個維度來解決: --- ### 1. 根本解決:從「批次加載」轉為「流式處理 (Streaming)」 導致 GC 壓力大的主因通常是 `findAll()` 或 `List` 把數萬筆資料一次性塞進 Heap。您可以改用 **JPA Stream**,讓資料像流水一樣進來,處理完一筆就丟掉一筆,這樣記憶體中永遠只會佔用極少量的空間。 **Repository 寫法:** ```java public interface BigDataRepository extends JpaRepository<MyEntity, Long> { // 使用 Stream 讀取,避免一次加載所有資料到記憶體 @Query("select e from MyEntity e") Stream<MyEntity> streamAllBy(); } ``` **Service 處理方式:** ```java @Transactional(readOnly = true) // Stream 必須在 Transaction 內執行 public void processBigData() { try (Stream<MyEntity> entityStream = repository.streamAllBy()) { entityStream .filter(e -> e.isValid()) // 在這裡做過濾處理 .forEach(e -> { // 執行業務邏輯,處理完後 e 就會變成可回收狀態 }); } } ``` * **優點:** 記憶體佔用極低,物件在 Young Gen 很快就會被回收,不會堆積到 Old Gen。 --- ### 2. 將過濾邏輯下推至資料庫 (Push-down) 如果您在 Java 內做過濾,代表您已經把「垃圾資料」從 DB 搬到了 JVM 內。 * **做法:** 盡量使用 `WHERE` 子句或 `Specification` 在 SQL 層級就把資料過濾掉。 * **效果:** 減少資料傳輸量(Network I/O)以及 JVM 創建實體類別(Entity Object)的負擔。 --- ### 3. 分批次處理 (Batch Processing) 如果您必須進行複雜的關聯計算,無法使用 Stream,請使用分頁(Pagination)或批次讀取。 ```java int pageSize = 500; Page<MyEntity> page = repository.findAll(PageRequest.of(0, pageSize)); while (page.hasContent()) { process(page.getContent()); // 處理這 500 筆 // 關鍵:處理完後手動提示或是確保 List 引用失效 page = repository.findAll(page.nextPage() ); } ``` --- ### 4. 更換 JVM 垃圾回收器 如果您的 Java 版本較新(JDK 11+ 或 JDK 17),預設的 G1GC 在處理大對象時仍可能有停頓。您可以考慮改用 **ZGC (Z Garbage Collector)**。 * **特性:** ZGC 的停頓時間(STW)通常控制在 **1ms** 以下,且不會隨著 Heap 大小增加而增加。 * **參數:** `-XX:+UseZGC` --- ### 5. 關於變數存儲的建議 您提到的「不能存 Service property」是完全正確的。為了在同一個 Request 的不同層級共享資料,但不影響執行緒安全,除了傳參,您有兩個專業選擇: * **ThreadLocal:** 在同一個 Thread 內共享變數。但要注意在高併發下,ThreadLocal 若沒清理會導致記憶體洩漏。 * **Request Scope Bean:** 如果是 Spring Web 環境,可以定義一個 `@Scope("request")` 的 Bean,Spring 會確保每個請求有一個獨立的實例。 ### 總結建議 1. **首選:** 將 `List` 改為 `Stream`,這是解決 GC 頻繁回收大量臨時物件的最有效手段。 2. **次選:** 檢查 SQL,確保過濾邏輯在 DB 完成。 3. **環境優化:** 如果硬體資源允許,將 GC 改為 ZGC 以消滅停頓感。 **您想了解如何將現有的過濾邏輯轉換成 SQL `Specification` 或 `Query` 嗎?這樣可以從源頭減少資料進入 Java。**