思考Java:老矣,但是否仍能胜任?

发表时间: 2023-05-12 12:06

1000万云上开发者,全栈云产品0元试用:点击「链接」免费试用,即刻开启云上实践之旅!


本文就Java真的老了吗展开讲述,诠释了作者作为一名Java开发者的所思所感。

作者 | 傅健丰(健风)

来源 | 阿里开发者公众号


最近抽空看了Go、Rust等一些语言的新版本特性,还有云原生的一些基础设施(Docker,Kubernetes,ServiceMesh,Dapr,Serverless),有点感慨Go真的是云原生的“一等公民”,像是启动速度快、依赖少、内存占用少、Goroutine 并发等无一不是击中Java的软肋。然后突发奇想在Google上搜了下“Java老矣”,能搜出520,000条结果。不禁想问:Java真的老了吗?


“落寞”的Java

自1995年出生以来,Java已经有27年历史了,曾经的风吹雨打风吹去,一些优秀的设计在今天看来似乎并不那么重要甚至过时了。比方说:

  • "Write Once, Run Everywhere"的平台无关特性在当年确实是真香,但现在这种部署的便利性已经完全可以交由Docker为代表的的容器提供了(从某种意义上说,JVM也是字节码的容器),而且做得更好,可以将整个运行环境进行打包。想想Docker的口号也是:"Build Once, Run Anywhere"。
  • Java 总体上是面向大规模、长时间运行的服务端应用而设计的。在语法层面,Java+Spring框架写出的代码一致性很高;在运行期,有JIT编译、GC等组件保障应用稳定可靠。这些特性对于企业级应用十分关键,曾经是Java最大的优势之一。但在微服务化甚至Serverless化的部署形态下,有了高可用的服务集群,也无须追求单个服务要 7×24 小时不可间断地运行,它们随时可以中断和更新,Java的这一优势无形中被削弱了。

另一个广为诟病的是Java的资源占用问题,这主要包含两方面:静态的程序大小和动态的内存占用。

  • 不管多大的应用,都要随身带一个臃肿的JRE环境(这里先不讨论模块化改造),加上各种复杂的Jar包依赖,看了下我们团队的每个Java应用的容器镜像大小都轻松上G。
  • 应用的运行期内存占用居高不下,这个是Java天生的缺陷,很难克服。

Java的启动时间也是一大心病,主要原因在于启动时虚拟机初始化和大量类加载的时间开销(当然还有一个罪魁祸首是Spring的bean初始化,我之前写了个异步初始化Spring Bean的starter rhino-boot-turbo,把串行改并行启动速度会快很多)。本身镜像体积大,拉取时间就长,再加上分钟级的启动时间,部署应用就更显得慢了。传统的企业应用更看重长时间运行的稳定性,重启和发布频率相对较低,对启动时间相对没那么敏感,然而对于需要快速迭代、水平扩展的微服务应用而言,更快的的启动速度就意味着更高的交付效率和更加快速的回滚。尤其是对于Serverless应用或函数,冷启动速度至关重要,之前看AWS Lambda函数允许最多运行5分钟,很难想象还要花一分钟时间先启动。

云原生的潮流滚滚而来,Java的这些缺陷在要求快速交付的大环境下显得格格不入,难怪Java与Go、Rust等原生语言相比,会显得“落寞”了。

作为一个Java程序员,肯定想问,Java还有机会吗?想起有位长者说过:一个人的命运啊,当然要靠自我的奋斗,另一方面,也要考虑历史的进程。我想把它改成:Java的命运啊,当然要靠自身的努力,另一方面,也要考虑队友们给不给力。


JDK的演进

我们的大部分系统都还跑在Java 8之上,因此作为开发同学对Java 8也是最熟悉的。从Java 9开始,JDK的版本号堪比版本狂魔Chrome涨得飞快,除去开发者能够肉眼感知的语法和API的变动(Productivity)之外,Java也在性能(Performance)上一直努力。

我捋了一下OpenJDK官网[1]从Java 9开始的JEP列表,按照个人理解列出了关键的一些特性。

Java 9:难产的模块化

在数次delay之后,Java 9终于正式引入了Java平台模块系统(JPMS),项目代号Jigsaw。在这之前,Java以package对代码进行组织,再将package和资源打成Jar包,模块则在package的概念上将多个逻辑上、功能上相关的包以及相关的资源文件封装成模块。关于模块的详细介绍,可以参考下官方的介绍文档:Understanding Java 9 Modules[2]。

此前,Java Runtime的庞大臃肿一直为人诟病(一个rt.jar就有60多M,整个JRE环境可以达到上百M),瘦身正是Project Jigsaw的目标[3]之一。此外,还有Jar Hell、安全性等等问题。

不过模块化看着很好,也隐藏着陷阱:

  • 不可忽视的改造成本
  • 虽然提供了未命名模块和自动模块,Oracle也提供了迁移指南和工具[4]供参考,但改造的成本依旧很大,特别是梳理模块之间的依赖关系,较为繁琐。
  • 小心使用内部API
  • 模块化的最大卖点之一是强大的封装性,它确保非public类以及非导出包中的类无法从模块外部访问。但在这之前,jar包中类的访问是没有限制的(即使是private也可以通过反射访问)。比如JDK中的大部分com.sun.*sun.*包是内部无法访问的,但这之前被用得很多(出于性能/向前兼容等等原因),虽然Oracle的建议是不要使用这些类:Why Developers Should Not Write Programs That Call 'sun' Packages[5]。
  • 小心使用内部JAR
  • 像lib/rt.jar和lib/tools.jar等内部 JAR不能再访问了。不过正常来说,应该只有IDE或类似工具会直接依赖?
  • 小心使用JAR中的资源
  • 一些API会在运行期获取JAR中的资源文件(例如通过ClassLoader.getSystemResource),在Java9之前会拿到 jar:file:<path-to-jar>!<path-to-file-in-jar>这类格式的URL Schema,而Java9之后则变成了 jrt:/<module-name>/<path-to-file-in-module>
  • 其他一些问题[6]


对于新的项目,使用模块构建似乎是值得的,但现状是,大多数开发者会忽略模块系统,尤其是对于已经运行了多年的大型项目,改造的成本令人望而却步。我猜测肯定会有人吐槽类似的问题:

  • 我已经分成不同jar包了,我感觉这样就可以了,有必要更进一步吗?
  • 我又不是开发中间件和框架的,我开发业务应用,为什么要关心这些?
  • 就算我有二方包要开放出去,为二方包维护模块定义似乎也带不来多少收益?
  • 该如何分离每个模块,基于什么原则?就跟DDD一样,我知道这东西很美好,有最佳实践可以参考吗?

搜了一下,似乎国外网友也有一样的疑惑[7]。不过,我认为让程序员可以定义应用程序的模块是什么,它们将如何被其他模块使用,以及它们依赖于哪些其他模块,这些事情还是有必要做的。

当然Java9除了模块化之外,还有一些其他特性也值得关注:

  • compact strings[8],通过对底层存储的优化来减少String的内存占用。String对象往往是堆内存的大头(通常来说可以达到25%),compact string可以减少最多一倍的内存占用;
  • AOT编译[9],一个实验性的AOT编译工具jaotc[10]。它借助了Graal编译器,将所输入的Java类文件转换为机器码,并存放至生成的动态共享库之中。jaotc的一大应用便是编译java.base module(也就是模块化后Java核心类库中最为基础的类)。这些类很有可能会被应用程序所调用,但调用频率未必高到能够触发即时编译。
  • JVMCI[11]( JVM 编译器接口),另一个experimental的编译特性。用Java写Java编译器,Java也可以说我能自举了!


关于 JVMCI 多介绍一些。相比用 C 或 C++ 编写的现有编译器(说的就是你,C2),用Java写编译器更容易维护和改进。JVMCI的API 提供了访问 JVM 结构、安装编译代码和插入 JVM 编译系统的机制,后面讲到的Graal正是基于JVMCI。

JVMCIJIT编译器与JVM的交互可以分为如下三个方面。

响应编译请求;

获取编译所需的元数据(如类、方法、字段)和反映程序执行状态的profile;

将生成的二进制码部署至代码缓存(code cache)里。

即时编译器通过这三个功能组成了一个响应编译请求、获取编译所需的数据,完成编译并部署的完整编译周期。


传统情况下,即时编译器是与Java虚拟机紧耦合的。也就是说,对即时编译器的更改需要重新编译整个Java虚拟机。这对于开发相对活跃的Graal来说显然是不可接受的。


为了让Java虚拟机与Graal解耦合,引入 JVMCI 将上述三个功能抽象成一个Java层面的接口。这样一来,在Graal所依赖的JVMCI版本不变的情况下,我们仅需要替换Graal编译器相关的jar包(Java 9以后的jmod文件),便可完成对Graal的升级。


其实JVMCI接口就长这样:


public interface JVMCICompiler {
/**
* Services a compilation request. This object should compile the method to machine code and
* install it in the code cache if the compilation is successful.
*/
CompilationRequestResult compileMethod(CompilationRequest request);
}


剩余60%,完整内容请点击下方链接查看:

一些杂想:Java老矣,尚能饭否?


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。