Eclipse 原生化

作者:John Healy

Eclipse 是一个开源的、可扩展的集成开发环境 (IDE),其受欢迎程度正在快速增长。它用 Java 编写,提供了一个多语言开发环境,允许开发人员使用 Java、C 和 C++ 进行编码。为了响应对 Red Hat Developer Suite(Eclipse 是其核心)的改进性能和额外平台覆盖的需求,我们创建了一个原生编译的 Eclipse 版本。Red Hat 版本的 Eclipse 被编译为二进制文件,并使用 libgcj 运行时库原生运行,类似于 C 程序使用 GNU C 库的运行方式,而不是像 Java 程序通常那样在虚拟机之上运行——尽管如果用户喜欢,仍然可以这样做。

为了原生编译 Eclipse,Red Hat 的 Eclipse 工程团队使用了 GCJ,这是一个免费的、优化的、提前编译的 Java 编译器。GCJ 可以将 Java 源代码编译为原生机器代码,将 Java 源代码编译为 Java 字节码,以及将 Java 字节码编译为原生机器代码。我们采用的方法是使用 GCJ 将 Java 字节码编译为原生机器代码。

本文讨论了为什么原生编译是一个有吸引力的选择;解释了我们为了使 GCJ、libgcj 和 Eclipse 成为可能而必须做的事情;并使用一个真实的例子表明,开源 Java 已经取得了长足的进步,现在在商业上很有用。

动机

在 Developer Suite 早期规划和工程的最初阶段,有两个主要因素促使我们走向原生编译:平台覆盖和性能。Red Hat Enterprise Linux 计划在多个 64 位架构上发布,我们希望确保 Developer Suite 可以在所有这些架构上运行。一个大问题是 Eclipse 从未在 64 位平台上运行过,并且它包含一些代码,特别是 SWT(Eclipse 中的图形工具包)与其原生 C 库之间的接口,这些代码假定为 32 位地址。除了必须创建一个干净的 64 位版本的 SWT 之外,我们还面临一个更重要的问题:当时不存在用于 x86_64(AMD 的 64 位架构)的 64 位 Java 虚拟机 (JVM),并且在我们需要发布之前看起来不太可能出现。

我们遇到的另一个问题是性能。Eclipse 在 Microsoft Windows 上运行良好,但当时可用的版本在 Linux 上非常慢。我们发现仅启动就需要一分钟以上,早期的用户测试发现界面有点过于迟缓,无法舒适地使用。例如,Eclipse 基于透视图,透视图是视图和编辑器的集合,一次只显示一个。用户相当频繁地在它们之间切换。但是,更改透视图会引入我们认为对于 Red Hat Developer Suite 所针对的企业开发市场来说不可接受的相当大的延迟。

我们提出的解决方案是使用 GCJ 将 Eclipse 编译为原生二进制文件,这些二进制文件可以在没有安装 JVM 的情况下运行。我们知道原生编译将有助于解决性能问题,因为我们将不再有 JVM 层带来的开销。它还将解决平台覆盖问题,因为 GCJ/libgcj 在我们必须支持的所有 64 位平台上都可用,尽管在某些情况下,例如 x86_64,它仍然需要大量工作。原生编译解决了我们遇到的技术问题,并为我们带来了减少外部依赖性、允许我们对开源 Java 进行一些重大改进以及证明开源 Java 已经成熟到在商业上可用的程度的额外好处。

方法

在这个项目的开始,我们真的不知道是否可以使用 GCJ 编译 Eclipse 并期望它运行。首先,Eclipse 是一个大型程序——超过两百万行代码,由wc计数。我们对 Eclipse 内部结构或它可能使用的运行时工具知之甚少。其次,GCJ 的背景是嵌入式系统,我们知道 Java 编程语言的某些部分,特别是类加载器,Eclipse 大量使用了这些部分,还需要进行工作。第三,免费的类库并不完整。我们不知道 Eclipse 是否可以使用我们尚未编写的工具,甚至不知道 Eclipse 是否会违反规则并使用内部的、未文档化的 com.sun.* 接口,就像太多 Java 程序似乎做的那样。

因此,我们采取了双管齐下的方法来确定像这样的项目是否可以成功。首先,我们使用 GCJ 创建了一个 Eclipse 使用但我们没有或无法实现的 API 列表。为了完成这项工作,我们编写了一个 shell 脚本,该脚本将尝试将每个 Eclipse Java 归档库(jar 文件)编译为目标代码。然后,我们查看错误消息以查看缺少什么。这个脚本的结果并不令人鼓舞:我们发现了大量缺失的包。尽管如此,还需要进行更多调查,因为有些事情没有道理。例如,存在对 Swing 图形用户界面类的依赖,但我们知道 Eclipse 使用的是 SWT 而不是 Swing。

进一步调查表明,许多奇怪的未定义引用不是来自 Eclipse 本身,而是来自随附的第三方 jar 文件。例如,Eclipse 包含其自己的 Ant 构建工具副本和其自己的 Apache Tomcat 动态 Web 服务器副本。我们知道在许多情况下,引用的类实际上不会在 Eclipse 环境中被调用。这鼓励我们重新审视如何让 Eclipse 工作。

我们的第二个攻击角度是尝试使用 libgcj 附带的字节码解释器运行 Eclipse。我们推断,通过这样做,我们将专注于运行时错误,包括前面提到的类加载器问题和 Eclipse 实际使用的缺失功能。

这种方法最初也令人沮丧。我们不仅遇到了类加载问题,还遇到了 libgcj 的保护域实现需要改进的事实。这些是 Java 安全沙箱架构的基础,该架构允许以安全的方式运行不受信任的代码。这方面的问题具有不幸的阴影效应——我们必须修复每个错误,然后才能发现下一个错误。

对 libgcj 的更改

我们对 libgcj 的第一轮更改仅是错误修复。我们正确地实现了保护域。然后,我们遍历了整个运行时,修复了与类加载相关的错误。由于 libgcj 中类加载的实现方式,我们不得不修改本机代码中所有可能加载类的位置,以将请求转发到相应的类加载器。

完成此操作后,我们能够使用 libgcj 字节码解释器启动 Eclipse。此时,问题变成了,我们如何才能真正利用 GCJ 来编译 Eclipse?

对这个难题的天真方法,即预编译所有类并将它们全部链接在一起,已被我们对 Eclipse 内部结构的调查排除。这种方法会与 Eclipse 相对复杂的类加载策略冲突。

更多调查显示,大多数类由 DelegatingURLClassLoader 的实例加载,它是标准 URLClassLoader 的子类,已扩展为理解 Eclipse 的插件架构。似乎最好的方法是修改 Eclipse,使其能够加载预编译的共享库以及字节码文件。我们推断,由于插件类加载的结构方式,所需的更改将是局部的。

实际上,我们不得不更进一步,也稍微扩展了 libgcj。libgcj 知道如何在响应对例如 Class.forName() 的调用时无形地加载共享库。但是,这种魔术总是发生在引导类加载器的级别。这对于 Eclipse 或任何其他定义自己类加载器的应用程序来说都不起作用,因此我们发明了一种新的 gcjlib URL 类型。这类似于 jar URL,但它指向一个共享库。我们还对 URLClassLoader 的实现进行了一些小的扩展,以便 gcjlib URL 将被特殊对待。

然而,这样做还不够。我们还必须解决链接问题。特别是,如果我们将 jar 文件编译为共享库,我们如何才能防止由于未解析的符号而导致 dlopen() 立即失败?解决这个问题的方法是恢复并清理 GCJ 中的 -fno-assume-compiled 选项。这个选项从未完成,它启用了一个替代 ABI,该 ABI 导致 GCJ 的输出在运行时而不是在链接时解析大多数引用。

-f-no-assume-compiled 选项有各种限制和低效率。未来计划中有一个更简洁的方法来实现相同的目标。在 GCJ 邮件列表(请参阅在线资源部分)中,此选项被称为二进制兼容性 ABI 或 -findirect-dispatch。这个新的 ABI 实现了 -fno-assume-compiled 所做的一切,但以更高效和兼容的方式。开发正在进行中,并且在这个新功能上进展顺利,这是为 GCJ 的企业就绪性做出贡献的几个功能之一。

对 Eclipse 的更改

一旦这一切就绪,我们终于准备好对 Eclipse 进行更改了。事实证明这些更改非常小。大部分工作涉及在三个不同的地方进行相同的更改。本质上,我们修改了 Eclipse,以便当它查找插件的 jar 文件时,它也会查找安装在其旁边的类似名称的共享库。如果存在共享库,我们会将传递给类加载器的 URL 从 jar URL 重写为 gcjlib URL。所有重写都是有条件地完成的,因此我们原生编译的 Eclipse 仍然可以使用未修改的 JVM。换句话说,如果用户宁愿使用 JVM,他们也不会被锁定为原生编译。

完成之后,我们编写了自己的启动器,该启动器了解如何从共享库引导 Eclipse 平台。这在一个适度的 90 行代码中完成。

性能分析

经过这一切,Eclipse 却出奇地慢。我们做错什么了吗?GCJ 编译的代码是否比当前流行的即时 (JIT) 编译器动态生成的代码差得多?-fno-assume-compiled 是否有巨大的开销?

GCJ 的一个很好的优势是,它的输出通常可以像对待任何目标代码一样对待。也就是说,诸如 OProfile 之类的现有工具可以直接应用于它,而无需进行任何更改。事实上,我们就是这样调查我们的性能问题的。

我们注意到的第一件事是在平台启动期间抛出了大量异常。在编译器编写者咕哝着(异常应该用于异常情况)声中,尽管我们正在考虑更改会违反 Java 语义的 GCJ 运行时,但我们在 OProfile 输出中注意到一个奇怪的符号。事实证明,libgcj 运行时深处的一小段有缺陷的汇编代码导致了异常处理表的线性搜索,而不是预期的二分搜索。每次抛出异常时,通过整个程序搜索的开销都非常巨大。对错误的汇编代码进行修复证明了这是问题所在,突然之间,我们原生编译的 Eclipse 能够比使用 JVM 的标准版本更快地启动一秒。为了进一步量化它,启动时间从修复前的超过一分钟下降到修复后的不到 15 秒。

局限性和无耻的 hack

目前,我们不直接从源代码编译 Eclipse 到目标代码。相反,我们编译为字节码,然后将 jar 文件编译为共享库。这样做有两个原因。首先,GCJ 源代码编译器中的一些错误尚未修复。其次,Eclipse 附带了自己的构建脚本,这些脚本从源代码编译为字节码。重新设计 Eclipse 构建系统以允许直接从源代码构建到二进制文件似乎比我们愿意维护的上游源代码的偏差更大。

此外,我们目前没有将所有 jar 文件预编译为共享库——有些仍然是 jar 文件,并在运行时解释。这样做是因为类库仍然不完整,并且这些 jar 文件引用了尚未实现的类。

我们的一个补丁不适合公共 GCJ。我们不得不禁用编译时字节码验证器,因为它存在太多错误,无法编译某些 Eclipse jar 文件。我们正在用更强大的验证器替换这个验证器。

此外,原生编译的 Eclipse 的一个局限性值得一提。您不能使用原生编译的 Eclipse 来调试 GCJ 编译的应用程序,因为 Eclipse 使用的 Java 调试线协议 JDWP 尚未在 libgcj 中实现。

意义和未来方向

Eclipse 原生编译的成就是开源 Java 基于 GCJ 和 libgcj/classpath 已经达到商业可用水平的有力迹象。话虽如此,它仍然不完整。在开源 Java 能够成为专有 JVM 的适当替代品之前,仍然需要填补一些相当大的空白。

需要改进的主要领域之一是 JIT 编译器的开发/集成。JIT 将允许基于 GCJ 的开源 Java 环境以类似于传统 JVM 的方式使用,这意味着出于性能原因,原生编译和特定于平台的二进制文件将不是必需的。

另一个需要改进的主要部分也是迄今为止最明显的缺失部分——Swing。作为 GNU Classpath 项目的一部分,开源 Swing 实现的工作进展顺利,但 Swing 是一项巨大的工程,GNU Classpath 实现仍然不太可用。

一个功能齐全且完全开源的 Java 环境是专有 JVM 的有吸引力的替代方案,现在触手可及。在过去的六个月中,Red Hat 已将支持开源 Java 解决方案和社区的工程师数量增加了一倍以上。Eclipse 是一个大型、复杂的软件,原生编译和运行它是对开源 Java 正在取得的进展的极好测试和证明。开源的力量在于其社区,因此请考虑加入开源 Java 社区,并以任何您感兴趣的方式为 GCJ 和 GNU Classpath 项目做出贡献。

本文资源: /article/7549

John Healy 是 Red Hat Eclipse 工程组的经理,总部位于多伦多 (people.redhat.com/jhealy)。过去,他曾从事嵌入式处理器的定制开源工具链以及 CRM 和计算机电话应用程序方面的工作。

Andrew Haley 成为程序员的时间比他记得的还要长。他是 GCJ 的维护者之一。他在 Red Hat 工作,Red Hat 支持他完成这项任务。

Tom Tromey 自 1990 年代初期以来一直从事自由软件工作。他的补丁出现在 GCC、Emacs、GNOME、Autoconf、GDB 以及他可能已经忘记的其他软件包中。他在 Red Hat 担任 Eclipse 工程团队的技术主管。可以通过 tromey@redhat.com 与他联系。

加载 Disqus 评论