本文是“Native Compilations Boosts Java”系列文章的一部分。你可以通过订阅RSS接收更新通知。
Java 主导着企业级应用。但在云计算领域,采用 Java 的成本比它的一些竞争对手更高。原生编译降低了在云端采用 Java 的成本:用它创建的应用程序启动速度更快,使用的内存更少。
那么,Java 用户的问题来了:原生 Java 是如何改变开发方式的?我们在什么情况下应该切换到原生 Java?什么情况下又不应该切换?我们应该使用什么框架?本系列文章将回答这些问题。
原生 Java 对于 Java 在不断演进的云世界中保持相关性至关重要
二十多年来,Java 一直是企业应用程序和网络服务的首选开发语言。它拥有一个非常丰富的中间件、库和工具生态系统,以及一个由经验丰富的开发人员组成的庞大社区。因此,它成为开发基于云的应用程序或将现有 Java 应用程序迁移到云端的明智之选。然而,Java 及其运行时的发展与今天的云计算需求之间存在不匹配的地方。因此,Java 需要做出改变才能在云计算中保持相关性!原生 Java 是最有前途的选择。让我来解释一下传统 Java 和云计算之间的不匹配之处。
Java 虚拟机(JVM)使用自适应即时(JIT)编译来最大化长生命周期进程的吞吐量。峰值吞吐量一直有着最高的优先级,内存便宜且可扩展,启动时间不太重要。
现在,像 Kubernetes 或 OpenShift 这样的基础设施,以及亚马逊、微软或谷歌提供的云服务,都可以通过小型廉价的容器(使用很少的 CPU 和内存资源)进行扩展。由于容器启动的频率更高,固定的 JVM 启动成本在总运行时间中所占的百分比变得更加显著。另外,Java 应用程序仍然需要内存来进行 JIT 编译。那么,如何让 Java 应用程序在容器中高效运行呢?
首先,越来越多的 Java 应用程序以微服务的形式运行,工作负载比单体应用程序要少。所以,它们的应用程序数据集更小,使用的内存也更少。
其次,一些框架(如 Quarkus 和 Micronaut)用离线转换的方式来注入所需的服务,而不是在启动时进行非常消耗资源的动态注入和转换。
第三,缩短 JVM 启动时间已经被证明是非常困难的。我们最好避免在每次启动时预先计算相关的已编译代码、元数据和常量对象数据。OpenJDK 已经尝试过多次,最著名的是 jaaotc AOT 编译器和类数据共享。不过 jaotc 已经被放弃了,类数据共享仍在进行当中。OpenJ9 是一个不同于 OpenJDK 的 Java 实现,它在 AOT 编译方面取得了一些显著的进展,但还没有被广泛采用。
做好这些优化是很难的,因为 JDK 运行时也是位于底层硬件和操作系统之上的一个抽象和可移植层。预先计算可能会带入一些构建时假设,而这些假设在运行时不再有效。这个问题可以说是原生 Java 面临的最大挑战。这就是为什么之前的努力都集中在为相同的运行时预生成内容上。不过,Java 的动态特性又带来了两个阻碍性问题。
首先,与其他 AOT 编译语言相比,JVM 和 JDK 维护了一个相对丰富的元数据模型。保留类结构和代码信息有助于运行时在加载新的类文件时对代码进行编译和重新编译。这就是为什么对于一些应用程序来说,元数据占用的资源相对于应用程序数据来说同样重要。
第二个问题是,大多数预先生成的代码和元数据链接必须是间接的,这样就可以在以后发生变更时重写它们。这样做的代价是双重的:将预生成的内容加载到内存中需要链接引用,而使用间接访问和控制传输会降低应用程序的速度。
原生 Java 通过一个直接的简化策略来解决这些问题:不支持动态演化的应用程序。这种策略就是让紧密链接的小型可执行文件在启动时预先计算所有的初始代码、数据和元数据的,以此来实现快速启动和较少的资源占用。这确实是一种解决方案,但也付出了一些代价。况且,它也不能解决匹配构建时假设与运行时配置的问题。
原生 Java 还有待完善
乍一看,打包方式似乎是 GraalVM Native 和 JVM 的主要区别。JVM 应用程序需要目标主机安装 Java 运行时,包括 Java 二进制文件、各种 JVM 库、JDK 运行时类和应用程序 JAR 文件。
相比之下,GraalVM Native 将这些 JAR 文件作为构建时的输入,并加入 JDK 运行时类和一些额外的 Java 代码,以提供与 JVM 等价的功能。它将所有这些东西编译并链接成某个目标 CPU 和操作系统的原生二进制文件。这个二进制文件不需要进行类加载或 JIT 编译。
这个完整的 AOT 编译有两个关键细节:首先,它需要知道所有将要被执行的方法所属的类。其次,它需要对目标 CPU 和操作系统有详细的了解。这两个要求都带来了重大挑战。
封闭式世界假设
第一个要求也被称为封闭式世界假设。封闭式世界应该只包含实际运行的代码。要封闭一个应用程序,首先要确定 main 方法显式引用了哪些类和方法。我们可以对类路径和 JDK 运行时中的所有字节码代码进行相对简单的分析。不幸的是,仅通过名字来跟踪对类、方法和字段的引用是不够的。
链接——Java 提供了类、方法和字段的间接链接,但不显式提到它们的名字。实际链接的内容取决于复杂的应用程序逻辑,这对 AOT 分析来说是不透明的。类加载方法,如 Class.forName(),可以加载在运行时计算出来的类名。字段和方法可以通过反射或方法句柄和 var 句柄进行访问,同样,它们的名字也可以通过计算获得。智能分析可能可以检测出使用了字符串字面量的名字,但无法检测动态计算的值。
字节码生成器——更糟糕的是,类可能是通过应用程序基于输入数据或运行时环境生成的字节码来定义的。一个相关的问题是字节码的运行时转换。在最好的情况下,我们可以使用 AOT 编译的等效程序修改其中的一些应用程序,但要修改这一类的应用程序是不可能的。
加载器和模块委托——这不仅仅是个什么类或代码可用的问题。即使我们可以准确地知道哪些类可能会被加载,应用程序逻辑仍然可以确定类的链接和可见性。同样,这类应用程序也不能使用 AOT 编译。
资源和服务加载——在加载类路径资源时也会遇到类似的问题。我们可以在类路径 JAR 中识别出资源,并将它们放入原生二进制文件中,但可能并不清楚实际将用到哪些,因为它们的名字可能会在运行时动态地计算出来。这一点特别重要,因为它会影响到 JDK 运行时的服务加载模型,包括函数的动态加载,比如 FileSystemProvider 或 LocaleProvider。AOT 编译器可以在编译时为每一个可能的选项提供支持,但这是以牺牲可执行文件的大小和内存占用为代价。
封闭式世界假设对开发者的影响
所有这些都意味着开发人员现在必须保证目标系统所需的所有代码在构建时可用。GraalVM 最近修改类路径资源的处理方式就是一个例子。最开始,在构建时缺失某些类会导致构建终止。--allow-incomplete-classpath 选项可以暂时解决这个问题,它将构建时配置错误转换成运行时错误。GraalVM 最近将这个临时解决方案变成了默认行为。虽然这样可以顺利地将应用程序变成原生 Java,但由此产生的运行时错误加长了编译、测试、异常、修复的周期。
封闭式世界还存在“Day 2”成本。监控工具通常会在运行时对类进行插装。理论上,这种插装可以在构建时进行,但在实际当中可能很难实现,甚至是不可能的,特别是对于那些特定于当前运行时配置或运行时输入数据的代码来说。对原生可执行文件的监控机制正在改进,但现在的开发者不能依靠他们常用的工具和工作流来监控原生部署。
构建时与运行时编译器配置
第二个需求是 AOT 编译的一个常见问题:它要么针对的是目标环境的特定硬件和运行时,要么为一些列目标环境生成代码。这增加了编译过程的复杂性:开发人员必须在构建时选择和配置编译器选项,而这些选项原本会采用默认值或在程序启动时配置。
这与其他 AOT 编译语言(如 C 语言或 Go 语言)不一样,它不仅仅是针对目标硬件或操作系统(如 Linux)的问题,它还需要对特定的 Java 实现进行高级配置,比如垃圾回收器或应用程序语言环境。
后面这些是必需的,因为将所有功能编译到可执行文件中会使其比动态 Java 更大、更慢。JIT 编译器会为当前硬件和运行时环境的特定功能生成代码。相比之下,AOT 编译器不得不引入条件性代码或为方法生成多种变体,以便覆盖所有可能的情况。
构建时编译配置需求对开发者的影响
AOT 编译使持续集成(CI)变得更加复杂。希望同时支持 x86-64 和 aarch64 架构的 Linux 部署?这会导致 CI 系统的编译时间增加一倍。还要为 Windows 和 macOS 平台的开发人员构建原生可执行文件?CI 编译时间又增加了一倍。所有这些都导致时间增加,直到拉取请求准备好合并。
这在未来只会变得更糟。测试不同的 GC 策略?这需要一个完整的编译周期,而不只是进行简单的命令行切换。验证压缩引用对应用程序堆大小的影响?这需要另一个完整的编译周期。
这些持续不断的麻烦剥夺了开发人员的乐趣。它们降低了实验速度,让结果收集变得十分费力。部署时间的增加导致将变更发布到生产环境和故障恢复出现延迟。
构建时初始化
实际上,对于通过 AOT 编译缩短启动时间和减少占用空间,还有第三个关键需求。原生可执行文件没有类加载器或 JIT 编译器,有更轻量级的 VM、更少的类和代码元数据,但 AOT 编译并不一定意味着类或方法会更少:在大多数情况下,JVM 运行时已经是只加载必需的代码。因此,AOT 编译器并不会大幅减少运行时代码量或运行代码所需的时间。所以需要更激进的策略,要么删除代码,要么用占用更少空间和执行时间的等价代码替换。
AOT 编译最关键的创新恰恰做到了这一点:在应用程序启动期间,JVM 的大部分工作是为静态 JDK 运行时状态初始化代码——其中大部分代码在每次启动时都是完全相同的。在构建时计算这些状态并将其包含在原生可执行文件中可以极大地改进启动速度。这个策略同样适用于中间件和应用程序状态。
因此,构建时初始化在构建时做了这些事情,消除了运行时负担。一些构建时初始化的代码也可以从原生可执行文件中移除,因为它们只在构建时运行。在许多情况下,移除其他方法和类会产生连锁反应,因为它们只在启动期间被调用。这种组合的效果最大限度地减少了 GraalVM 的启动时间和内存占用。
不幸的是,构建时初始化面临的问题不会比前两个需求少。大多数静态初始化都很简单,就是将字段设置为常数或某些确定性计算的结果,这些值在任何硬件上的任何运行时环境中都是相同的。
但是,有一些静态字段依赖了运行时细节。静态初始化器可以执行任意代码,包括那些依赖精确的初始化顺序或时间、硬件或操作系统配置、应用程序数据输入的代码。当无法实现构建时初始化时,运行时初始化就会介入。这种决策以类为最小单位:只要有一个字段不能在构建时初始化,就会在运行时初始化整个类。
静态字段的值也可能依赖其他静态字段。因此,构建时初始化需要进行全局的分析,而不是局部分析。
构建时初始化对开发者的影响
虽然构建时初始化是原生 Java 的一项超能力,但它很可能会持续为开发人员带来复杂性。每一个需要构建时初始化的静态字段让构建时初始化像波浪一样在需要创建字段值的类中移动。
举个例子:
假设 BTIExample 类是在构建时初始化的,那么它所有的超类和实现的接口以及静态初始化器引用的类都必须在构建时初始化:BTIExample、Object、B、A、IFoo、Logger、String、Logger 的超类。calculate_constant()方法和 Logger.getLogger()以及 B 的构造函数(未显示)所使用的类也需要在构建时初始化。
其中任何一个类(或它们依赖的类)发生变化都可能导致无法在构建时初始化 BTIExample。构建时初始化可以看作是在依赖关系图上传播的病毒。为了支持另一个类或者避免某些类在运行时初始化,看似无害的错误修复、重构或库升级都可能导致更多的类需要构建时初始化。
但是,构建时初始化也可能会捕获太多构建环境的信息。例如,捕获构建机器的环境变量,并将其保存在构建时初始化类的静态字段中。这在现有的 Java 类中非常常见,主要是为了确保字段值的一致性,并避免重复获取。但是在原生 Java 中,这可能会带来安全风险。
开发生命周期也需要做出调整
原生 Java 不仅仅改变了应用程序部署,开发过程也随之发生了变化:你不仅需要从开发者角度考虑采用新的框架、最小化反射和其他动态行为,并最大限度地利用构建时初始化的优势,你还需要检查构建和测试过程。
你需要知道如何初始化你的开发库,因为一个库的构建时初始化可能需要(或被阻塞!)另一个库。在构建时捕获的每一个状态都需要进行验证,确保不会捕获到安全敏感信息,并且对未来的所有执行都有效。
将初始化工作放到构建时还意味着在本地和 CI 系统中构建应用程序将花费更长的时间。AOT 编译需要配备大量 CPU 和内存的机器来全面分析程序的每一个元素。原生 Java 显然存在这种权衡——编译时间并不会减少,只是从运行时的 JIT 编译转移到构建时的 AOT 编译。它还需要更长的时间,因为构建时初始化的封闭式分析和验证比 JIT 编译要复杂得多。
正如这篇关于Quarkus的文章所说的,我们最好是在动态 JVM 上运行测试用例。这样可以测试你的业务逻辑,运行单元测试和集成测试,并确保所有东西在动态 JVM 上都可以正确运行。
然而,测试原生可执行文件仍然是必要的:正如其他一些文章所说的,使用 GraalVM 原生镜像构建的基于框架的封闭式应用程序可能会缺少一些东西,并且原生 Java 并不保证与动态 Java 虚拟机 Bug 兼容。
现在,我们通常不能针对单独的原生可执行文件执行单元测试,因为反射所需的方法可能没有被注册,或者可能已经从原生代码中删除了。但是,将单元测试包含在原生可执行文件中会导致其他方法也被包含在里面,从而增加了文件体积和安全攻击面。
因此,我们必须针对动态 JVM 和原生可执行文件执行测试。你在日常开发工作中会做些什么?只针对动态 JVM 做测试?在打开拉取请求之前编译成原生可执行文件?这些改变将影响你的内部开发循环速度。
说到速度,较长的编译时间也将影响 CI 管道运行的速度。更长时间的构建和测试周期是否会改变你的 DevOps 指标,比如同步恢复时间(MTTR)?也许吧。使用更强大的编译机器是否增加了 CI 成本?也许吧。使用现有的应用程序性能监视(APM)工具(如 Datadog)和其他插装代理是否会增加复杂性?当然会。
这里存在一些权衡。将一些工作转移到构建时(并扩展到开发时)是一种选择:它带来了运行时好处,但成本不会消失。采用原生 Java 需要做出大量的改变。虽然这样做是有好处的,但并不是所有的场景都值得这么做。认真思考并做好准备,需要做出改变的不仅是你的软件,还有你的开发方式。
通过 Leyden 来实现标准化是原生 Java 取得成功的关键
标准化是 Java 的一项超能力,它是整个 Java 生态系统的基石。标准化可以确保无论有多少种不同的 JDK 实现,在概念层面都只有一种 Java 语言和一种 Java 运行时模型。相同的应用程序可以运行在任意一个 JDK 上——无论它是 OpenJDK 的衍生版本还是 Eclipse OpenJ9 这样的独立实现。这为开发人员带来了信心:平台“就是好用”。再加上 Java 长期以来对向后兼容性的承诺(旧的 Java 1.4 JAR 文件今天仍然可以在 Java 18 上运行),我们看到了框架、库和应用程序生态系统的蓬勃发展。Java 持续的、谨慎的、有标准支持的发展是这种增长的关键。
Java 的标准化承诺你的应用程序可以一直运行下去。这个生态系统是值得投入的。这些库持续有效,不要求你对每一个版本做出重大修改。那些框架不需要在每次发布时都重新发明轮子。应用程序开发人员可以专注于增加业务价值,而不是一直被不兼容的变更占用了精力。并不是所有的编程语言生态系统都能保证这些。随着 Java 版本的快速和频繁发布,这些保证对于 Java 保持演进而又不失去用户是至关重要的。
这些承诺听起来很棒,但“标准化”是如何实现的呢?当我们提到“Java 标准”时,实际指的是 Java 语言规范(JLS)和 JVM 规范(JVM),以及核心 JDK 运行时类的 JavaDoc 规范。这两个规范是 Java 所提供的保证的核心,因为它们(而不是实现)定义了“Java”。它们非常详细地定义了语言和运行时行为,因此实现者可以独立实现 JVM 和 Java 编译器。相比之下,其他许多语言的标准更多地被视为他们已实现的东西的文档,而不是作为他们应该如何实现的指示。
每个 JDK 版本都是基于规范的附带更新。这种定期的修订创建了两个关键的 Java 可交付成果:对给定版本的 Java 和 JDK 的确定性行为的明确性声明,以及对不同版本之间的行为差异的可见性说明。这种清晰性和可见性对于任何实现和维护 Java 应用程序、中间件和库的人来说都是至关重要的。
Java 规范的更新甚至比新的 OpenJDK 版本更重要。每一个伴随规范演进的新特性都不能破坏现有的应用程序,需要有详尽的测试套件来检查实现与规范的一致性。关键在于,这些测试是基于规范而不是实现。
原生 Java 已经逃离了这个规范过程,正在分裂这个生态系统。这不是有意而为之的,但如果原生 Java 的发展继续独立于 Java 平台的其他部分,那么分离将会继续。现在,框架和库开发者必须努力粉饰这种分裂,这是不可避免的。他们只依赖原生 Java 的特性,避免使用大多数动态特性,如反射、MethodHandle,甚至动态类加载。这有效地形成了动态 Java 的一个子集。再加上一些特性(如 Finalizer、Signal Handler 或类初始化)的语义发生了变化,动态 Java 与原生 Java 之间的分歧越来越大。
而且我们不能保证今天用原生 Java 构建的应用程序的行为到下一个版本原生 Java 时能够保持一致。一些主要的行为——比如前面讨论的--allowed-incomplete-classpath 改动,以及将默认行为从构建时初始化改为运行时初始化——在不同版本之间发生了变化。这些选择以牺牲当前用户为代价来提高采用率,它们可能不是糟糕的决定,但破坏了原生 Java 生态系统的稳定性,因为它们消除了 Java 标准化的承诺。
原生 Java 的许多行为——尤其是像构建时初始化这样的关键特性——仍然处于变化之中。这很好,甚至连动态 Java 也在发生改变!原生 Java 缺少的是关于什么是可行的、什么是不可行的以及如何发生改变的明确声明。如果说原生 Java 的边界留了很大的空白,似乎没有什么问题,只是我们不知道边界在哪里,它正在以一种未知的方式发生转变。
缺乏标准化不仅仅是框架和库开发者的问题,因为这些看似实用的变更会影响原生 Java 及其保证能力的稳定性。应用程序开发人员在每次发布时都要验证他们的应用程序——尤其是它的资源使用情况时,这会让他们感到痛苦。现在,动态 Java 还需要对新版本做一些验证,但这通常需要来自特定用户的响应,并且只会带来边际性能成本。原生 Java 可能需要持续进行调优,或者承受部署成本的增加——开发人员在每次更新时都需要支付的税费。
正如 Mark Reinhold 在2020年4月所说,Leyden 项目的任务是解决 Java 的“启动时间慢、达到峰值性能时间慢、占用空间大”问题。最初,它的任务是将“静态镜像的概念引入 Java 平台和 JDK”。现在,Mark 在最近的一篇文章中重申了对这些痛点的关注,同时认识到在完全动态的 JVM 和原生 Java 之间存在着“一系列的约束”。Leyden 的目标是探索这个频谱,并确定和量化出完全 AOT 编译原生镜像和完全动态的 JIT 编译运行时之间的一个中间位置,在让用户可以选择保留应用程序所需的一些动态行为的情况下能够对内存占用和启动时间进行增量改进。
Leyden 将扩展现有的 Java 规范,为这个频谱中的不同点提供支持,包括原生 Java 所需的封闭式世界约束。原生 Java 的所有关键属性——封闭式世界假设、构建时编译和构建时初始化——都被赋予精确的、定义良好的语义,并置于标准化过程之中。Leyden 项目最近才创建了它的邮件列表,不过 Java 生态系统和 GraalVM 社区已经围绕这些主题进行了探索。
通过 Leyden 将原生 Java 引入到现有的 Java 标准化过程将为传统 Java、其生态系统、库和中间件的蓬勃发展提供同样坚实的基础。标准化是对原生 Java 和动态 Java 之间日益增长的技术债务的补救措施。Leyden 的增量路径将有助于缓解所有开发者的迁移痛点。
OpenJDK 需要引入原生 Java,以便与其他增强功能共同演进
这一系列文章展示了原生 Java 的优点。在云计算时代保持相关性对 Java 来说至关重要。这对 Java 社区来说有很多好处。但是,原生 Java 需要在应用程序的开发和部署方式上做出大量的变化。原生 Java 存在于核心平台的稳定性保证和标准化过程之外,所以它有与 Java 定义发生分歧的风险。
与此同时,动态 Java 在 OpenJDK 中继续演进。有一些主要的项目正在进行中:Loom 增加了轻量级线程和结构化并发性,Valhalla 引入了新类型,“写起代码像类,用起来像整型”,Panama 改进了 Java 与非 Java 代码的互操作方式,Amber 发布了可提升开发者效率的小特性。
这些项目为 Java 带来了新的功能,它们对 MethodHandle、invokedynamic 和运行时代码生成的进一步使用增加了平台的动态特性。它们被设计成一个连贯的整体。原生 Java 还不是这个连贯整体的一部分。通过 Leyden 项目将原生 Java 引入 OpenJDK 可以实现动态 Java、Java 的新特性和原生 Java 的共同演进。
这种共同演进对于原生 Java 取得长期成功来说至关重要。如果没有这种共同演进,原生 Java 将永远落后于动态 Java。这种滞后的一个例子是,如果在原生 Java 中使用了注解,那么Java记录的JSON序列化就会失败。但是 GraalVM 目前也错过了影响 Java 新特性设计的机会。对规范的微小调整都可能会造成简单高效的原生实现、高内存使用和长编译时间的实现以及原生 Java 无法实现的特性之间的差异。
到目前为止,原生 Java 在跟进平台方面是非常成功的。但它的成功是通过替代来适应 Java 平台和核心 JDK 库的:一些类经过修改,变成可与原生 Java 一起工作的 Java 伴生类。但它们冒着破坏被修改代码的不变量的风险。当不能修改原始代码时,替换是一种非常实用的解决方案。但它们没有被规模化。它们也面临着与其他语言的“猴子补丁”解决方案相同的问题——它们很强大,但也很危险。它们可能会因为所修改的类发生变化而变得不正确。最近的一个例子是 JDK 运行时替换,它在 JDK 发生变化后变得无效。所幸的是,Quarkus 团队发现并解决了这个问题。
将原生 Java 引入 OpenJDK 提供了“变得更好”的机会,通过修改 Java 平台,而不是使用替换技巧——不仅直接更新 JDK 类库,还潜在地更新编程模型。它确保现有的 OpenJDK 项目在开发特性时会检查整个平台——包括动态的和原生的场景。它还确保应用程序受益于作为平台一等公民的原生 Java,为两种部署模型带来更好的解决方案。
结论
在过去的 20 年里,Java 一直是占主导地位的企业级开发语言,它建立在标准化过程提供的稳定性基础之上。得益于摩尔定律,语言、运行时和库之间的协同演进一直在追求硬件的快速发展,这一切帮助努力追求让应用程序发挥最大性能的开发者简化了工作。
原生 Java 已经崛起,它让 Java 适应了资源受限的云端部署。但它现在正站在十字路口。它可以继续独立演进,每一次发布都有与动态 Java 分道扬镳的风险,直到它成为一个独立的实体,拥有自己的受众、社区和库。或者,原生 Java 也可以加入 Java 标准的旗帜之下,与平台的其他部分一起演进,成为对所有应用场景都有利的东西。这将为原生 Java 带来功能的稳定性,并促进通用部署实践的出现。
随着 Leyden 项目开始成形,我们希望它能够成为动态 Java 和原生 Java 塑造共同未来的地方,为所有 Java 用户实现更快的启动和更小的空间占用。今天,GraalVM 仍然是原生 Java 的可行选择。在不久的将来,将只有一个 Java 规范可以决定你的程序如何跨越动态和原生之间的频谱,并且与底层实现无关。
作者简介
Andrew Dinn 是红帽 Java 团队的杰出工程师。他是 OpenJDK 项目的评审人员,GraalVM 项目贡献者和 JBoss Byteman 项目负责人。Andrew 作为一名专业的程序员和学术研究员已经工作了近 40 年,其中最近 15 年是在红帽公司。他主要关注的是语言运行时虚拟机,但他的兴趣和经验也涵盖了系统软件的许多其他领域。
Dan Heidinga 是 Red Hat 的首席软件工程师。他活跃于各种各样的 Java 和 JVM 相关项目中。Dan 是 Eclipse OpenJ9 项目负责人、qbicc 项目贡献者和 OpenJDK 项目 CRaC 的提交者。他还参与了 Vahalla 和 Amber 计划。他还参与了关于编程语言和运行时应该如何工作的规范讨论。
原文链接:
评论