您可以购买更多服务器。
或者,您可以停止浪费已经购买的服务器。
我们在一个普通的 Spring Boot 服务上调整了三个开关。RAM
减少了 60%。启动速度提高了 30%。
无需重写。无需英雄事迹。无需凌晨两点回滚。
如果您在 JVM 上交付 API,这将是您能获得的最经济的性能。
这个故事适合从事生产的团队。
因为不知道的代价是内存失控、冷启动不符合 SLA,以及寻呼机将周末视为工作日。
我们开始的基线
一个无聊的 Java 17 + Spring Boot 3.x REST 服务。
Tomcat。HikariCP。Jackson。标准配置。
稳定负载:200-300 RPS。
更改前:稳定状态下 RSS 为 780 MB。冷启动到“已启动……”耗时 620 毫秒。
没有什么“着火”,但每个豆荚的成本都超过了应有的水平。
我们打开的 3 个开关(以及它们为何有效)
1)迁移到 Java 21 LTS + 开启字符串重复数据删除
工作原理:现代 HotSpot plus-XX:+UseStringDeduplication压缩由 JSON、标头和 ORM 层创建的重复字符串。
相同的数据,更少的字节。
如何操作:
- 使用 Java 21 基础镜像(例如 Temurin)。
- 添加此 JVM 标志:
-XX:+UseStringDeduplication。
解析文本或分配大量小字符串的服务中,堆内存会更精简。大多数 API 都是如此。
按 Enter 键或单击即可查看完整尺寸的图像
2)为应用程序工作启用虚拟线程
其工作原理:虚拟线程为您提供数千个廉价、阻塞友好的线程,而不会导致堆栈膨胀。
你的代码仍然保持命令式。并发性上升。池化的内存占用减少。
如何用最少的代码来实现:
// src/main/java/com/example/Config.java package com.example; import java.util.concurrent.Executors; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.TaskExecutorAdapter; public class Config { (name = TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) TaskExecutor appExecutor() { return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor()); } }
现在@Async,您的调度程序和内部执行程序使用每个任务虚拟线程模型,而无需删除逻辑。
提示:保持同步热部分较短以避免固定。
3)使用 CDS + Lazy Init 加速冷启动(在安全的地方)
工作原理: 类数据共享 (CDS)会预热类元数据,以便 JVM 链接速度更快。
延迟初始化会推迟启动时不需要的 bean 的创建。
如何操作:
- 运行
-Xshare:auto(许多构建中的默认设置)或在构建时生成共享存档。 - 添加(小心地)
spring.main.lazy-initialization=true以快速启动,然后在启动运行器或就绪探测器中预热关键路径。
无需手动调整每个 bean,启动速度就会更快。
之前与之后:重要的数字
- RSS 内存: 780 MB → 310 MB(-60%)。
- 冷启动: 620 毫秒 → 430 毫秒(-31%)。
- 稳定的 p99 延迟: 240 毫秒 → 215 毫秒(-10%)。
- 吞吐量余量:在相同限制下增加 25%。
简单、可比、有意义。
这就是利益相关者所记住的。
底层发生了什么变化
Before +------------------------+ | 200 platform threads | -> hefty stacks, fixed pool, idle cost +------------------------+ After +------------------------+ | 20 platform threads | -> carriers only +------------------------+ || \/ +------------------------------------------+ | thousands of virtual threads on demand | -> cheap, block-friendly +------------------------------------------+
您不再需要为主要等待 I/O 的线程支付堆栈内存。
我们遇到的陷阱是为了防止您遇到
- 固定陷阱:长
synchronized块或本机调用会固定虚拟线程。保持它们简短;最好在小型临界区周围使用锁。 - ThreadLocals:大量
ThreadLocal使用虚拟线程会导致线程使用量成倍增加。请审计所有在其中存储大型对象的操作。 - 延迟初始化的意外情况:
/ready第一个请求可能会初始化 bean。在启动时或使用轻量级运行器进行简单预热即可。 - 关于池大小的误区:一旦虚拟线程处理突发事件,通常可以大幅缩小固定池的大小。务必进行实际测试。
安全推出检查清单
- 运送一个带有 Java 21、CDS 和字符串重复数据删除的金丝雀荚。
- 首先启用虚拟线程执行器来执行内部任务。
- 观察RSS、分配率、p95/p99和GC 暂停时间。
- 如果图表显示一天的交通模式为绿色,则扩展到车队。
- 只有在冷击不会造成损害的路径上才考虑延迟初始化。
小步前进,实实在在的收获,没有波澜。
为什么这种方法可以跨团队扩展
- 这是配置优先的。大部分工作都集中在 Docker 标志和一个 bean 上。
- 它是框架原生的。Spring Boot 拥抱 Java 21 特性;你不需要与你的堆栈作斗争。
- 它对企业友好。更低的内存意味着更高的节点密度。更快的启动速度意味着更顺畅的部署。这不仅能降低账单,还能让值班更安心。
每个人都忘记的部分
性能不是一种特性,而是一种没有浪费的东西。
Java 21 + Spring Boot 为您提供了现代运行时、更便宜的并发性和更快的启动,而无需触及产品代码。
如果您正在关注云账单或 pod 限制,请从这里开始。
拨动这些开关。衡量。保留有用的。
您的用户不会注意到任何事情——除了您的应用程序感觉更快并且不再要求更多内存。