1. 引言
Spring Boot 允許我們創建具備自動配置和啓動依賴等功能的生產級應用程序。 然而,Spring Boot 應用程序最常見的抱怨之一是其內存佔用量。 即使是一個基本的 Spring Boot 應用程序,使用嵌入式服務器的情況下,啓動時也會消耗 150MB 的內存。
在本教程中,我們將探討內存佔用量為何會發生,並研究如何在不影響應用程序功能的情況下減少內存使用量。
2. Spring Boot 為什麼消耗如此多的內存?
以下幾個因素影響了內存的消耗。 我們將在本節中探討其中一些最重要的因素。
2.1. JVM 架構
64 位 JVM 中的對象引用比 32 位 JVM 中的對象引用大兩倍,這是設計決定的。例如,相同的應用程序可能在 32 位 JVM 上使用 110MB,而在 64 位 JVM 上使用 190MB。
此外,JVM 本身也會消耗內存用於 JIT 編譯器、代碼緩存、類元數據、內部結構、線程堆棧等。即使我們將最大堆大小設置為 64MB,使用 –Xmx64m 選項,總內存使用量通常會更高。
2.2. 內嵌服務器線程
Spring Boot 運行一個內嵌服務器,例如 Tomcat 或 Jetty。默認情況下,Tomcat 會創建 200 個工作線程,每個線程消耗 1MB 的堆棧空間(在 64 位 JVM 中)。即使沒有請求到達服務器,Tomcat 本身也會至少消耗 200MB 的內存。
2.3. 框架開銷
Spring Boot 在底層執行了大量的複雜操作,包括自動配置、依賴注入和代理等功能。所有這些功能都需要元數據和內存中的緩存對象。
3. 設置 JVM 選項
JVM 允許我們設置各種選項,以幫助優化內存使用情況。 在本節中,讓我們來研究一些這些設置。
3.1. 使用串行垃圾收集器
每個Java應用程序都會創建短生命週期的對象,這些對象存在於堆內存中。JVM定期運行垃圾收集過程,清理不再使用的對象。
JVM有三種不同的垃圾收集器類型,它們在速度、內存和CPU使用率方面有所不同。默認情況下,現代JVM會選擇多線程垃圾收集器,它具有高吞吐量,非常適合具有許多CPU核心的大型應用程序。但對於小型應用程序來説,這有點過度。
如果我們的應用程序很小,例如,在有限的內存限制下運行的Spring Boot應用程序,那麼使用串行垃圾收集器會更合適,因為它使用更少的後台線程,從而降低了內存需求。
要啓用串行垃圾收集器,可以在啓動Spring Boot應用程序時使用<em>-XX:+UseSerialGC</em>選項,如下所示:
java -XX:+UseSerialGC -jar myapp.jar3.2. 減少線程堆棧大小
當 JVM 啓動線程時,會為其分配堆棧內存。堆棧大小決定了它可以處理的嵌套方法調用數量。
默認情況下,JVM 為每個線程分配 1MB 的內存。如果存在 1000 個線程,即使應用程序處於空閒狀態,也需要分配 1GB 的內存。對於小型應用程序來説,1MB 的默認堆棧大小是浪費的內存。
幸運的是,JVM 允許我們使用 -Xss 選項來控制堆棧大小。
如果我們的應用程序的遞歸方法調用不深,如大多數 Spring Boot 應用程序,我們可以將其大小減少到 512KB,而不是默認的 1MB。當有數百個線程運行時,這種節省會迅速累積。
以下是一個示例用法,其中我們使用輕量級 GC,每個線程使用 512KB 的內存:
java -Xss512k -XX:+UseSerialGC -jar myapp.jar3.3. 限制最大內存使用量
當 JVM 啓動時,它會檢查可用的內存,並根據這些信息確定堆大小和非堆內存空間。如果我們的筆記本電腦有 16GB 的 RAM,JVM 會認為有充足的內存可用。
但是,如果在 Docker 容器中部署相同的應用程序,該容器可能僅允許 100 到 200MB 的內存。問題在於 JVM 不一定總是知道容器的限制,它可能會認為可以利用比允許使用的更多的內存。這甚至可能導致應用程序崩潰。
幸運的是,我們可以使用 -XX:MaxRAM 選項設置內存限制。例如,-XX:MaxRAM=72m 將所有內存的硬限制設置為 72MB,允許 JVM 根據需要分配內存。
以下是設置總內存為 72MB 的用法,其中每個線程堆棧大小為 512KB,並使用串行垃圾收集器:
java -XX:MaxRAM=72m -Xss512k -XX:+UseSerialGC -jar myapp.jar4. 減少 Web 服務器線程
當啓動帶有嵌入式 Tomcat 服務器的 Spring Boot 應用程序時,它使用線程池來處理傳入的請求。每個請求將由一個線程處理。 默認情況下,線程池包含 200 個工作線程,對於較小的應用程序來説可能過於冗餘。
這些線程並不只是空閒地等待,它們會消耗堆棧內存並膨脹內存,這在低內存環境中(如 Docker 或免費雲層)是不理想的,這些環境通常具有資源限制。
幸運的是,我們有一種方法可以減少線程池的大小。我們可以通過在應用程序的 application.properties 或 application.yml 文件中進行一個小修改來實現。
下面的設置將限制 Tomcat 服務器使用 20 個工作線程,這對於小型應用程序來説非常合適:
server.tomcat.max-threads=205. 容器友好實踐
當我們將應用程序部署在 Docker 容器內時,我們通常會設置 CPU 和內存使用限制。如果應用程序超出這些限制使用資源,該應用程序將被立即終止。這種行為通常被稱為“內存耗盡終止”(OOMKill)。
在本文中,讓我們來探討一些容器友好的實踐。
5.1. 顯式設置容器資源限制
啓動容器時,最好始終明確定義其內存限制,這是一種最佳實踐。
例如,以下命令將以最多 128MB 的內存分配啓動容器。如果 JVM 嘗試分配超過 128MB 的內存,容器將被終止:
docker run -m 128m my-spring-boot-app5.2. 將 JVM 標誌與容器限制匹配
即使我們設置了容器的內存限制,JVM 仍然可能認為它有權訪問 GB 級別的 RAM。為了解決這個問題,我們需要明確地告知 JVM 可以使用多少內存,通過使用 -XX:MaxRAMPercentage 選項來實現。
以下是帶有示例命令的命令:此命令將以最大 128 MB 的內存啓動容器,其中 JVM 可以使用 75% 的內存,即 96 MB。我們還使用了 SerialGC,這進一步節省了內存:
docker run -m 128m openjdk:17-jdk java -XX:MaxRAMPercentage=75.0 -XX:+UseSerialGC -Xss512k -jar myapp.jar6. 其他優化技術
除了我們之前討論的策略之外,還可以考慮其他優化技術以實現高效的內存使用。一種直接的方法是移除任何未使用的 starter 依賴項。每個 starter 都引入了額外的依賴項和初始化代碼,從而消耗內存。
另一個實用方法是禁用我們不使用的緩存。當我們不使用諸如 EhCache 或 Caffeine 之類的緩存框架時,最好不要包含它們,因為它們通常會將數據存儲在內存中,這會迅速增加內存佔用並膨脹內存佔用量。
最後,如果可行,我們還可以考慮切換到 32 位 JVM。如果我們的應用程序較小並且需要小於 1.5 GB 的堆空間,那麼使用 32 位 JVM 可以顯著降低內存開銷。
7. 避免過度優化
雖然我們討論過的策略降低了整體內存佔用,但過度優化可能會導致意外問題,並且成本也可能很高。
在優化之前,必須瞭解我們的應用程序及其未來的擴展需求。有時,應用程序可能確實需要大量的內存,這使得優化空間有限,而應該增加資源。
目標不是減少內存使用並降低成本,而是根據應用程序的需求,以最佳方式使用內存,並防止由於內存不足而導致的問題。
8. 結論
Spring Boot 應用通常比純 Java 應用消耗更多的內存,這是由於嵌入式服務器、框架特性和 JVM 使用造成的。
然而,通過調整 JVM 選項、減少服務器線程、精簡依賴項以及在啓動時設置容器感知標誌,我們可以顯著降低內存使用量。
雖然過度優化不建議採用,但我們討論的策略將使應用程序更高效、更適合雲環境。