内容


衡量 Java 本机编译

从 Java 源程序生成本机代码的优缺点

Comments

尽管 Java 语言有许多优点,但是仍然存在几个问题限制了在关键项目中的使用。它们包括执行速度、内存占用、磁盘占用以及 JVM 可用性。虽然 JIT 编译器极大改进了平台的执行速度,J2ME 大幅降低内存占用,但是在许多领域中,Java 应用程序完全无法和它们本机的竞争对手(通常是 C/C++)竞争。为了解决这些问题,许多开发人员已经转向使用 Java 本机编译器,它们允许用 Java 语言编写应用程序,然后将它们编译成本机可执行程序。这种解决方案将以平台无关性为代价,但是它可以导致更快的执行速度和更小的内存占用,这些对于当今许多应用程序都很关键。

为使您能快速掌握 Java 本机编译技术,我们将首先讨论代码编译基础,包括为什么许多开发人员正在使用 Java 本机编译器编译他们的应用程序的简要概述。接下来,我们将使用自由软件编译器和两个不同的应用程序(一个很简单,另一个复杂一些)来测试 Java 本机编译的结果。这些示例和所产生的度量结果将作为研究如何比较最新的 Java 本机编译器和 JVM 的第一手资料。

代码编译基础

要理解本文讨论的内容,您应该熟悉三种最常用的代码编译方法:

  • 使用 Java 编译器(例如,javac)编译 Java 代码
  • 编译本机代码,例如针对特定硬件/操作系统(OS)平台的 C/C++
  • 使用针对特定硬件/OS 平台的 Java 本机编译器来编译 Java 代码

使用 Java 编译器编译 Java 代码是最简单的。我们只要用 Java 语言编写源代码,使用 Java 编译器将源代码编译成 Java 字节码,然后就可以在任何安装了 JVM 的硬件/OS 平台上执行结果了。其缺点是 Java 依赖于 JVM 来实现其特点“一次编写,随处运行”可移植性;不仅要在运行 Java 应用程序的任何平台上安装有可用的 JVM,而且必须还有大量系统资源(内存和磁盘空间)用以支持 JVM。因此,许多开发人员仍然依靠不太灵活但却更具针对性的语言,例如,C/C++。

编译 C/C++ 源程序与编译 Java 源程序相似。只要编写了代码,我们通过一个针对特定硬件/OS 平台的编译器和链接器来运行它。只有在目标平台上才可以执行生成的应用程序,但是不需要安装 JVM(虽然它可能需要一些支持共享库,这取决于所使用的语言)。几乎使用这种方法开发的最简单的应用程序都必须针对每个要运行它们的硬件/OS 平台单独定制。

第三种方法尝试结合以上两种解决方案的优点,允许开发人员使用 Java 语言编写应用程序,然后将它们编译成本机可执行程序。编写了 Java 代码之后,就可以通过 Java 编译器生成 Java 字节码,然后将 Java 字节码编译成本机代码来运行它,或者在 Java 本机编译器中直接运行 Java 代码。需要的步骤数取决于所使用的编译器的需求。

这种方法的优点是可以在 未安装 JVM的目标平台上执行结果代码。这样做的目的是使 Java 应用程序以更快的速度执行,大幅降低运行所需的磁盘空间和内存(虽然有必要为 Java 本机编译器提供支持资源库)。

编译器的目标平台、它们提供的 Java 支持级别以及它们使用的系统资源的数量都是不相同的。在本文的 参考资料一节中可以找到一些当前可用的本机编译器的清单。

关于测试设置

对市场上每种本机编译器的功能部件和性能进行比较已经大大超越了本文的范围。我使用一种编译器 ― GNU 编译器 Java 编程语言版(GNU Compiler for the Java Programming Language, GCJ)作为示例来详细说明本机编译的过程与结果。GCJ 是一种为 GNU 编译器集(GNU Compiler Collection,GCC)开发的编译器,GNU 编译器集是 GNU 项目的一部分。与其它出自 GNU 项目的所有软件一样,GCJ 是双重意义上的自由软件,因此可以很容易地获取(请参阅 参考资料)。如果您正在认真考虑您产品的本机编译途径,当然应该尽可能多地评估编译器,或许可以使用本文中建立的标准。

我的测试系统硬件是一台装有 450 MHz Pentium II 处理器和 320 MB 内存的 PC 机。操作系统是最近安装的 Mandrake 8.1 Linux 分发版。这个分发版带有 GCJ 的 3.0.1 版本,它包含在 GCC 3.0.1 中并且作为 8.1 Mandrake 分发版一部分提供。

我已经运行了两个独立的应用程序,一个很简单,另一个复杂一些。为了比较系统和 Java 平台的性能,我将应用程序编译成 Java 字节码。我使用 Sun JDK 版本 1.3.1.02 Linux 版来编译 Java 代码,然后在下列 JVM 上测试结果类:

  • Kaffe 1.0.6
  • Sun JVM 1.3.1_02
  • IBM JRE 1.3.1

为实现本文的目的,我测量了执行速度、执行内存开销和磁盘空间。

测试 1:Prime.java

第一个测试应用程序很简单,由单一类 prime.java 组成。这个应用程序实现一个非常基本的搜索质数的算法。 清单 1 显示了 prime.java 的源代码。

清单 1. prime.java 的源代码
import java.io.*;
class prime
{
   private static boolean isPrime(long i)
   {
       for(long test = 2; test < i; test++)
       {
       if(i%test == 0)
       {
        return false;
       }
       }       
       return true;
   }
   public static void main(String[] args) throws IOException
   {
       long start_time = System.currentTimeMillis();
       long n_loops =  50000;
       long n_primes = 0;
       for(long i = 0; i < n_loops; i++)
       {
       if(isPrime(i))
           {
           n_primes++;
           }
       }
   
       long end_time = System.currentTimeMillis();
       System.out.println(n_primes + " primes found");
       System.out.println("Time taken = " + (end_time - start_time));
   }
}

如您所见,代码从 0 循环到 50000。运行时,它尝试用每个它遇到的数除以每个小于这个数的数字,以找出是否有余数。(不可否认,这是搜索质数的一种蛮力方法,但是它可以满足该示例的需要。)

我使用下列命令将 prime.java 编译成本机可执行程序:

gcj prime.java -O3 --main=prime -o prime

参数 -O3 表示“优化速度”;参数 --main 告诉 GCJ 哪一个类包含运行应用程序时将使用的 main 方法;参数 -o Prime 命名生成的可执行程序。有关一套完整的命令行参数,请参阅 GCJ 文档。

为了编译 Java 字节码测试,我使用了下列命令:

/usr/java/jdk1.3.1_02/bin/javac -O prime.java

接下来,对于每个测试 JVM,我使用下列命令调用代码:

  • 本机./prime
  • Kaffe/usr/bin/java prime
  • Sun JDK/usr/java/jdk1.3.1_02/bin/java prime
  • IBM JRE/opt/IBMJava2-13/jre/bin/java prime

prime.java 的测试结果

如前所述,我测试了执行速度、内存使用和磁盘空间使用情况。下表详细说明了第一个测试的结果。

表 1. Prime.java:执行速度
实现以毫秒为单位的时间(三次运行的平均值 ― 分值越低越好)
本机40180
Kaffe75456
Sun JDK67315
IBM JRE18188
表 2. Prime.java:内存使用
实现VM 大小(KB)VM RSS(KB)
本机70243528
Kaffe88883564
Sun JDK1695606636
IBM JRE819366288

请注意,VM 大小等于进程映象的总和。这包括该进程所使用的所有代码、数据和共享库,包括已经交换出去的页面。VM 驻留集大小(RSS)等于实际驻留在 RAM 中的进程(代码和数据)部分的大小,包括共享库。这给出了一个进程大概使用多少 RAM 的近似值。

简单来说,如果一个进程分配了大量内存,这会显示在 VM 大小中,但是直到真正被使用(例如,读或写)时才会显示在 VM RSS 中。实际上,VM RSS 是更重要的测量指标,因为它更准确地反映了系统的性能。

表 3. Prime.java:磁盘空间使用
实现编译的大小总合(字节)
本机22268
Java 类962

请注意,表 3 中显示的测量不包含共享库和 JVM,并且是剥去可执行程序后进行测量的。

测试 2:SciMark 2

对于第二个测试,我使用了一个更复杂的 Java 应用程序 SciMark 2 Java 基准测试程序。可以免费获得本文使用的命令行版本(请参阅 参考资料)。SciMark 2 是一个很复杂的应用程序。它实现了许多用来准确地评测 JVM 的效率的基准测试程序。

我使用下列命令将 SciMark 2 编译成本机可执行程序:

gcj-3.0.1 -O3 commandline.java Random.java FFT.java SOR.java Stopwatch.java 
  SparseCompRow.java LU.java kernel.java MonteCarlo.java 
    --main=jnt.scimark2.commandline -o scimark

然后,我使用下列命令将应用程序编译成 Java 字节码:

/usr/java/jdk1.3.1_02/bin/javac -O *.java

SciMark 2 基准测试程序能以两种模式运行,正常模式和大模式。您使用的模式决定使用的问题集的大小。我已经用这两种模式运行了测试。

我使用下列命令以正常模式调用代码:

  • 本机./scimark
  • Kaffe/usr/bin/java jnt.scimark2.commandline
  • Sun JDK/usr/java/jdk1.3.1_02/bin/java jnt.scimark2.commandline
  • IBM JRE/opt/IBMJava2-13/jre/bin/java jnt.scimark2.commandline

对于更大的问题集,我使用下列命令:

  • 本机./scimark -large
  • Kaffe/usr/bin/java jnt.scimark2.commandline -large
  • Sun JDK/usr/java/jdk1.3.1_02/bin/java jnt.scimark2.commandline -large
  • IBM JRE/opt/IBMJava2-13/jre/bin/java jnt.scimark2.commandline -large

SciMark 2 的测试结果

以下各表显示了编译 SciMark 2 的结果。请注意结果中正常模式与大模式的区别。

表 4. SciMark 2,正常模式:执行速度
实现复合分值(三次运行的平均值 ― 分值越高越好)
本机15.22
Kaffe7.01
Sun JDK22.86
IBM JRE25.29
表 5. SciMark 2,正常模式:内存使用
实现VM 大小(KB)VM RSS(KB)
本机97885956
Kaffe88884092
Sun JDK1696927428
IBM JRE819647408
表 6. SciMark 2,大模式:执行速度
实现复合分值(三次运行的平均值 ― 分值越高越好)
本机8.78
Kaffe5.72
Sun JDK12.04
IBM JRE15.04
表 7. SciMark 2,大模式:内存使用
实现VM 大小(KB)VM RSS(KB)
本机6288859072
Kaffe5805656988
Sun JDK16969264624
IBM JRE8196457704
表 8. SciMark 2:两种模式使用的磁盘空间
实现编译大小(字节)
本机49588
Java 类16318

再次声明,表 8 中的测量不包括共享库和 JVM,并且剥去了可执行程序后测量的。

本机编译的优缺点

从上面的测试结果应该明显看出,Java 本机编译究竟是成功还是失败还难有定论。某些基准测试程序显示在本机编译的可执行程序比使用某些 JVM 版本快;另外一些则相反。同样,不同 JVM 之间,某些操作的速度也大相径庭。执行的“工作集”内存测试显示执行时在内存使用方面并没有很大差别。要进一步探索该领域,可以采用不同的垃圾信息收集方案来进行本机和 JVM 测试。

本机版本只是在磁盘空间使用方面明显优于 JVM 版本,而且只有当考虑到 JVM 的大小时,这才能成立。虽然类本身很小,但是所测试的 JVM 却很大(IBM 和 Sun JVM 的 jre 子目录中的递归目录清单显示仅是 JRE 就占用了 50 MB 磁盘空间)。但是,请不要忘记还可以使用许多更小的 JVM,虽然 JVM 和单个应用程序的组合比本机可执行程序与 GCJ 运行时库 libgcj.so(少于 3 MB)的组合大得多,但是本机版本的可执行程序的大小却大很多。因此,在需要大量应用程序的情况下,JVM 版本可能是最终的赢家。

除了这些有点模糊的结果之外,使用 Java 本机编译还可能产生许多潜在问题。它们是:

  • 失去了平台无关性:实际上,这并不是什么大不了的问题。因为源程序是用 Java 语言编写的,所以您仍然可以选择生成可以在任何地方运行的 Java 字节码版本,然后按需要在特定平台上使用本机编译器。
  • 类支持/编译器成熟度:某些编译器仍然相对不太成熟,并且可能无法支持您应用程序所需的所有 Java 类。例如,虽然 GCJ 支持大部分上至 1.1 版本规范的 Java 语言构造,但是,它 支持通常与 JVM 一起提供的所有 Java 类库。最重要的是,它几乎不支持 AWT,这使 GCJ 无法适用于 GUI 应用程序。不同的编译器支持不同级别的类库;Excelsior JET 声称是完全支持 AWT 和 Swing 的编译器。
  • 支持/复杂程度:因为这个领域相对较新,所以开发人员常常无法很好地了解它。诊断工具可能在基础上有点薄弱,所以要诊断本机编译的 Java 应用程序中发生的问题可能更困难(尤其当 Java 字节码版本中没有发生该错误时)。

结束语

当开发应用程序时,通常确定 Java 本机编译是否适合您的特定环境的唯一实际方法是完成一个问题解决周期。

  1. 确定希望使用本机编译来解决的确切问题。
  2. 研究可用的本机编译器,然后提出一些可能解决问题的本机编译器。
  3. 尝试您选择的所有编译器来编译您的应用程序,然后观察结果。

尽管 Java 本机编译技术还比较稚嫩,并且缺少明确的结果,但它却是 Java 语言中一个激动人心的新领域。利用现有选项的最佳方法是,或许可以使用本文中建立的某些方法和标准亲自研究和测试 Java 本机编译。

虽然本机编译不会取代 JVM(许多人认为它会取代 JVM),但是已经证明,对于某些应用程序和环境,它是正确的解决方案。本机编译将 Java 语言的使用范围扩展到一些新领域,短短几年前在这些领域中还无法应用 Java 语言。整体而言,这仅对于 Java 语言和 Java 社区是件好事。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=52929
ArticleTitle=衡量 Java 本机编译
publish-date=01182002