如何減少 Spring Boot 記憶體使用量?
1. 簡介
Spring Boot 讓我們能夠建立具有自動配置和啟動器相依性等功能的生產就緒應用程式。然而,Spring Boot 應用程式最常見的問題之一是其記憶體佔用。即使是具有嵌入式伺服器的基本 Spring Boot 應用程式在啟動時也會消耗 150 MB 記憶體。
在本教程中,我們將探討發生這種情況的原因,並研究在不影響應用程式功能的情況下減少記憶體使用的方法。
2. Spring Boot 為什麼佔用這麼多記憶體?
有幾個因素會影響記憶體消耗。本節我們將探討一些最重要的因素。
2.1. JVM 架構
64 位元 JVM 中的物件參考大小是 32 位元 JVM 中物件參考大小的兩倍。這是設計使然。例如,同一個應用在 32 位元 JVM 上可能使用 110 MB 內存,而在 64 位元 JVM 上則可能使用 190 MB 內存。
此外,JVM 本身也會消耗 JIT 編譯器、程式碼快取、類別元資料、內部結構、執行緒堆疊等記憶體。即使我們使用–Xmx64m
將最大堆大小設為 64 MB,總記憶體使用量通常也會高得多。
2.2. 嵌入式伺服器執行緒
Spring Boot 運行嵌入式伺服器,例如 Tomcat 或 Jetty。預設情況下,Tomcat 會建立 200 個工作線程,每個線程佔用 1 MB 的堆疊空間(在 64 位元 JVM 中)。即使沒有對伺服器的請求,Tomcat 本身也會消耗至少 200 MB 的記憶體。
2.3. 框架開銷
Spring Boot 在底層完成了許多繁重的工作,包括自動配置、依賴注入、代理等等。所有這些功能都需要記憶體中的元資料和快取物件。
3.設定JVM選項
JVM 讓我們可以設定各種有助於改善記憶體使用率的選項。在本節中,我們將研究其中的一些設定。
3.1. 使用串列垃圾收集器
每個 Java 應用程式都會創建一些在應用程式中使用的短暫物件。這些物件位於堆記憶體中。 JVM 會定期執行垃圾回收程序來清理不再使用的物件。
JVM 有三種不同類型的垃圾收集器,每種類型在速度、記憶體和 CPU 使用率方面各不相同。現代 JVM 預設選擇多執行緒垃圾收集器,它提供高吞吐量,非常適合具有多 CPU 核心的大型應用程式。但對於小型應用程式來說,多執行緒垃圾收集器就有點過了。
如果我們的應用程式很小,例如在嚴格記憶體限制下運行的 Spring Boot 應用程序,那麼切換到串行垃圾收集器更有意義,它使用更少的後台線程,從而需要更少的記憶體。
要啟用串行垃圾收集器,我們可以在啟動 Spring Boot 應用程式時使用-XX:+UseSerialGC
選項,如下所示:
java -XX:+UseSerialGC -jar myapp.jar
3.2. 減少執行緒堆疊大小
JVM 啟動執行緒時,會為其分配堆疊記憶體。堆疊大小決定了執行緒可以處理的巢狀方法呼叫數量。
預設情況下,JVM 為每個執行緒分配 1MB 記憶體。如果有 1000 個線程,即使應用程式處於空閒狀態,也需要 1GB 記憶體。對於小型應用程式來說,每個線程 1MB 記憶體是浪費的。
幸運的是,JVM 允許我們使用-Xss
選項來控制堆疊大小。
如果我們的應用的遞歸方法呼叫不是很深(就像大多數 Spring Boot 應用一樣),我們可以將大小減小到 512 KB,而不是預設的 1 MB。當我們運行數百個執行緒時,這種節省會迅速累積起來。
以下是我們使用輕量級 GC 的範例用法,其中每個執行緒使用 512 KB 記憶體:
java -Xss512k -XX:+UseSerialGC -jar myapp.jar
3.3. 限制最大 RAM
JVM 啟動時會檢查可用內存,並據此確定堆大小和非堆空間。如果我們的筆記型電腦有 16 GB 的 RAM,JVM 會假定它有足夠的可用記憶體。
但是,如果我們將同一個應用程式部署在 Docker 容器中,則該容器可能只允許使用 100 到 200 MB 的記憶體。問題在於,JVM 並不總是了解容器的限制,它可能會認為自己可以使用超出允許範圍的記憶體。這甚至可能導致應用程式崩潰。
幸運的是,我們可以使用-XX:MaxRAM
選項來設定記憶體上限。例如, XX:MaxRAM=72m
將所有記憶體的硬上限設為 72 MB,從而允許 JVM 根據需要分配記憶體。
以下是我們將總記憶體設定為 72 MB、每個執行緒堆疊大小為 512 KB 的使用情況,我們使用串列垃圾收集器:
java -XX:MaxRAM=72m -Xss512k -XX:+UseSerialGC -jar myapp.jar
4.減少Web伺服器線程
當我們啟動一個具有嵌入式 Tomcat 伺服器的 Spring Boot 應用程式時,它會使用執行緒池處理傳入的請求。每個請求將由一個線程處理。預設情況下,線程池將包含 200 個工作線程,這對於較小的應用程式來說可能有些過度。
這些線程不僅僅是處於空閒狀態;它們還會消耗堆疊記憶體並使記憶體膨脹,這在 Docker 或免費雲層等低記憶體環境中是不可取的,因為這些環境通常具有資源限制。
幸運的是,我們有辦法減少線程池的大小。我們可以在應用程式中的application.properties
或application.yml
檔案中做一些小的修改來實現這一點。
以下設定將限制 Tomcat 伺服器使用 20 個工作線程,這對於小型應用程式來說是完美的:
server.tomcat.max-threads=20
5. 容器友善實踐
當我們將應用程式部署到 Docker 容器時,我們通常會設定 CPU 和記憶體使用量的限制。如果應用程式使用的資源超出了這些限制,則該應用程式將立即終止。這通常被稱為「記憶體不足終止」(OOMKill)。
在本節中,讓我們研究一些容器友善實踐。
5.1. 明確設定容器限制
當我們啟動容器時,始終定義其記憶體限制是一種很好的做法。
例如,以下命令將啟動一個最多分配 128 MB 記憶體的容器。如果 JVM 嘗試分配超過 128 MB 的內存,容器將被終止:
docker run -m 128m my-spring-boot-app
5.2. 將 JVM 標誌與容器限制相符
即使我們設定了容器的記憶體上限,JVM 可能仍然會認為它可以使用數 GB 的 RAM。為了解決這個問題,我們需要使用-XX:MaxRAMPercentage
選項明確告知 JVM 它可以使用多少記憶體。
下面是帶有範例用法的命令。此命令將啟動最大記憶體為 128 MB 的容器,其中 JVM 可以使用其中的 75%,即 96 MB。我們還使用了 SerialGC,這進一步節省了記憶體:
docker run -m 128m openjdk:17-jdk java -XX:MaxRAMPercentage=75.0 -XX:+UseSerialGC -Xss512k -jar myapp.jar
6. 其他優化技術
除了我們目前討論過的策略之外,還可以考慮其他最佳化技術來提高記憶體使用率。一種簡單的方法是移除所有未使用的啟動程序相依性。每個啟動程序都會引入額外的依賴項和初始化程式碼,這會消耗記憶體。
另一個實用的方法是停用應用程式中不使用的快取。如果我們不使用 EhCache 或 Caffeine 之類的快取框架,最好不要添加它們,因為它們通常會將資料儲存在記憶體中,這會迅速累積並佔用過多的記憶體空間。
最後,如果可行的話,我們也可以考慮切換到 32 位元 JVM。如果我們的應用程式很小,並且所需的堆空間小於 1.5 GB,那麼使用 32 位元 JVM 可以顯著減少記憶體開銷。
7.避免過度優化
雖然我們討論的策略減少了整體記憶體佔用,但過度優化可能會導致意外問題並且代價高昂。
在進行最佳化之前,請務必了解應用程式的需求和未來的規模。有時,應用程式可能確實需要充足的內存,此時優化空間不大,反而需要增加資源。
這樣做的目的不是減少內存使用量和降低成本,而是根據應用程式需求最佳地使用內存,並潛在地防止任何與內存不足相關的問題。
8. 結論
由於嵌入式伺服器、框架功能和 JVM 的使用,Spring Boot 應用程式通常比普通 Java 應用程式使用更多的記憶體。
但是,透過調整 JVM 選項、減少伺服器執行緒、修剪依賴項以及在啟動期間設定容器感知標誌,我們可以大幅減少記憶體使用量。
雖然不建議過度優化,但我們討論的策略將使應用程式更有效率、更適合雲端。