Rust比Spring快10倍?我做了实测,发现真相没那么简单

给一个.side project选运行时的时候,顺手跑了一下TechEmpower Benchmark和社区压力测试。数字出来之后,我重新想了想自己对Java性能的认知。

配置吞吐量P99延迟内存占用
Spring Boot (JVM)4,200 req/s45ms280 MB
Spring Boot (Native)3,600 req/s38ms55 MB
Quarkus (Native)5,100 req/s28ms35 MB
Rust (Axum + Tokio)42,000 req/s3ms12 MB

测试条件:简单JSON返回 + PostgreSQL查询,100并发连接,持续60秒压测。

Rust的数字确实漂亮——吞吐量是Spring Boot的10倍,内存只有后者的1/23。但这篇文章不是Rust吹哨文。我想聊聊这些数字到底怎么来的,以及什么时候你根本不需要Rust。

GC税:你以为的快,其实是在交保护费

性能差距最大的单点因素,就是垃圾回收。而且这个税,大多数Java程序员是不知道自己在交的。

Spring Boot的JVM里,GC在工作的时候你的程序是暂停的

Spring Boot跑在HotSpot JVM上,默认用的是G1GC。听起来高大上,但原理很简单粗暴:程序运行中不断创建对象,堆内存越用越多,满了就停下来——所有线程一起停——去做垃圾回收,回收完了再继续。

这个"一起停"就是问题。

在中等负载下,年轻代大概1到3秒就满了,触发一次年轻代GC,所有线程冻结5到15毫秒。每隔几分钟来一次混合GC,冻结时间拉到20到50毫秒。内存紧张的时候还会触发一次Full GC,整个JVM直接冻住100到500毫秒。

在你的应用代码里完全看不到GC的影子,但它会忠实地体现在尾部延迟上。更讨厌的是GC抖动——平均停顿10毫秒听起来还行,但分布曲线有个长长的尾巴。每几千个请求里就有一个撞上50毫秒以上的停顿。你的P99看起来光鲜,P99.9可能惨不忍睹。

如果你曾经觉得"我的Java服务明明挺快的,为什么总有几个请求莫名其妙地慢",大概率就是GC在作祟。

Quarkus Native Image:用编译时的工作换运行时的不打扰

Quarkus编译成GraalVM Native Image之后,默认用的是Serial GC。单线程的stop-the-world收集器,但因为整个堆只有35到60MB,停顿时间只有1到3毫秒。而且这个停顿非常稳定可预测,不存在JVM那种抖动。

更极端一点,可以用Epsilon GC——一个什么都不做的收集器。永远不回收内存,用到上限就进程重启。听着很粗暴,但对短生命周期容器来说反而完美,Kubernetes会负责处理重启逻辑。

Rust:编译时就决定了内存归属

Rust没有垃圾收集器。不是低pause GC,是根本没有GC。

内存管理在编译阶段就完成了。变量离开作用域,内存立即释放,没有运行时开销,没有后台线程扫描堆,没有"等一下"的操作。

结果就是:没有GC停顿,没有GC CPU开销,没有抖动。每个请求的延迟完全由你的代码决定,不取决于GC什么时候想跑。所以Rust的P99是3毫秒而Spring Boot是45毫秒——核心差异不在于谁跑代码更快,而在于谁从来不暂停。

编译方式的对决:JIT预热、Native编译和LLVM

第二个拉开差距的地方,是代码怎么变成机器能认识的指令。

Spring Boot的JVM模式:先慢后快

HotSpot的C2 JIT编译器在运行时分析代码,找到热点路径,编译成优化后的机器码。预热60到90秒之后,Spring Boot的吞吐量表现相当不错。

但有两个坑。

第一个是冷启动悬崖。启动后前90秒,代码以解释模式运行,性能差距巨大:

  • 前30秒:P99 = 450毫秒
  • 30到60秒:P99 = 120毫秒(部分JIT编译)
  • 90秒以后:P99 = 45毫秒(完全优化)

同一个人在跑同一个服务,冷启动和预热后的性能相差10倍。在容器化环境里Pod频繁扩缩容,你经常要在最需要性能的时候跑在冷实例上。

第二个是启动时间。JVM模式启动要2到3秒。对Serverless函数和滚动发布来说,这个时间挺要命的。

Quarkus + GraalVM Native Image:编译时定型

Native Image在构建阶段把整个应用编译成独立的二进制文件。没有JVM,没有字节码,没有解释器。

  • 启动时间:0.03到0.06秒(JVM是2到3秒)
  • 没有冷启动悬崖,第一个请求就是最佳性能
  • 内存减少5到8倍(没有类元数据、没有JIT编译器、没有字节码常驻)

代价:因为没有JIT运行时优化,极限吞吐量通常比完全预热后的HotSpot JVM低10%到25%。JIT编译器能做基于运行时profile的激进优化(虚函数内联、循环展开),AOT编译做不到。

Spring Boot Native(3600 req/s)比Spring Boot JVM(4200 req/s)略慢,原因就在这里——但这个比较只在JVM完全预热之后才成立。

Rust + LLVM:零成本抽象

Rust通过LLVM后端直接编译成机器码。和Native Image一样是AOT编译,但更进一步:没有运行时 substrate(GraalVM Native Image还带15到30MB的 substrate),没有GC,甚至连Serial GC都没有。LLVM在编译时做非常激进的优化,代码质量和C/C++同档次。

还有一个特点:零成本抽象。Rust的高级特性——迭代器、async/await、泛型——编译后和手写循环的机器码一样高效。没有运行时多态开销,没有隐藏的动态派发成本。

所以Rust能以12MB内存跑出42,000 req/s。它不是一个"更好的Java",而是一个完全不同的计算模型。

CPU时间都花在哪了:Spring Boot和Rust的性能消耗解剖

看完了框架,再来解剖CPU时间的分配。

Spring Boot (JVM) — 4,200 req/s

  • 42%:业务逻辑和数据库查询
  • 18%:JSON序列化/反序列化(Jackson)
  • 15%:GC开销(G1GC后台线程和停顿)
  • 12%:JVM基础设施(类加载、反射、代理生成)
  • 8%:HTTP栈(Tomcat/Netty开销)
  • 5%:JIT编译器后台线程

Rust (Axum + Tokio) — 42,000 req/s

  • 70%:业务逻辑和数据库查询
  • 12%:JSON序列化(serde,零拷贝,编译时生成)
  • 0%:GC(根本不存在)
  • 0%:运行时开销(没有VM,没有substrate)
  • 15%:Tokio异步运行时 + Hyper HTTP
  • 3%:系统调用

从数字看,CPU花在框架上的比例和性能成反比。Spring Boot JVM花42%的CPU在业务逻辑上,58%在框架上。Rust花70%在业务逻辑上,30%在HTTP/异步基础设施上。吞吐量差了10倍,但CPU干的是同样的工作——区别在于浪费了多少。

内存:一张AWS账单告诉你为什么这不只是技术指标

内存占用不只是一个技术数字,它直接换算成钱。

在AWS ECS Fargate(us-east-1,20个实例,24/7运行)上对比:

  • Spring Boot JVM(0.5 vCPU,1GB/实例):每月约665美元
  • Spring Boot Native(0.25 vCPU,0.5GB/实例):每月约180美元
  • Quarkus Native(0.25 vCPU,0.5GB/实例):每月约180美元
  • Rust(0.25 vCPU,0.5GB/实例):每月约180美元

真正的成本悬崖是JVM到Native这一步——730%的费用差距。Fargate的最低配置是0.25 vCPU/0.5GB,所以Quarkus Native和Rust落在同一个收费档。

把这个乘以10个服务,就是每月6650美元和1800美元的差别。每年省58000美元,就因为从JVM切到了Native Image。

所以真正想说的不是"Rust比Quarkus便宜",而是JVM的内存和CPU开销是AWS账单上实实在在的一行项目,GraalVM Native Image可以在不离开Java生态的情况下抹掉大部分开销。

GraalVM Native Image:Java开发者最现实的选择

如果你是Java团队,看到Rust的数字流口水,GraalVM Native Image是最务实的折中方案。

Native Image能给你:

  • 启动速度提升40倍(2.5秒降到60毫秒)
  • 内存减少5到8倍(280MB降到35到55MB)
  • 没有JIT预热悬崖,从第一个请求开始就是最佳性能
  • Serial GC下GC停顿降到2毫秒以下
  • 单一二进制部署,不需要JRE

诚实的代价:

  • 构建时间:3到7分钟(JVM模式10到25秒),影响开发体验
  • 极限吞吐量:比完全预热的JVM低10%到25%
  • 生态兼容性问题:部分Java库用反射的方式和AOT不兼容
  • 调试能力受限:没有jstack、jmap、JFR、VisualVM,JVM工具链全部消失
  • 闭世界假设:禁止动态类加载和运行时字节码生成

建议的使用场景:

  • I/O密集型服务(REST API、CRUD服务、事件消费者)——启动速度、内存占用和延迟可预测性比极限CPU吞吐量重要,用Native Image。
  • 计算密集型工作(批处理、流处理、ML推理)——JIT编译器的运行时优化真能带来15%到25%的提升,别用Native Image,继续跑JVM。

我在AWS Lambda上部署过一个Quarkus + GraalVM Native Image的REST API,走CloudFront → API Gateway。冷启动500毫秒,Lambda内存1024MB。同样的API在Spring Boot JVM Lambda上需要2048MB内存,冷启动还要10到15秒。Quarkus Native把内存砍了一半,启动速度快了20到30倍,整个Lambda月费不到50美元。

Rust:什么时候Native Image还不够

先说清楚Rust适合什么、不适合什么。

Cloudflare的边缘代理、Discord的消息系统、AWS Lambda的Firecracker虚拟机、Linkerd的服务网格代理——都是Rust,都是基于同样原因选的:P99要小于5毫秒,或者吞吐量要扛住2万req/s以上。

但Rust有一个真实的生产力差距:一个高级Java开发者2到3天能交付一个生产级REST API。同样的API一个有经验的Rust开发者要5到7天,Rust新手要2到3周。

Rust编译器出了名的严格。借用检查器在编译期捕获内存bug——这也是Rust零内存安全漏洞的原因——但学习曲线很陡。“和借用检查器搏斗"是每个Rust新人都要经历的阶段,大概持续2到4个月。

对于企业CRUD服务,10倍性能优势很少值得2到3倍的生产力成本。但对于性能关键的底层基础设施,情况完全不同。

怎么选:一个实操决策矩阵

跑完benchmark和生产部署之后,这是我的决策框架:

大多数企业API服务:选Quarkus + GraalVM Native Image。 Java生态、团队上手快、性能提升显著,三者平衡最好。

Spring主导的团队:选Spring Boot 3.x + GraalVM Native Image。 不要为了框架换框架,Native Image能弥补大部分性能差距。

性能关键路径(API网关、数据摄取管道、实时事件处理器、P99低于5毫秒是硬需求):选Rust。 为了正确的场景值得投入。

计算密集型工作(批处理、流处理、ML推理):继续用Spring Boot或Quarkus的JVM模式。 JIT编译器在这里真的有用。

最务实的做法是混合架构: API网关用Rust,核心业务服务用Quarkus/Spring Native,批处理跑在JVM上。每个服务用对的工具。

数据汇总

核心数字在这里:

指标JVMNativeRust
启动时间2,500ms25ms3ms
内存占用280MB35MB12MB
吞吐量4,200 req/s5,100 req/s42,000 req/s
P99延迟45ms28ms3ms
GC停顿5-50ms1-3ms0ms

如果你现在在用Java,GraalVM Native Image配合Quarkus是最划算的选择——内存砍一半,启动快40倍,GC停顿降到2毫秒以下。团队不需要学新语言。

Rust留给你真正需要P99小于5毫秒的那5%的服务。剩下的,Native Image足够好了。JVM不会消亡,但2026年了还不用Native Image,确实是在自己的账单上白扔钱。


常见问题

Q: Quarkus Native Image和Spring Boot Native Image哪个更好?

如果你的团队已经用Spring,迁移框架的成本可能比性能收益还高。Spring Boot 3.x的Native Image支持已经成熟,性能差距和Quarkus Native相比不大。但如果是从零开始,Quarkus的云原生设计让它在Native Image场景下通常表现更好。

Q: 我有一个高流量API,但团队都是Java背景,该怎么过渡?

不要一次性全部重写。先把一个非核心服务用Quarkus Native Image部署上线,感受一下构建流程和生态兼容性。确认没问题之后逐步迁移。混合架构也是合理的选择——Rust处理最关键的前置网关,Java服务处理业务逻辑。

Q: GC抖动有办法缓解吗,不一定要上Native Image?

可以。用ZGC或Shenandoah这两个低暂停GC器可以把停顿降到亚毫秒级别。但它们依然有CPU开销和抖动,内存占用也比Native Image高不少。这是JVM内的最好方案,但和真正的无GC还是有本质差距。