简介: 在高性能的背后,Java 的启动性能差也令人印象深刻,大家印象中的 Java 笨重缓慢的印象也大多来源于此。高性能和快启动速度似乎有一些相悖,本文将和大家一起探究两者是否可以兼得。
作者 | 梁希
高性能和快启动速度,能否鱼和熊掌兼得?
Java 作为一门面向对象编程语言,在性能方面的卓越表现独树一帜。
《Energy Efficiency across Programming Languages,How Does Energy, Time, and Memory Relate?》这份报告调研了各大编程语言的执行效率,虽然场景的丰富程度有限,但是也能够让我们见微知著。
从表中,我们可以看到,Java 的执行效率非常高,约为最快的 C 语言的一半。这在主流的编程语言中,仅次于 C、Rust 和 C++。
Java 的优异性能得益于 Hotspot 中非常优秀的 JIT 编译器。Java 的 Server Compiler(C2) 编译器是 Cliff Click 博士的作品,使用了 Sea-of-Nodes 模型。而这项技术,也通过时间证明了它代表了业界的最先进水平:
- 著名的 V8(JavaScript 引擎)的 TurboFan 编译器使用了相同的设计,只是用更加现代的方式去实现;
- Hotspot 使用 Graal JVMCI 做 JIT 时,性能基本与 C2 持平;
- Azul 的商业化产品将 Hotspot 中的 C2 compiler 替换成 LLVM,峰值性能和 C2 也是持平。
在高性能的背后,Java 的启动性能差也令人印象深刻,大家印象中的 Java 笨重缓慢的印象也大多来源于此。高性能和快启动速度似乎有一些相悖,本文将和大家一起探究两者是否可以兼得。
JAVA 启动慢的根因
1、框架复杂
JakartaEE 是 Oracle 将 J2EE 捐赠给 Eclipse 基金会后的新名字。Java 在1999年推出时便发布了 J2EE 规范,EJB(Java Enterprise Beans) 定义了企业级开发所需要的安全、IoC、AOP、事务、并发等能力。设计极度复杂,最基本的应用都需要大量的配置文件,使用非常不便。
随着互联网的兴起,EJB 逐渐被更加轻量和免费的 Spring 框架取代,Spring 成了 Java 企业开发的事实标准。Spring 虽然定位更加轻量,但是骨子里依然很大程度地受 JakartaEE 的影响,比如早期版本大量 xml 配置的使用、大量 JakartaEE 相关的注解(比如JSR 330依赖注入),以及规范(如JSR 340 Servlet API)的使用。
但 Spring 仍是一个企业级的框架,我们看几个 Spring 框架的设计哲学:
- 在每一层都提供选项,Spring 可以让你尽可能的推迟选择。
- 适应不同的视角,Spring 具有灵活性,它不会强制为你决定该怎么选择。它以不同的视角支持广泛的应用需求。
- 保持强大的向后兼容性。
在这种设计哲学的影响下,必然存在大量的可配置和初始化逻辑,以及复杂的设计模式来支撑这种灵活性。我们通过一个试验来看:
我们跑一个spring-boot-web的helloword,通过-verbose:class可以看到依赖的class文件:
$ java -verbose:class -jar myapp-1.0-SNAPSHOT.jar | grep spring | head -n 5 [Loaded org.springframework.boot.loader.Launcher from file:/Users/yulei/tmp/myapp-1.0-SNAPSHOT.jar] [Loaded org.springframework.boot.loader.ExecutableArchiveLauncher from file:/Users/yulei/tmp/myapp-1.0-SNAPSHOT.jar] [Loaded org.springframework.boot.loader.JarLauncher from file:/Users/yulei/tmp/myapp-1.0-SNAPSHOT.jar] [Loaded org.springframework.boot.loader.archive.Archive from file:/Users/yulei/tmp/myapp-1.0-SNAPSHOT.jar] [Loaded org.springframework.boot.loader.LaunchedURLClassLoader from file:/Users/yulei/tmp/myapp-1.0-SNAPSHOT.jar] $ java -verbose:class -jar myapp-1.0-SNAPSHOT.jar | egrep '^\[Loaded' > classes $ wc classes 7404 29638 1175552 classes
class 个数到达惊人的 7404 个。
我们再对比下 JavaScript 生态,使用常用的 express 编写一个基本应用:
const express = require('express') const app = express() app.get('/', (req, res) => { res.send('Hello World!') }) app.listen(3000, () => { console.log(`Example app listening at http://localhost:${port}`) })
我们借用 Node 的 debug 环境变量分析:
NODE_DEBUG=module node app.js 2>&1 | head -n 5 MODULE 18614: looking for "/Users/yulei/tmp/myapp/app.js" in ["/Users/yulei/.node_modules","/Users/yulei/.node_libraries","/usr/local/Cellar/node/14.4.0/lib/node"] MODULE 18614: load "/Users/yulei/tmp/myapp/app.js" for module "." MODULE 18614: Module._load REQUEST express parent: . MODULE 18614: looking for "express" in ["/Users/yulei/tmp/myapp/node_modules","/Users/yulei/tmp/node_modules","/Users/yulei/node_modules","/Users/node_modules","/node_modules","/Users/yulei/.node_modules","/Users/yulei/.node_libraries","/usr/local/Cellar/node/14.4.0/lib/node"] MODULE 18614: load "/Users/yulei/tmp/myapp/node_modules/express/index.js" for module "/Users/yulei/tmp/myapp/node_modules/express/index.js" $ NODE_DEBUG=module node app.js 2>&1 | grep ': load "' > js $ wc js 55 392 8192 js
这里只依赖了区区 55个 js 文件。
虽然拿 spring-boot 和 express 比并不公平。在 Java 世界也可以基于 Vert.X、Netty 等更加轻量的框架来构建应用,但是在实践中,大家几乎都会不假思索地选择 spring-boot,以便享受 Java 开源生态的便利。
2、一次编译,到处运行
Java 启动慢是因为框架复杂吗?答案只能说框架复杂是启动慢的原因之一。通过 GraalVM 的 Native Image 功能结合 spring-native 特性,可以将 spring-boot 应用的启动时间缩短约十倍。
Java 的 Slogan 是 "Write once, run anywhere"(WORA),Java 也确实通过字节码和虚拟机技术做到了这一点。
WORA 使得开发者在 MacOS 上开发调试完成的应用可以快速部署到 Linux 服务器,跨平台性也让 Maven 中心仓库更加易于维护,促成了 Java 开源生态的繁荣。
我们来看一下 WORA 对 Java 的影响:
- Class Loading
Java 通