使用 GCJ 编译 Java
Java 并没有像最初宣传的那样普及,但它仍然是一种流行的语言,广泛用于内部和服务器端开发以及其他应用。Java 在自由软件领域的影响力相对较小,尽管现在许多项目都在使用它。使用 Java 的自由项目示例包括 Apache 基金会的 Jakarta (jakarta.apache.org)、W3C 的各种 XML 工具 (www.w3.org) 和 Freenet (freenet.sourceforge.net)。另请参阅 FSF 的 Java 页面 (www.gnu.org/software/java)。
相对较少的项目使用 Java 的一个原因是真实或感知的缺乏高质量的自由 Java 实现。然而,自 Java 早期以来,就存在两种自由 Java 实现。一种是 Kaffe (www.kaffe.org),最初由 Tim Wilkinson 编写,至今仍由他共同创立的公司 Transvirtual 开发。另一种是 GCJ(用于 Java 语言的 GNU 编译器),我于 1996 年在 Cygnus Solutions 开始开发(本文讨论的就是 GCJ)。自 GCC 3.0 版本以来,GCJ 已完全集成并作为 GCC 语言得到支持。
实现 Java 的传统方式是一个两步过程:翻译阶段和执行阶段。(在这方面,Java 类似于 C。)Java 程序由 javac 编译,生成一个或多个扩展名为 .class 的文件。每个这样的文件都是单个类中信息的二进制表示,包括类方法的表达式和语句。所有这些都已转换为字节码,字节码基本上是基于堆栈的虚拟计算机的指令集。(由于某些芯片也具有 Java 字节码指令集,因此它也可以是真实的指令集。)
执行阶段由 Java 虚拟机 (JVM) 处理,JVM 读取并执行 .class 文件。Sun 的版本称为普通的“java”。可以将 JVM 视为指令集为 Java 字节码的机器的模拟器。
使用解释器(模拟器)会增加相当多的执行开销。高性能 JVM 的常见解决方案是使用动态翻译或即时 (JIT) 编译器。在这种情况下,运行时系统会注意到某个方法已被调用足够多次,值得为该方法动态生成机器代码。将来对该方法的调用将直接执行机器代码。
JIT 的一个问题是启动开销。编译一个方法需要时间,特别是如果您想进行任何优化,并且此编译在每次应用程序运行时都会完成。如果您决定仅编译最常执行的方法,那么您还需要测量这些方法的开销。另一个问题是,好的 JIT 很复杂,并且占用相当大的空间(加上生成的代码需要空间,这可能是在原始字节码使用的空间之上)。这些空间中很少有可以放在共享内存中。
传统的 Java 实现技术也无法与其他语言很好地互操作。应用程序的部署方式不同(Java 存档 .jar 文件,而不是可执行文件);它们需要一个庞大的运行时系统,并且在 Java 和 C/C++ 之间进行调用既慢又不方便。
GCJ 项目的方法是彻底的传统。我们将 Java 简单地视为另一种编程语言,并以我们实现其他编译语言的方式来实现它。由于 Cygnus 长期以来一直参与 GCC,GCC 已经被用于编译多种不同的编程语言(C、C++、Pascal、Ada、Modula2、Fortran、Chill),因此考虑使用 GCC 将 Java 编译为本机代码是有意义的。
总的来说,编译 Java 程序实际上比编译 C++ 程序要简单得多,因为 Java 没有模板,也没有预处理器。类型系统、对象模型和异常处理模型也更简单。为了编译 Java 程序,程序基本上表示为抽象语法树,使用与 GCC 用于其所有语言相同的数据结构。对于每个 Java 构造,我们使用与等效 C++ 将使用的相同的内部表示,GCC 负责其余部分。
然后,GCJ 可以利用已经为 GNU 工具构建的所有优化和工具。优化的示例包括公共子表达式消除、强度缩减、循环优化和寄存器分配。此外,GCJ 可以进行比即时编译器更复杂和耗时的优化。然而,有些人认为,JIT 可以进行更量身定制和自适应的优化(例如,根据实际执行情况更改代码)。事实上,Sun 的 HotSpot 技术就是基于这个前提,并且它确实做得非常出色。说实话,由 GCJ 编译的程序运行速度并不总是明显快于在基于 JIT 的 Java 实现上运行的速度;有时甚至可能更慢,但这通常是因为我们没有时间在 GCJ 中实现 Java 特定的优化和调整,而不是 HotSpot 技术的任何内在优势。GCJ 通常比其他 JVM 快得多,并且随着人们对其进行改进,它变得越来越快。
GCJ 的一个巨大优势是启动速度和适度的内存使用量。最初,人们声称字节码比本机指令集更节省空间。这在某种程度上是正确的,但请记住,.class 文件中大约一半的空间被符号(非指令)信息占用。这些符号对于每个 .class 文件都是重复的,而 ELF 可执行文件或库可以进行更多的共享。但是,字节码真正输给本机代码的是在带有 JIT 的 JVM 中的内存方面。启动 Sun 的 JVM 并 JIT 编译和应用程序的类需要大量的时间和内存。例如,Sun 的 Java IDE Forte(在 NetBeans 开源版本中可用)非常庞大。启动 NetBeans 需要 74MB(由 top 命令报告),然后您才能真正开始做任何事情。Java 应用程序使用的主内存量使其部署变得复杂。JEmacs (JEmacs.sourceforge.net) 就是一个例子,这是我的一个(不太活跃的)项目,旨在用 Java 使用 Swing(以及下面讨论的 Kawa,用于 Emacs Lisp 支持)来实现 Emacs。使用 Sun 的 JDK1.3.1 启动一个简单的编辑器窗口需要 26MB(根据 top)。相比之下,XEmacs 需要 8MB。
使用 GCJ 与 JDK1.3.1 运行 Kawa 测试套件,GCJ 大约快两倍,导致大约一半的页面错误(根据 time 命令),并且使用大约 25% 更少的内存(根据 top)。测试套件是一个脚本,它多次启动 Java 环境,并运行太多不同的东西,以至于 JIT 无法提供帮助(这不利于 JDK)。它还交互式地加载 Scheme 代码,因此 GCJ 必须使用其解释器来运行它(这不利于 GCJ)。这个实验不是一个真正的基准测试,但它确实表明,即使在其当前状态下,您也可以使用 GCJ 获得改进的性能。(与往常一样,如果您关心性能,请根据您预期的工作负载运行您自己的基准测试。)
GCJ 还有其他优点,例如使用 GDB 进行调试以及与 C/C++ 接口(如下所述)。最后,GCJ 是自由软件,基于行业标准的 GCC,允许对其进行自由修改、移植和分发。
有些人抱怨说,提前编译会失去字节码一次编写,到处运行的巨大可移植性优势。然而,这种说法忽略了分发和安装之间的区别。我们不建议将本机可执行文件作为分发格式,除非作为特定架构的预构建软件包(例如,RPM)。您仍然可以使用 Java 字节码作为分发格式,即使它们相对于 Java 源代码没有任何主要优势。(Java 源代码往往比 C 或 C++ 源代码具有更少的可移植性问题。)我们建议,当您安装 Java 应用程序时,如果它尚未编译为本机代码,则应将其编译为本机代码。
对于任何使用过 GCC 编译 C 或 C++ 程序的人来说,使用 GCC 运行 Java 程序都很熟悉。要编译 Java 程序 MyJavaProg.java,请键入
gcj -c -g -O MyJavaProg.java
要链接它,请使用命令
gcj --main=MyJavaProg -o MyJavaProg MyJavaProg.o这就像编译 C++ 程序 mycxxprog.cc 一样
g++ -c -g -O mycxxprog.cc然后链接以创建可执行文件 mycxxprog
g++ -o mycxxprog mycxxprog.o唯一的新方面是选项 --main=MyJavaProg。这是必需的,因为编写包含 main 方法的 Java 类以用于测试或小型实用程序是很常见的。因此,如果您将一堆 Java 编译的类链接在一起,可能会有很多 main 方法,您需要告诉链接器在应用程序启动时应该调用哪个方法。
您还可以选择将一组 Java 类编译成共享库(.so 文件)。事实上,GCJ 运行时系统被编译成一个 .so 文件。虽然这方面的细节属于另一篇文章,但如果您感到好奇,可以查看 Kawa(如下讨论)的 Makefile 以了解其工作原理。
GCJ 不仅仅是一个编译器。它旨在成为一个完整的 Java 环境,具有类似于 Sun 的 JDK 的功能。如果您为 gcj 指定 -C 选项,它将编译为标准的 .class 文件。具体来说,目标是 gcj -C 应该成为 Sun 的 javac 命令的插件替代品。
GCJ 附带一个字节码解释器(由 Kresten Krab Thorup 贡献),并具有功能齐全的 ClassLoader。独立的 gij 程序可以作为 Sun 的 java 命令的插件替代品。
GCJ 与 libgcj 一起工作,libgcj 包含在 GCC 3.0 中。这个运行时库包括核心运行时支持、Hans Boehm 备受赞誉的保守垃圾回收器、字节码解释器和一个大型类库。由于法律和技术原因,GCJ 无法发布 Sun 的类库,因此它有自己的类库。GNU Classpath 项目现在使用与 libgcj 和 libstdc++ 相同的许可证和 FSF 版权,并且类正在这两个项目之间合并。我们使用 GPL,但有一个特殊的例外,即如果您将 libgcj 与其他文件链接以生成可执行文件,这本身不会导致可执行文件由 GPL 编译。因此,即使是专有程序也可以与标准的 C++ 或 Java 运行时库链接。
libgcj 库包括运行非 GUI 应用程序所需的大多数标准 Java 类,包括 java.lang、java.io、java.util、java.net、java.security、java.sql 和 java.math 包中的全部或大部分类。主要缺少的组件是使用 AWT 或 Swing 进行图形处理的类。大多数更高级别的 AWT 类都已实现,但较低级别的对等类不够完整,无法使用。需要志愿者来提供帮助。
虽然您可以使用 Java 做很多事情,但有时您希望调用用另一种语言编写的库。这可能是因为您需要访问无法用 Java 编写的底层代码,现有的库提供了您需要的功能并且不想重写,或者您需要进行底层性能黑客攻击以提高速度。您可以通过声明一些 Java 方法为 native,而不是编写方法体,而是用其他语言提供实现来完成所有这些操作。1997 年,Sun 发布了 Java 本地接口 (JNI),它是用 C 或 C++ 编写本地方法的标准。JNI 的主要目标是可移植性,即为一个 Java 实现编写的本地方法应该可以在另一个 Java 实现上工作,而无需重新编译。这是为闭源、分布式二进制文件世界设计的,在自由软件环境中价值较低,尤其是在您更改芯片或操作系统类型时必须重新编译的情况下。
为了确保 JNI 的可移植性,一切都是通过函数表间接完成的。这使得 JNI 非常慢。更糟糕的是,编写所有这些函数并遵循所有规则既乏味又容易出错。虽然 GCJ 支持 JNI,但它也提供了另一种选择。编译型本地接口 (CNI,也可以代表 Cygnus Native Interface) 基于 Java 基本上是 C++ 的一个子集,并且 GCC 对 C++ 和 Java 使用相同的调用约定的想法。那么,有什么比能够使用 C++ 编写 Java 方法,并使用标准 C++ 语法访问 Java 字段和调用 Java 方法更自然的呢?由于它们使用相同的调用约定和数据布局,因此 C++ 和 Java 之间不需要转换或神奇的粘合剂。
CNI 和 JNI 的示例将不得不等待以后的文章。GCJ 手册 (gcc.gnu.org/onlinedocs/gcj) 相当详细地介绍了 CNI,并且 libgcj 源代码包含许多示例。
Java 字节码是对 Java 程序的一种相当直接的编码,并非真正为其他任何东西而设计。然而,它们已被用于编码用其他语言编写的程序。请参阅 grunge.cs.tu-berlin.de/~tolk/vmlanguages.html 以获取在 Java 之上实现的其他编程语言的列表。其中大多数是解释器,但少数实际上编译为字节码。前者可以按原样使用 GCJ;后者有可能可以使用 GCJ 编译为本机代码。
Kawa 就是这样一个编译器,自 1996 年以来我一直在开发它。Kawa 既是使用 Java 实现语言的工具包,又是 Scheme 编程语言的实现。您可以使用 GCJ 构建和运行 Kawa,而无需任何非自由软件。Kawa 主页 (www.gnu.org/software/kawa) 提供了有关使用 GCJ 下载和构建 Kawa 的说明。
您可以在交互模式下使用 Kawa。在这里,我们首先定义阶乘函数,然后调用它
$ kawa #|kawa:1|# (define (factorial x) #|(---:2|# (if (< x 2) x (* x (factorial (- x 1))))) #|kawa:3|# (factorial 30) 265252859812191058636308480000000
值得注意的有趣之处在于,阶乘函数实际上由 Kawa 编译为字节码,并立即作为新类加载。此过程使用 Java 的 ClassLoader 机制在运行时为包含类字节码的字节数组定义一个新类。新类的方法由 GCJ 的字节码解释器解释。
当然,通常更方便的是将代码放在文件中
$ cat > factorial.scm (define (factorial x) (if (< x 2) x (* x (factorial (- x 1))))) (format #t "Factorial ~d is ~d.~%~!" 30 (factorial 30)) ^D $ kawa -f factorial.scm Factorial 30 is 265252859812191058636308480000000.
您可以通过使用 Kawa 提前编译 Scheme 代码来提高其性能,从而创建一个或多个 .class 文件
$ kawa --main -C factorial.scm (compiling factorial.scm)然后您可以加载编译后的文件
$ kawa -f factorial.class Factorial 30 is 265252859812191058636308480000000.要将类文件编译为本机代码,您可以使用 gckawa,这是一个脚本,用于设置适当的环境变量(LD_LIBRARY_PATH 和 CLASSPATH)并调用 gcj
$ gckawa -o factorial --main=factorial -g -O factorial*.class在这种情况下,不需要在 factorial*.class 中使用通配符,但以防 Kawa 需要生成多个 .class 文件,这是一个好主意。
然后,您可以执行生成的阶乘程序,这是一个普通的 GNU/Linux ELF 可执行文件。它与共享库 libgcj.so(GCJ 运行时库)和 libkawa.so(Kawa 运行时库)链接。
相同的方法可以用于其他语言。例如,我目前正在使用 Kawa 实现 W3C 的新 XML 查询语言 XQuery。
已使用 GCJ 构建的其他应用程序包括 Apache 模块、GNU-Paperclips 和 Jigsaw。

Per Bothner (www.bothner.com/per) 自 1980 年代以来一直从事 GNU 软件工作。在 Cygnus,他是 GCJ 项目的技术负责人。他目前是一名独立顾问。