揭秘SpringBoot内存占用:150MB瘦身实战指南

由于嵌入式服务器、框架功能和 JVM 的使用,Spring Boot 应用程序通常比普通 Java 应用程序使用更多的内存。但是,通过调整 JVM 选项、减少服务器线程、修剪依赖项以及在启动期间设置容器感知标志,我们可以显著减少内存使用量。虽然不建议过度优化,但我们讨论的策略将使应用程序更高效、更适合云。

Spring Boot 使我们能够创建具有自动配置和启动器依赖项等功能的生产就绪应用程序。然而,Spring Boot 应用程序最常见的问题之一是其内存占用。即使是带有嵌入式服务器的基本 Spring Boot 应用程序在启动时也会消耗 150 MB 内存。

在本教程中,我们将探讨发生这种情况的原因,并研究在不影响应用程序功能的情况下减少内存使用的方法。

Spring Boot 为什么占用这么多内存?
有几个因素会影响内存消耗。本节我们将探讨一些最重要的因素。

JVM 架构
64 位 JVM 中的对象引用大小是 32 位 JVM 中对象引用大小的两倍。这是设计使然。例如,同一个应用在 32 位 JVM 上可能使用 110 MB 内存,而在 64 位 JVM 上则可能使用 190 MB 内存。

此外,JVM 本身也会消耗 JIT 编译器、代码缓存、类元数据、内部结构、线程堆栈等内存。即使我们使用 –Xmx64m 将最大堆大小设置为 64 MB ,总内存使用量通常也会高得多。

嵌入式服务器线程
Spring Boot 运行嵌入式服务器,例如 Tomcat 或 Jetty。默认情况下,Tomcat 会创建 200 个工作线程,每个线程占用 1 MB 的堆栈空间(在 64 位 JVM 中)。即使没有对服务器的请求,Tomcat 本身也会消耗至少 200 MB 的内存。

 框架开销
Spring Boot 在底层完成了很多繁重的工作,包括自动配置、依赖注入、代理等等。所有这些功能都需要内存中的元数据和缓存对象。

设置JVM选项
JVM 允许我们设置各种有助于改善内存使用率的选项。在本节中,我们将研究其中的一些设置。

使用串行垃圾收集器
每个 Java 应用程序都会创建一些在应用程序中使用的短暂对象。这些对象位于堆内存中。JVM 会定期运行垃圾回收进程来清理不再使用的对象。

JVM 有三种不同类型的垃圾收集器,每种类型在速度、内存和 CPU 使用率方面各不相同。现代 JVM 默认选择多线程垃圾收集器,这种垃圾收集器吞吐量高,非常适合具有多 CPU 核心的大型应用程序。但对于小型应用程序来说,这种垃圾收集器就有点过了。

如果我们的应用程序很小,比如在严格内存限制下运行的 Spring Boot 应用程序,那么切换到串行垃圾收集器更有意义,它使用更少的后台线程,从而需要更少的内存。

要启用串行垃圾收集器,我们可以在启动 Spring Boot 应用程序时使用-XX:+UseSerialGC选项,如下所示:

java -XX:+UseSerialGC -jar myapp.jar

减少线程堆栈大小
JVM 启动线程时,会为其分配堆栈内存。堆栈大小决定了线程可以处理的嵌套方法调用数量。

默认情况下,JVM 为每个线程分配 1MB 内存。如果有 1000 个线程,即使应用程序处于空闲状态,也需要 1GB 内存。对于小型应用程序来说,每个线程 1MB 内存是浪费的。

幸运的是,JVM 允许我们使用-Xss选项来控制堆栈大小。

如果我们的应用的递归方法调用不是很深(就像大多数 Spring Boot 应用一样),我们可以将大小减小到 512 KB,而不是默认的 1 MB。当我们运行数百个线程时,这种节省会迅速累积起来。

下面是我们使用轻量级 GC 的示例用法,其中每个线程使用 512 KB 内存:

java -Xss512k -XX:+UseSerialGC -jar myapp.jar

限制最大 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


减少Web服务器线程
当我们启动一个带有嵌入式 Tomcat 服务器的 Spring Boot 应用程序时,它会使用线程池处理传入的请求。每个请求将由一个线程处理。默认情况下,线程池将包含 200 个工作线程,这对于较小的应用程序来说可能有些过度。

这些线程不仅仅是处于空闲状态;它们还会消耗堆栈内存并使内存膨胀,这在 Docker 或免费云层等低内存环境中是不可取的,因为这些环境通常具有资源限制。

幸运的是,我们有办法减少线程池的大小。我们可以在应用程序中的application.properties或application.yml文件中做一些小的修改来实现这一点。

以下设置将限制 Tomcat 服务器使用 20 个工作线程,这对于小型应用程序来说是完美的:

server.tomcat.max-threads=20

容器友好实践
当我们将应用程序部署到 Docker 容器中时,我们通常会设置 CPU 和内存使用量的限制。如果应用程序使用的资源超出了这些限制,应用程序将被立即终止。这通常被称为“内存不足终止”(OOMKill)。

在本节中,让我们研究一些容器友好型实践。

明确设置容器限制
当我们启动容器时,始终定义其内存限制是一种很好的做法。

例如,以下命令将启动一个最多分配 128 MB 内存的容器。如果 JVM 尝试分配超过 128 MB 的内存,容器将被终止:

docker run -m 128m my-spring-boot-app

将 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


其他优化技术
除了我们目前讨论过的策略之外,还可以考虑其他优化技术来高效利用内存。一种简单的 方法是移除所有未使用的启动器依赖项。每个启动器都会引入额外的依赖项和初始化代码,这会消耗内存。

另一个实用的方法是禁用应用中不使用的缓存。如果我们不使用 EhCache 或 Caffeine 之类的缓存框架,最好不要添加它们,因为它们通常会将数据存储在内存中,这会迅速累积并占用过多的内存空间。

最后,如果可行的话,我们还可以考虑切换到 32 位 JVM。如果我们的应用程序很小,并且所需的堆空间小于 1.5 GB,那么使用 32 位 JVM 可以显著减少内存开销。

避免过度优化
虽然我们讨论的策略减少了整体内存占用,但过度优化可能会导致意外问题并且代价高昂。

在进行优化之前,务必了解应用程序的需求和未来的规模。有时,应用程序可能确实需要充足的内存,此时优化空间不大,反而需要增加资源。

这样做的目的不是减少内存使用量和降低成本,而是根据应用程序需求最佳地使用内存,并潜在地防止任何与内存不足相关的问题。