1GB 内存的 ECS 跑 Spring Boot,这属于“极限操作”。在现在的技术环境下,Spring Boot 默认启动往往就要占用 200-400MB 甚至更多,再加上操作系统本身、数据库连接池、日志输出等开销,很容易直接 OOM(Out Of Memory)或者被系统 Swap 机制拖死。
如果你必须在这台机器上部署,或者这是为了压测/学习目的,以下是经过实战验证的优化清单,按优先级排序:
1. JVM 参数是生死线
这是最关键的一步。不要使用默认的 -Xmx 和 -Xms。
-
设置堆内存上限:
建议将最大堆内存设置为物理内存的 50%-60%。对于 1GB 机器,建议设为-Xmx512m或-Xms512m。java -jar app.jar -Xms512m -Xmx512m- 注意:一定要让
-Xms和-Xmx相等,避免 JVM 动态调整堆大小带来的性能抖动和额外内存开销。
-
开启 G1 GC:
Java 8u191+ 或 Java 11+ 默认就是 G1,但如果是老版本 Java 8,务必指定:
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED(某些框架需要)
更关键的是确保使用 G1 垃圾回收器:-XX:+UseG1GC。G1 对低延迟和大堆(相对小堆而言)更友好,且能更好地控制停顿时间。 -
元空间(Metaspace)限制:
Spring Boot 依赖较多,类加载多,默认 Metaspace 可能无限增长直到耗尽内存。
设置-XX:MaxMetaspaceSize=128m,防止元空间泄漏导致 OOM。 -
关闭不必要的调试信息:
去掉-Xdebug、-Xrunjdwp等开发期参数,生产环境不需要这些。
2. 应用层面的“瘦身”
-
排除 Tomcat 容器:
Spring Boot 默认内嵌 Tomcat,虽然方便,但有一定开销。如果追求极致,可以考虑切换到 Undertow(基于 Netty,内存占用更低,并发能力更强)。<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> -
精简依赖:
检查pom.xml/build.gradle,移除所有未使用的 starter。例如,如果不使用 Actuator,就删掉;如果不使用 JPA,就别引入 Hibernate 的大包。 -
禁用自动配置:
在application.yml或main方法中,通过exclude排除不需要的 AutoConfiguration 类。比如不用 Redis 就排除 RedisAutoConfiguration,不用 JPA 就排除 DataSourceAutoConfiguration 等。每少一个自动配置,JVM 初始化和运行时内存都省一点。 -
异步日志 + 本地磁盘缓冲:
内存紧张时,I/O 也会成为瓶颈。使用 Logback 或 Log4j2 时,配置异步 Appender,并将日志写入本地磁盘而非直接打印到 stdout/stderr(尤其是当 stdout 被重定向到文件时,频繁刷新会消耗大量 CPU 和内存)。
3. 操作系统与中间件协同
-
关闭 Swap(交换分区):
非常重要! Linux 的 Swap 机制会导致严重的性能抖动(Thrashing)。当内存不足时,系统会将部分内存页换出到磁盘,而磁盘 I/O 极慢,会导致整个服务响应极慢甚至假死。# 查看 swap free -h # 临时关闭 sudo swapoff -a # 永久关闭需修改 /etc/fstab宁愿 OOM Crash 重启,也不要让系统进入 Swap 状态。配合 Kubernetes 或简单的监控脚本,在 OOM 后自动拉起即可。
-
数据库连接池调优:
如果你在同一台机器上部署了 MySQL/MongoDB 等,务必限制连接池大小。
例如 HikariCP:spring: datasource: hikari: maximum-pool-size: 5 # 1GB 内存下,5-10 个连接足够,别设成 20+ minimum-idle: 2每个连接都会占用一定内存(线程栈、缓冲区),连接数过多会迅速吃光内存。
-
考虑嵌入式数据库或轻量级存储:
如果可能,用 H2 内存数据库代替 MySQL,或用 SQLite 代替 MongoDB。对于 1GB 机器,跑一个完整的 MySQL 实例是非常奢侈且危险的。
4. 监控与兜底策略
-
健康检查与自动重启:
由于内存有限,长期运行可能出现内存泄漏或碎片化。建议在 ECS 上使用简单的守护进程(如 systemd 或 supervisor)监控应用进程,一旦崩溃立即重启。# /etc/systemd/system/myapp.service [Service] Restart=always RestartSec=5 Environment="JAVA_OPTS=-Xms512m -Xmx512m -XX:+UseG1GC" -
启用 JVM 内置监控:
开启-XX:+UnlockDiagnosticVMOptions -XX:+PrintGCDetails暂时用于排查,但生产环境建议接入 Prometheus + Grafana,重点监控Heap Used,Non-Heap Used,GC Count,GC Time。 -
阿里云/腾讯云原生监控:
利用云厂商提供的云监控插件,设置内存使用率超过 85% 时报警,提前干预。
5. 架构层面的妥协
-
单体 vs 微服务:
在 1GB 内存上谈“微服务”是伪命题。真正的微服务意味着多个独立进程,每个都要有 JVM 开销。此时应将其视为单体应用(Monolith)来设计,只是代码结构上保持模块化。不要试图在一台 1GB 机器上同时运行 Eureka/Nacos + Gateway + 两个业务服务。 -
缓存策略:
避免在应用中创建过大的本地缓存(如 Caffeine/Guava Cache)。如果必须缓存,考虑使用外部 Redis(如果预算允许加一台便宜的 Redis 实例),否则本地缓存要严格设置 TTL 和最大条目数。
总结 checklist
- JVM:
-Xms512m -Xmx512m -XX:+UseG1GC -XX:MaxMetaspaceSize=128m - 容器: 优先 Undertow,其次 Tomcat(调小 maxThreads)
- OS: 关闭 Swap
- DB: 连接池 ≤ 10
- 依赖: 剔除无用 Starter,禁用无用 AutoConfiguration
- 心态: 接受偶尔 OOM 重启,做好自动化运维准备
最后提醒:1GB 内存适合做 Demo、测试环境、极低流量的内部工具或边缘计算节点。对于面向公众的生产系统,至少升级到 2GB 起步,体验会有质的飞跃。
云技术博客