2.2 Quarkus方式
我们确信你想知道Quarkus如何比传统框架启动更快、消耗更少内存的所有复杂细节,对吗?我们先解释传统框架,再讲述Quarkus方式。
图2-2描述了传统框架的一些众所周知的特征:
• 代码中的注解定义了预期的行为,多年来我们使用过很多这样的例子。典型的例子包括@Entity、@Autowired、@Inject等。
• 各种配置文件。从定义类应该如何连接到一起,到配置持久数据源,以及这两者之间的一切,这些配置文件都可以完成。
• 仅在启动期间用于创建运行时元数据的类,以及用于应用程序运行的类。
• 利用反射确定要调用的方法,将值设置到对象中,并仅按名称动态加载类。
图2-2:Quarkus方式
我们当然不是说Quarkus没有注解、配置文件或传统框架的任何其他功能,而是说Quarkus以一种非常不同的方式来处理这些功能。
为什么传统框架的这些特征会被认为是“不好的”?这是一个很好的问题,回答这个问题需要对这些框架如何处理前面的特征有所了解。当需要解析任何类型的配置或发现任何注解时,都需要用框架类来执行这项工作。根据过程的复杂程度,执行任务可能需要几十到几百个类。此外,每个类通常都会在自己内部保持状态(表示启动时的中间状态),或处理完一切后的最终理想状态。
这没什么特别的。框架已经以这种方式工作了多年,甚至几十年。然而,你可能没有意识到的是,即使JVM进程运行了6个月而没有重启,用于执行这些启动任务的所有类仍然存在!虽然这些类占用的任何内存最终都应该被垃圾收集,但前提是这些类在工作完成时正确释放了对内存的控制,启动类的类元数据仍然存在于JVM中,即使是在最新的Java版本上。这些启动类可能看起来不太多,但几百个不再需要的类可能会影响JVM所需的内存量。
这个问题影响到今天所有的JVM,没有框架进行特殊处理。只有当类的所有对象都可以被垃圾收集,对该类的所有引用都被删除,而且最重要的是,同一个类加载器中的所有其他类也不再被引用时,JVM才能对启动期间使用的所有类进行垃圾收集,而且永远不会再次使用。为了方便启动类的垃圾收集,框架需要为启动类使用一个类加载器,为运行时类使用另一个类加载器。在使用线程池(尤其是ForkJoinPool)时,以及在启动期间设置线程局部变量时,很难为启动类启用垃圾收集。
如果类对象永远不再被使用,我们为什么要在JVM内存中保留它们?理想情况下,我们不应该这样做,因为这是浪费。这就是Quarkus闪耀的地方。Quarkus扩展的设计和构建旨在将传统框架启动处理的各个部分分离为更小的工作块。这样做使得Maven或Gradle的构建过程能够利用这些较小的工作块,并在构建过程中执行这些工作块,而不是等到运行时开始。在构建期间利用启动类意味着这些类不需要在运行时包含在JVM中!这为我们节省了内存和启动时间。
Quarkus在构建时如何帮助实现这一点,以及运行时需要的输出在哪里?Quarkus扩展使用字节码记录器(bytecode recorder)来完成一切工作,从为运行时的类设置静态值,到创建新的类来保存运行时所需的元数据。这是什么意思?前面我们讨论了框架在启动期间做了大量工作,Quarkus能够在构建期间创建该工作的输出,并编写字节码,与传统框架在启动时的结果相同。在运行时,JVM没有执行启动工作,而是将Quarkus扩展编写的类加载到内存中,就好像所有启动工作都刚刚发生,而不需要内存和类来完成这些工作。
查看传统框架在启动时执行的一些步骤,我们可以在图2-3中看到Quarkus如何以不同的方式处理它们。
图2-3:传统框架与Quarkus中的框架启动阶段
虽然Quarkus在构建时读取配置,但某些属性(如位置和认证凭据)仍在运行时配置和读取。然而,在构建时可以决定的一切以应用程序为中心的事情都是在构建过程中处理的。到目前为止,我们一直在使用构建时来描述Quarkus何时完成这些通常与启动相关的任务,但还有另一个术语:提前(AOT)编译。你已经看到,Quarkus与传统框架的不同之处在于其优化应用程序代码和依赖项的方式。是的,这种方法减少了通常在运行时处理的可变性。
然而,部署在云平台或容器中的现代工作负载不需要这种可变性,因为几乎所有东西都是在构建时就已知的。我们希望你现在能更清楚地了解Quarkus通过这种创新方法提供了什么,以及为什么Quarkus再次为云中的Java开发带来了兴奋。