在 2GB 内存的 Ubuntu 服务器上跑 Spring Boot,属于典型的“小马拉大车”场景。很多新手容易犯的错误是:直接套用生产环境的标准 JVM 参数(比如 -Xmx1g -Xms1g),结果还没等请求进来,服务器因为 OOM(Out Of Memory)或者 Swap 频繁交换直接把服务拖垮了。
要在这种资源受限的环境下稳住性能,核心思路就三个:极致压缩 JVM 堆内存、利用 Linux 内核特性、以及从代码层面做减法。下面我把实操中验证过的方案拆解开来讲。
一、 JVM 参数的“抠门”艺术
这是最立竿见影的部分。2GB 内存里,操作系统本身需要预留至少 500MB-800MB 用于缓存、文件描述符和系统进程,留给 Java 的空间其实很紧张。
1. 堆内存设置原则
不要设太大。建议将最大堆内存(-Xmx)和初始堆内存(-Xms)设置为 512MB – 768MB。
- 如果设为 1GB,剩余不到 1GB 给非堆内存(Metaspace)、线程栈、DirectBuffer 和 OS 缓存,极易触发 GC 风暴或直接 OOM。
- 推荐参数:
-Xms512m -Xmx512m或-Xms768m -Xmx768m。保持Xms=Xmx是为了避免 JVM 在运行时动态调整堆大小带来的性能抖动。
2. 垃圾回收器选择:ZGC 还是 G1?
- Java 11+ / 17+:强烈建议尝试 ZGC(如果业务对停顿时间敏感)。ZGC 的设计初衷就是低延迟,且在大对象处理上比 G1 更平滑。但在小堆内存下,ZGC 的并发标记阶段开销需要评估。
- 通用稳妥方案:G1 GC。对于中小堆内存,G1 的表现非常稳定。
- 参数示例:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 - 注意:开启 ZGC 时可能需要配合
-XX:ConcGCThreads=4来限制并发线程数,防止抢占 CPU。
- 参数示例:
3. 元空间(Metaspace)控制
Spring Boot 启动时会加载大量类,元空间消耗很快。
- 默认情况下 Metaspace 没有硬上限,可能会吃掉堆外内存。
- 建议显式设置上限:
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m。这能防止元空间无限增长导致系统内存耗尽。
4. 关闭不必要的调试信息
去掉 -verbose:gc、-XX:+PrintGCDetails 等在生产环境毫无意义且消耗 I/O 的参数。如果必须看日志,使用 JFR(Java Flight Recorder)采样模式,而不是全量打印。
二、 Linux 内核层面的微调
Ubuntu 默认的 kernel 参数是为通用场景设计的,针对高并发小内存服务器需要做针对性调整。
1. 减少 Swap 的使用(关键)
Swap 是性能杀手。当物理内存不足时,Swap 会导致响应时间从毫秒级飙升到秒级甚至分钟级。
- 操作:编辑
/etc/sysctl.conf,设置:vm.swappiness = 1 vm.overcommit_memory = 1swappiness=1意味着只有万不得已才用 Swap;overcommit_memory=1允许超卖内存分配,配合良好的 GC 策略,可以避免因瞬时内存峰值导致的 Kill 进程。
2. TCP 连接优化
Spring Boot 通常伴随 Tomcat/Jetty,会建立大量短连接。
- 增加本地端口范围:
net.ipv4.ip_local_port_range = 1024 65535 - 快速回收 TIME_WAIT 状态:
net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 15这能显著提高并发处理能力,防止端口耗尽。
3. 文件描述符限制
Spring Boot 应用和 Nginx(如果有前置)都需要大量 FD。
- 修改
/etc/security/limits.conf:* soft nofile 65535 * hard nofile 65535 - 同时在 systemd 服务文件中添加:
[Service] LimitNOFILE=65535
三、 应用层与架构优化
硬件改不了,只能靠软件凑。
1. 瘦身启动器(Starter)
Spring Boot 自动配置功能很强,但也引入了大量不需要的 Bean。
- 做法:仔细检查
pom.xml或build.gradle。如果你没用到 JPA,就别引spring-boot-starter-data-jpa,改用 MyBatis 或 JDBC Template。没用到 Actuator 监控,就别引actuator。每去掉一个 Starter,可能节省几十 MB 内存和几秒启动时间。 - 移除冗余依赖:有些库自带了 commons-lang3、slf4j 等,确保没有版本冲突导致的 jar 包膨胀。
2. 数据库连接池调优
HikariCP 是默认且优秀的,但在小内存服务器上,连接数不宜过多。
- 每个数据库连接大约占用 1-2MB 内存(取决于驱动和查询缓存)。
- 如果连接池最大为 20,加上其他开销,可能就要吃掉 40-50MB。
- 建议根据实际并发量设置
maximumPoolSize。对于小型服务,max-pool-size: 10往往足够,既能保证并发,又节省内存。
3. 启用 Gzip/Brotli 压缩
在 Nginx 或 Spring Boot 内部(通过 server.compression.enabled=true)启用压缩。
- 虽然这主要节省带宽,但减少了网络传输数据量,间接降低了客户端等待时间和服务器输出流的内存缓冲压力。
4. 异步化与非阻塞 IO
如果涉及耗时操作(如发送邮件、生成报表、调用第三方 API),务必使用 @Async 或 Reactor/WebFlux 模型。
- 同步阻塞会占用线程栈(每个线程约 1MB)。2GB 内存最多支撑几百个活跃线程,一旦线程池打满,新请求直接排队或拒绝,体验极差。
- 将耗时任务丢到独立线程池,主线程快速返回,能极大提升吞吐量。
四、 运维与监控建议
1. 容器化部署(Docker)
即使只有一台机器,也建议用 Docker 运行。
- 优势:可以通过
--memory=1.5g --cpus=1严格限制容器资源,防止某个实例失控拖垮整台机器。 - 方便迁移和扩容。
2. 轻量级监控
不要用 Prometheus + Grafana 全套,太重。
- 使用 Micrometer + Prometheus 暴露指标即可。
- 或者直接使用阿里云/腾讯云自带的云监控 Agent,它们对资源占用极低。
- 重点监控:Heap Used, Non-Heap Used, Thread Count, GC Time。
3. 定期重启
在小内存环境下,长期运行可能出现内存泄漏或碎片化问题。
- 可以设置定时任务(如每周凌晨 4 点)优雅重启服务,释放潜在的资源泄漏。
总结 checklist
- JVM:
-Xms512m -Xmx512m, G1 GC 或 ZGC, Metaspace 限制在 256m。 - OS:
vm.swappiness=1, 放宽文件描述符限制。 - Code: 剔除无用 Starter, 连接池大小 <= 10, 耗时操作异步化。
- Deploy: Docker 限制内存上限, 启用 Gzip。
这套组合拳下来,2GB 内存跑一个中等负载的 Spring Boot 单体应用是完全可行的。记住,小内存服务器的优化本质是“取舍”:牺牲一点弹性,换取绝对的稳定性和可控性。
云技术博客