使用类共享提高性能

探索 IBM JRE 中最新的类共享特性

IBM®JRE for Java™SE 6 的最新版本增强了在版本 5 中首次引入的类共享特性。在本文中,性能分析师 Adam Pilkington 和 Graham Rawson 详细介绍这些改进,包括对应用程序启动时间和内存使用量的改进。

Adam Pilkington, 软件工程师, EMC

Adam Pilkington 的照片Adam Pilkington 是 IBM Java Technology Centre 的 Java 性能分析师,主要关注 Java 6 的 WebSphere Application Server 性能。在 2006 年加入 IBM 之前,他是英国一家大型金融服务组织的 J2EE 技术架构师。他拥有数学和计算机科学学位。


developerWorks 投稿作者

Graham Rawson, 软件工程师, EMC

Graham RawsonGraham Rawson 是 IBM Java Technology Centre 的 Java 性能分析师,他领导位于英国 Hursley 的 IBM Laboratory 的 Java 性能团队。Graham 已经在 IBM 工作了 24 年,担任过支持在 Hursley 开发的 CICS Transaction Server 和 Java 技术的各种职位。Graham 拥有 University of East Anglia(英国 Norwich)的理科学士学位和 University of Oxford(英国)的软件工程认证。



2008 年 11 月 03 日

共享类基础结构是在 IBM JRE for the Java platform SE 的版本 5 中首次引入的。最新版本对这个特性的改进有助于提高 Java 应用程序在启动时间和内存使用量方面的性能。在本文中,我们介绍这些改进并使用 Eclipse 和 Apache Tomcat 作为客户端和服务器端操作环境的示例,从而展示这些改进的好处。我们将提供安装说明,让您可以自己实践一下,但是您应该熟悉这两个应用程序以及 IBM 的类共享特性。如果您还不熟悉 IBM 的类共享特性,建议先阅读文章 “Java 技术,IBM 风格 : 类共享”,这篇文章解释了基本概念。

如果希望实践本文提供的示例,现在可以下载 IBM JRE for Java 6 for Linux®and AIX®的实现。目前没有可以单独下载的 Windows®实现,而是以 预构建的 Eclipse 下载包形式提供这个实现。注意,需要 IBM 注册(免费)。

IBM 共享类中的新特性?

IBM JRE for Java 5 允许通过缓存在 JVM 之间共享类。在 IBM JRE for Java 6 中,可以使这个缓存持久化并用它共享编译的代码。另外,存储这些缓存项的方法效率更高了。

共享的类

在 Java 虚拟机 (JVM) 之间共享类的功能是在 IBM JRE for the Java platform SE 的版本 5 中首次引入的,在 Java 6 中继续支持并进一步增强了此功能。当 JVM 装载类时,可以把它们放在缓存中。当以后请求这个类时,会尽可能通过缓存满足请求,而不必从对应的 JAR 文件再次装载这个类。

可以使用清单 1 中的命令行选项控制缓存的最大大小,但是请注意,这个最大大小可能受到操作系统共享内存限制的约束:

清单 1. 设置最大缓存大小的命令行选项
 Running java -X will show the following option ... 

 Arguments to the following options are expressed in bytes. 
 Values suffixed with "k" (kilo) or "m" (mega) will be factored accordingly. 
 : 
 -Xscmx<x>       set size of new shared class cache to <x> 
 :

Ahead of Time (AOT) 代码存储

JVM 通常在执行程序时把 Java 方法编译为原生代码。在每次运行程序时,都会生成原生代码。IBM JRE for Java 6 SR1 JVM 引入了使用 Ahead of Time 编译技术编译 Java 方法的功能。用这种技术生成的原生代码不但可以在当前的 JVM 中使用,而且可以存储在共享类缓存中。使用共享类缓存启动的另一个 JVM 可以使用缓存中存储的 AOT 代码,从而减少启动时间。这是由于节省了编译所需的时间,而且执行采用 AOT 代码形式的方法速度更快。AOT 代码是原生代码,执行速度通常比解释的代码快(但是不太可能像 JIT 生成的代码那么快)。

可以使用命令行选项定义 AOT 代码在共享类缓存可以占用的最小和最大空间,见清单 2。如果没有指定可以存储的 AOT 代码最大量,默认设置是使用整个缓存。但是,这不会导致整个缓存被 AOT 代码填满,因为只能从缓存中已有的类生成 AOT 代码。

清单 2. 控制缓存的 AOT 代码量的命令行选项
 Running java -X will show the following options ... 

 Arguments to the following options are expressed in bytes. 
 Values suffixed with "k" (kilo) or "m" (mega) will be factored accordingly. 
 : 
 -Xscminaot<x>   set minimum shared classes cache space reserved for AOT data to <x> 
 -Xscmaxaot<x>   set maximum shared classes cache space allowed for AOT data to <x>

图 1 说明共享类和 AOT 代码如何占用缓存空间,以及缓存空间设置如何控制它们使用的可用空间额度。

图 1. 共享类缓存的组成
缓存的组成

稍后进一步讨论 AOT 代码

类压缩

为了尽可能提高使用共享类缓存的效率,JVM 使用压缩技术增加能够存储的类的数量。类压缩是自动执行的,无法通过命令行选项修改。

持久化缓存

IBM JRE for Java 5 中的共享类缓存是使用共享内存段实现的,这使 JVM 能够共享同一个缓存,但是操作系统重新引导之后缓存就失效了。这意味着,在重新引导之后启动的第一个 JVM 必须重新构建缓存。在 Java 6 中,缓存的默认实现改为使用内存映射文件。这使缓存持久化,在操作系统重新启动之后仍然有效。

AOT 详解

AOT 编译器是 IBM JRE for Java 6 中新增的编译机制。在使用以前的 IBM JRE 版本时,可以以两种方式执行 Java 方法:解释和编译。解释方式是解释并执行组成此方法的 Java 字节码。编译方式是由一个称为即时 (Just-in-Time,JIT) 编译器的 JRE 组件把代码编译和优化为原生机器码,然后执行。JIT 编译是自动执行的。在实际运行方法时执行编译过程,采用的编译技术取决于执行期间对实际方法的分析。

什么是 AOT 代码?

AOT 代码是通过 AOT 编译生成的 Java 方法的原生代码版本。与 JIT 编译不同,AOT 编译并不根据对 Java 方法的动态分析执行优化。通常,AOT 编译的原生代码版本比解释的 Java 字节码执行得快,但是没有 JIT 编译的原生代码那么快。

AOT 编译的主要目的是,通过提供 Java 方法的预编译版本加快应用程序的启动速度。与生成 JIT 编译的代码相比,从共享类缓存装载这些预编译的 AOT 方法能够更快地获得可执行的 Java 方法原生代码版本。通过快速装载 AOT 编译的代码,JVM 可以更快地获得原生代码版本,减少解释 Java 方法所需的时间。AOT 编译的方法仍然属于 JIT 编译处理的范围,所以在最初以 AOT 代码形式执行方法之后,JIT 仍然可以进一步优化它。

AOT 是共享类的组成部分

生成的 AOT 代码存储在共享类缓存的一个区域中。使用这个共享类缓存的其他 JVM 都可以执行这些 AOT 代码,这样就避免了编译的开销。

这种实现不同于实时 JVM;在实时 JVM 中,AOT 代码由一个实用程序 (jxeinajar) 执行编译并存储在 JAR 文件中,更多信息参见 “实时 Java,第 1 部分 : 使用 Java 语言编写实时系统”。

由 JVM 执行的 AOT 代码并不是直接共享的,而是从共享类缓存复制出来的。因为每个 JVM 仍然拥有 AOT 代码的拷贝,所以这种实现方式对于内存使用量没有直接的好处。但是,由于能够重用 AOT 代码并避免重复编译,可以节省内存和 CPU。

AOT 问题诊断

可以通过三个命令行选项帮助了解应用程序对哪些方法执行了 AOT 编译,以及这些方法在共享类缓存中占用多少空间:

  • -Xjit:verbose:使用这个命令报告 JIT 执行的所有 AOT 编译。
  • -Xshareclasses:verboseAOT:使用这个命令报告在共享类缓存中存储或读取的所有 AOT 代码。
  • java -Xshareclasses:printAllStats:使用这个命令报告共享类缓存统计数据,包括存储的 AOT 代码和占用的空间。

清单 3 显示的是在清空共享类缓存之后 第一次调用 Tomcat 服务器的输出,这里应用了运行时选项 -Xjit:verbose-Xshareclasses:verboseAOT

清单 3. 选项 -Xjit:verbose-Xshareclasses:verboseAOT
 + (AOT cold) java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder; 
 Storing AOT code for ROMMethod 0x02359850 in shared cache... Succeeded. 
 + (AOT cold) sun/misc/URLClassPath$JarLoader.ensureOpen()V @ 0x0147BF9C-0x0147C106 Q_SZ=3 
 Storing AOT code for ROMMethod 0x023CBFC4 in shared cache... Succeeded. 
 + (AOT cold) java/util/jar/JarFile.getEntry(Ljava/lang/String;)Ljava/util/zip/ZipEntry; 
 Storing AOT code for ROMMethod 0x023CE38C in shared cache... Succeeded.

在启动 Tomcat 服务器之后,使用 java -Xshareclasses:printAllStats命令获得共享类缓存统计数据,这显示存储在共享类缓存中的方法(清单 4 是部分输出):

清单 4. 使用 java -Xshareclasses:printAllStats显示存储在共享类缓存中的方法
 1: 0x43469B8C AOT: append 
	 for ROMClass java/lang/StringBuilder at 0x42539178. 
 1: 0x43469634 AOT: ensureOpen 
	 for ROMClass sun/misc/URLClassPath$JarLoader at 0x425AB758. 
 1: 0x434693A8 AOT: getEntry 
	 for ROMClass java/util/jar/JarFile at 0x425ADAD8.

如清单 5 所示,在以后使用共享类缓存调用 Tomcat 服务器时,会发现这些方法已经经过 AOT 编译,从缓存中装载它们即可,不需要重复编译:

清单 5. 寻找和装载 AOT 编译的方法
 Finding AOT code for ROMMethod 0x02359850 in shared cache... Succeeded. 
 (AOT load) java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder; 
 Finding AOT code for ROMMethod 0x023CBFC4 in shared cache... Succeeded. 
 (AOT load) sun/misc/URLClassPath$JarLoader.ensureOpen()V 
 Finding AOT code for ROMMethod 0x023CE38C in shared cache... Succeeded. 
 (AOT load) java/util/jar/JarFile.getEntry(Ljava/lang/String;)Ljava/util/zip/ZipEntry;

AOT 编译通过启发式决策选择候选方法,这会进一步改进启动速度。因此,对应用程序的后续调用可能会导致更多的方法被 AOT 编译。

对于已经 AOT 编译的方法,如果它满足必要的重新编译条件,就可能再执行 JIT 编译。但是,AOT 编译的目标是选择在应用程序启动时需要的方法,而 JIT 编译的目标是对频繁使用的方法进行优化,因此 AOT 编译的方法可能使用得不够频繁,不足以触发 JIT 编译。

清单 6 是使用 -Xjit:verbose执行 SPECjbb2005 基准测试时的部分输出,其中包含两个方法的 AOT 编译报告:com/ibm/security/util/ObjectIdentifier.equalsjava/math/BigDecimal.multiply。第一个方法并不进一步执行 JIT 编译,但是使用得比较频繁的 java/math/BigDecimal.multiply会 JIT 编译两次,最终到达 hot 优化级别。

SPECjbb2005 的启动阶段并不长,所以只有几个方法执行 AOT 编译。注意,AOT 编译以 cold优化级别执行,这反映 AOT 的总体目标是加快应用程序的启动。

清单 6. 在使用 -Xjit:verbose时报告的优化
 + (AOT cold) com/ibm/security/util/ObjectIdentifier.equals(Ljava/lang/Object;) 
 Storing AOT code for ROMMethod 0x118B8AF4 in shared cache... Succeeded. 

 + (AOT cold) java/math/BigDecimal.multiply(Ljava/math/BigDecimal;)Ljava/math/BigDecimal; 
 Storing AOT code for ROMMethod 0x119D3C60 in shared cache... Succeeded. 
 + (warm) java/math/BigDecimal.multiply(Ljava/math/BigDecimal;)Ljava/math/BigDecimal; 
 + (hot) java/math/BigDecimal.multiply(Ljava/math/BigDecimal;)Ljava/math/BigDecimal;

java -Xshareclasses:printAllStats命令产生的共享类缓存统计数据列出每个 AOT 编译的方法和缓存的每个共享类。可以通过这些信息了解共享类缓存的大小是否是合适的。例如,清单 7 说明缓存的总大小是 16776844 字节,只占用了其中的 40%,1668 个 ROMClass 占用 5950936 字节,458 个 AOT 编译的方法占用 683772 字节:

清单 7. 缓存的详细信息
 base address       = 0x424DE000 
 end address        = 0x434D0000 
 allocation pointer = 0x4295E748 

 cache size         = 16776844 
 free bytes         = 9971656 
 ROMClass bytes = 5950936AOT bytes = 683772
 Data bytes         = 57428 
 Metadata bytes     = 113052 
 Metadata % used    = 1% 

 # ROMClasses = 1668# AOT Methods = 458
 # Classpaths       = 7 
 # URLs             = 0 
 # Tokens           = 0 
 # Stale classes    = 0 
 % Stale classes    = 0% 

 Cache is 40% full

清单 8 是 -Xshareclasses:destroyAll命令产生的输出,这表明缓存已经被销毁。这个命令还会发出消息 Could not create the Java virtual machine。所以不必惊慌,这是正常的。

清单 8. 销毁缓存
 Attempting to destroy all caches in cacheDir C:\...\javasharedresources\ 
 
 JVMSHRC256I Persistent shared cache "eclipse" has been destroyed 
 Could not create the Java virtual machine.

度量内存使用量

可以使用许多性能工具检查共享类给内存使用量带来的好处。使用的工具取决于底层操作系统。在检查内存使用量时必须记住一点:缓存是通过一个内存映射文件实现的,这使多个虚拟机可以共享它的内容。用来检查内存使用量的工具必须能够区分共享内存(可由多个 JVM 访问和共享)和私有内存(只能由一个 JVM 访问)。

Virtual Address Dump 实用程序 (Windows)

Virtual Address Dump (vadump) 实用程序是 Microsoft®资源集中的一个工具,可以使用它提供关于应用程序或共享类缓存的内存使用量的信息。vadump 会产生大量信息,但是我们只需要关于工作集大小的报告,这会提供应用程序的内存使用量信息。vadump -os -p <pid>命令显示给定的进程 ID 的工作集信息。

产生的输出包含关于一个进程使用的内存的大量信息。为了了解使用共享类所产生的内存改进,我们主要关注 Grand Total Working Set部分,以及类数据共享如何影响 PrivateShareableShared在这个数字中的比例。清单 9 显示一个 vadump 汇总输出示例。共享类是通过内存映射文件实现的,所以它们占用的内存显示在 Mapped Data输出行中。

清单 9. vadump 输出示例
 vadump -os -p 5364 
 Category                        Total        Private Shareable    Shared 
                           Pages    KBytes    KBytes    KBytes    KBytes 
      Page Table Pages        29       116       116         0         0 
      Other System             8        32        32         0         0 
      Code/StaticData       2079      8316      5328       140      2848 
      Heap                    87       348       348         0         0 
      Stack                    4        16        16         0         0 
      Teb                      1         4         4         0         0 
      Mapped Data             95       380         0        24     356
      Other Data              61       244       240         4         0 

      Total Modules         2079      8316      5328       140      2848 
      Total Dynamic Data     248       992       608        28       356 
      Total System            37       148       148         0         0 
 Grand Total Working Set     2364      9456      6084       168      3204

要想找到 vadump 命令中需要的进程 ID,可以使用 Windows Task Manager:

  1. 打开 Task Manager 应用程序并选择 Processes选项卡。
  2. 找到称为 PID 的列。(如果没有出现这个列,那么单击 View > Select Columns并选择 PID复选框,见图 2)。
  3. 找到希望检查的进程,记下 PID 列中的值。这就是需要传递给 vadump 的进程 ID。
图 2. 在 Task Manager 中寻找进程 ID 信息
显示进程 ID 信息

使用 top 度量 Linux 上的内存使用量

有许多 Linux 工具可以检查内存使用量。top命令适合展示共享类的效果。为了让输出更容易理解,我们将在命令行上提供进程 ID 并以批模式运行此命令。清单 10 给出命令行和输出示例。

清单 10. top 命令行和输出示例
 top -b -n 1 -p <pid> 
	
 top - 13:33:41 up 18 days,  9:30,  1 user,  load average: 0.00, 0.00, 0.00 
 Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie 
 Cpu(s):  0.0% us,  0.0% sy,  0.0% ni, 100.0% id,  0.0% wa,  0.0% hi,  0.0% si 
 Mem:   8157972k total,   311312k used,  7846660k free,    56448k buffers 
 Swap:  2104472k total,        0k used,  2104472k free,   141956k cached 

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND 
 7073 root      15   0 41616  13m 2228 S  0.0  0.2   5:43.70 X

下面的值是我们最感兴趣的:

  • VIRT —Virtual Image (KB):此任务使用的虚拟内存总量。它包含所有代码、数据和共享库,以及已经换出的页面。
  • RES —Resident size (KB):此任务使用的未交换物理内存。
  • SHR —Shared Mem size (KB):此任务使用的共享内存量。它仅仅反映可以由其他进程共享的内存。

通过配置 Eclipse 使用共享类特性

为了展示使用共享类可以实现的内存使用量和启动改进效果,我们要度量它对两个应用程序的影响:Eclipse(代表客户端桌面应用程序)和 Apache Tomcat(代表服务器端应用程序)。

正如在本文开头提到的,目前还没有针对 Windows 的单独的 IBM SDK for Java 6 和 Java 平台运行库。如果您使用 Windows(而不是 Linux 或 AIX),就需要下载 预构建的 Eclipse 包

如果使用 LinuxAIX,那么下载单独的 IBM SDK for Java 6 实现,然后从 Eclipse 项目网站下载所需的 Eclipse 版本(参见 参考资料)。按照 Eclipse 安装说明配置 Eclipse,让它能够使用 IBM SDK for Java 6。

在安装 Eclipse 之后,还需要执行以下步骤:

  1. 为插件启用类共享。把 OSGI 插件适配器(参见 参考资料)安装到 Eclipse 插件目录中。
  2. 下载 SampleView.jar(参见 下载)并把它安装到 Eclipse 插件目录中。在视图初始化时,这个插件连接 IBM JVM 跟踪并输出一些跟踪点,从而简化了对 Eclipse 启动时间的计时。我们将在下一节中讨论如何使用 IBM JVM 跟踪提供启动统计数据。
  3. 创建两个工作空间 workspace1workspace2。这样就可以启动两个 Eclipse 实例,让它们指向不同的工作空间,但是共享同一个类缓存。

还需要设置 Tomcat(如果还没有设置的话)。只需从 Apache Tomcat 网站下载这个应用程序,对下载包进行解压,然后按照文件 running.txt 中的说明操作。

性能比较

我们将使用前几节介绍的工具和应用程序度量共享类提供的性能收益。我们尽可能隔离共享类特性(通过尽可能禁用其他特性),以便更容易解释结果。

Eclipse 性能:内存使用量

为了检查内存使用量,我们使用不同的工作空间在同一个 Windows 上同时运行多个 Eclipse 实例。然后收集 vadump 数据对以三种不同模式启动的 Eclipse 进行比较:

  • 以一般方式启动 Eclipse,不启用任何共享类功能。
  • 第一次用一个清空的共享类缓存启动 Eclipse。
  • 使用相同的共享类缓存启动第二个 Eclipse 实例。

为了在 Eclipse 中启用共享类,需要创建一个新的启动命令行,其中应该包含正确的 JVM 选项。只创建一个新的快捷方式是不够的,而是应该创建一个用来启动 Eclipse 的批文件,见清单 11。它执行以下功能:

  • 接受一个值为 1 或 2 的命令行参数,这两个值分别对应于在配置 Eclipse 时创建的工作空间。
  • 如果指定工作空间 1,那么清空已经存在的任何共享类缓存。
  • 在 Eclipse 终止运行之后,输出缓存统计数据。
清单 11. 用来启动 Eclipse 的批文件
 @echo off 
 rem batch file to start Eclipse using the specified workspace 
 SET ECLIPSE_HOME=C:\java\eclipse\IBMEclipse\eclipse 
 SET JVM=C:\java\eclipse\IBMEclipse\ibm_sdk50\jre\bin\java.exe 
 SET WNAME=C:\java\eclipse\workspace%1 
 SET SC_OPTS=-Xshareclasses:name=eclipse,verbose 
 SET VMARGS=%SC_OPTS% 

 echo Clearing shared classes cache 
 if %1==1 %JVM% -Xshareclasses:destroyAll 

 echo JVM version 
 %JVM% -version 

 echo Starting Eclipse 
 %ECLIPSE_HOME%\eclipse.exe -nosplash -data %WNAME% -vm %JVM% -vmargs %VMARGS% 
 %JVM% -Xshareclasses:name=eclipse,printStats

清单 12 给出不使用共享类的 Eclipse 实例的 vadump 报告。在 vadump 报告中,我们最感兴趣的字段是 Shareable KBytes、Shared KBytes 和 Grand Total Working Set KBytes。

清单 12. 不使用共享类的 Eclipse 的 vadump 输出
 Category                        Total        Private Shareable    Shared 
                           Pages    KBytes    KBytes    KBytes    KBytes 
      Page Table Pages        54       216       216         0         0 
      Other System            28       112       112         0         0 
      Code/StaticData       4199     16796     11500      1052      4244 
      Heap                  9400     37600     37600         0         0 
      Stack                   98       392       392         0         0 
      Teb                     21        84        84         0         0 
      Mapped Data            130       520         0        36       484 
      Other Data            5337     21348     21344         4         0 

      Total Modules         4199     16796     11500      1052      4244 
      Total Dynamic Data   14986     59944     59420        40       484 
      Total System            82       328       328         0         0 
 Grand Total Working Set    19267     77068     71248      1092      4728

清单 13 给出在使用 清单 11中的批文件启动 Eclipse 时 vadump 的输出。可以看到有大约 4MB 的类(Shareable Mapped Data 为 4116 KBytes)被放在缓存中,这导致总工作集大小增加了相应的数量。突出显示的数据项说明内存可供其他进程共享。在比较 vadump 的输出时要记住一点:尽管这些输出是在 Eclipse 启动时产生的,但是报告的数字仍然有一些小差异。

清单 13. 第一次使用共享类缓存启动 Eclipse 时的 vadump 输出和统计数据
 Category                        Total        Private Shareable    Shared 
                           Pages    KBytes    KBytes    KBytes    KBytes 
      Page Table Pages        54       216       216         0         0 
      Other System            28       112       112         0         0 
      Code/StaticData       4256     17024     11676      1072      4276 
      Heap                  8631     34524     34524         0         0 
      Stack                  103       412       412         0         0 
      Teb                     20        80        80         0         0 
      Mapped Data           1155      4620         0      4116      504 
      Other Data            5386     21544     21540         4         0 

      Total Modules         4256     17024     11676      1072      4276 
      Total Dynamic Data   15295     61180     56556      4120       504 
      Total System            82       328       328         0         0 
 Grand Total Working Set    19633     78532    68560      5192      4780 

 Current statistics for cache "eclipse": 


 base address       = 0x42B0E000 
 end address        = 0x43B00000 
 allocation pointer = 0x42E0B958 

 cache size         = 16776844 
 free bytes         = 12005976 
 ROMClass bytes     = 4001256
 AOT bytes          = 625428 
 Data bytes         = 57043 
 Metadata bytes     = 87141 
 Metadata % used    = 1% 

 # ROMClasses       = 1334 
 # AOT Methods      = 480 
 # Classpaths       = 4 
 # URLs             = 0 
 # Tokens           = 0 
 # Stale classes    = 0 
 % Stale classes    = 0%

启动另一个 Eclipse 实例,然后在此实例上运行 vadump,输出见清单 14。初看上去,内存使用量的差异非常小。但是仔细观察就会发现,4MB 的内存(Shared Mapped Data 为 4564 KBytes)实际上是与另一个进程共享的。对于使用共享内存的每个进程,vadump(和 Task Manager)把共享内存都计算在 Grand Total Working Set 之内。第二个 Eclipse 实例的内存使用量低 4MB,这是因为它共享由第一个 Eclipse 实例创建并填充的类缓存。

这里给出的结果反映 Eclipse 中只安装了很少几个插件时的启动情况。如果安装更多的插件,就会有更多的类被放在共享类缓存中,启动时间也会有相应的改进。

清单 14. 使用现有的共享类缓存启动第二个 Eclipse 的 vadump 输出
 Category                        Total        Private Shareable    Shared 
                           Pages    KBytes    KBytes    KBytes    KBytes 
      Page Table Pages        54       216       216         0         0 
      Other System            29       116       116         0         0 
      Code/StaticData       4254     17016     11676         0      5340 
      Heap                  8684     34736     34736         0         0 
      Stack                   98       392       392         0         0 
      Teb                     20        80        80         0         0 
      Mapped Data           1150      4600         0        36      4564
      Other Data            5261     21044     21040         4         0 

      Total Modules         4254     17016     11676         0      5340 
      Total Dynamic Data   15213     60852     56248        40      4564 
      Total System            83       332       332         0         0 
 Grand Total Working Set    19550     78200     68256        40      9904

Eclipse 性能:启动

除了对内存使用量的改进之外,由于从缓存(而不是磁盘)装载类,共享类还会减少启动时间。另外,使用缓存中的 AOT 代码也有助于减少启动时间。为了统计 Eclipse 的启动时间,我们将使用一个定制的视图,它在装载时使用 IBM JVM 跟踪输出消息。还必须修改 清单 11所示的 Eclipse 启动批文件,以便启用 JVM 跟踪并记录以下跟踪事件:

  • 跟踪的初始化:跟踪在 JVM 启动之后几乎立即启动,这发生在装载任何类之前。我们以此作为启动时间计时的起点。
  • 示例视图消息:当视图初始化时输出第一个消息,收到这个消息就说明 Eclipse 已经启动了。我们以此作为启动时间计时的终点。

清单 15 给出修改后的批文件,增加的 JVM 跟踪配置行以粗体显示:

清单 15. 启用跟踪的 Eclipse 启动批文件
 @echo off 
 rem batch file to time Eclipse startup 
 SET ECLIPSE_HOME=C:\java\eclipse\IBMEclipse\eclipse 
 SET WNAME=C:\java\eclipse\workspace%1 
 SET JVM=C:\java\eclipse\IBMEclipse\ibm_sdk60\jre\bin\java.exe 
 SET TRACE_OPTS=-Xtrace:iprint=tpnid{j9trc.0},iprint=SampleView
 SET SC_OPTS=-Xshareclasses:name=eclipse,verbose 
 SET VMARGS=%SC_OPTS% %TRACE_OPTS% 

 echo Clearing shared classes cache 
 if %1==1 %JVM% -Xshareclasses:destroyAll 

 echo JVM version 
 %JVM% -version 

 echo VM arguments 
 echo %VMARGS% 

 echo Starting Eclipse 
 %ECLIPSE_HOME%\eclipse.exe -nosplash -data %WNAME% -vm %JVM% -vmargs %VMARGS% 

 %JVM% -Xshareclasses:name=eclipse,printStats

清单 16 和清单 17 分别给出在不启用和启用共享类的情况下启动 Eclipse 时的输出。可以看到启动时间有大约 1 秒的改进,这表示启动时间减少了 25%。启用共享类时的计时结果反映的是第二个 Eclipse 的启动情况,因为第一个 Eclipse 实例用来填充缓存。对于这个非常 “干净” 的 Eclipse 版本,在缓存中只存储 4MB 的数据;对于更大更复杂的 Eclipse 应用程序,会更充分地利用类共享减少启动时间。

清单 16. Eclipse 启动,不启用共享类
 09:47:55.296*0x41471300   j9trc.0         - Trace initialized for VM = 00096238 
 09:47:59.500 0x41471300SampleView.2         - Event id 1, text = Mark 
 09:47:59.500 0x41471300SampleView.0         > Entering getElements(Object parent) 
 09:47:59.500 0x41471300SampleView.1         < Exiting getElements(Object parent) 

 Startup = 4.204 seconds
清单 17. Eclipse 启动,启用共享类
 09:30:40.171*0x41471300   j9trc.0         - Trace initialized for VM = 000962A8 
 [-Xshareclasses verbose output enabled] 
 JVMSHRC158I Successfully created shared class cache "eclipse" 
 JVMSHRC166I Attached to cache "eclipse", size=16777176 bytes 
 09:30:43.484 0x41471300SampleView.2         - Event id 1, text = Mark 
 09:30:43.484 0x41471300SampleView.0         > Entering getElements(Object 
 parent) 09:30:43.484 0x41471300SampleView.1         < Exiting 
 getElements(Object parent) 

 Startup = 3.313 seconds

Tomcat 性能:内存使用量

到目前为止,我们已经看到了在客户端环境中共享类产生的启动和内存使用量改进效果。在服务器端环境中也可以产生这些改进。正如前面提到的,我们使用 Tomcat 作为服务器端应用程序。Tomcat 并不要求为使用 IBM JVM 执行任何特殊步骤。使用共享类所需的惟一步骤是,为 JVM_OPTS 环境变量设置适当的值(见清单 18),Tomcat 根据这个变量用特定的命令行选项启动 JVM:

清单 18. 为 Tomcat 设置 JVM 选项
 export JAVA_OPTS="-Xmx32m -Xms32m -Xshareclasses:name=tomcat,verbose"

为了展示共享类在不同平台上的效果,我们使用 IBM JVM 和 Tomcat 的 Linux 版本。

正如前面提到的,top命令是在 Linux 上度量 Tomcat 内存使用量的好工具。对于这个示例,我们先在不启用共享类的情况下启动 Tomcat 并运行 top(从 JVM_OPTS 环境变量中删除 “-Xshareclasses:name=tomcat,verbose”),然后在启用共享类的情况下再测试一次。然后,启动第二个 Tomcat 实例,以此展示共享同一个类缓存的两个 Tomcat 进程的内存使用量差异。清单 19、清单 20 和清单 21 分别给出这三种情况下的 top输出。清单 22 给出共享类缓存统计数据。

清单 19. 不启用共享类时的 Tomcat 内存使用量
 Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie 
 Cpu(s):  0.1% us,  0.0% sy,  0.0% ni, 99.9% id,  0.0% wa,  0.0% hi,  0.0% si 
 Mem:   8157972k total,  1727072k used,  6430900k free,   101152k buffers 
 Swap:  2104472k total,        0k used,  2104472k free,  1370944k cached 

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND 
 24595 jbench    25   0 66744  54m 8400 S  0.0  0.7   0:03.71 java
清单 20. 启用共享类时的 Tomcat 内存使用量
 Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie 
 Cpu(s):  0.0% us,  0.0% sy,  0.0% ni, 99.9% id,  0.1% wa,  0.0% hi,  0.0% si 
 Mem:   8157972k total,  1728800k used,  6429172k free,   101152k buffers 
 Swap:  2104472k total,        0k used,  2104472k free,  1376084k cached 

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND 
 24621 jbench    17   0 78440  56m  14m S  0.0  0.7   0:04.04 java
清单 21. 共享同一个类缓存的两个 Tomcat 实例的内存使用量
 Tasks:   2 total,   0 running,   2 sleeping,   0 stopped,   0 zombie 
 Cpu(s):  0.0% us,  0.0% sy,  0.0% ni, 100.0% id,  0.0% wa,  0.0% hi,  0.0% si 
 Mem:   8157972k total,  1766440k used,  6391532k free,   101152k buffers 
 Swap:  2104472k total,        0k used,  2104472k free,  1376084k cached 

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND 
 24621 jbench    17   0 78440  56m  14m S  0.0  0.7   0:04.08 java 
 24674 jbench    16   0 77600  51m  14m S  0.0  0.6   0:02.28 java
清单 22. Tomcat 使用的缓存的当前统计数据
base address       = 0x76D0E000 
 end address        = 0x77D00000 
 allocation pointer = 0x77186268 

 cache size         = 16776852 
 free bytes         = 10085680 
 ROMClass bytes     = 5911028 
 AOT bytes          = 621280 
 Data bytes         = 57051 
 Metadata bytes     = 101813 
 Metadata % used    = 1% 

 # ROMClasses       = 1634 
 # AOT Methods      = 452 
 # Classpaths       = 6 
 # URLs             = 0 
 # Tokens           = 0 
 # Stale classes    = 0 
 % Stale classes    = 0% 

 Cache is 39% full

在刚看到启用和不启用共享类的 Tomcat 内存使用量对比结果时,看不出启用共享类的好处,因为内存使用量数字增加了。但是,如果仔细研究这些数字,就会看出真实的情况:

  • SHR 增加了大约 6MB(从 8400KB 增加到 14MB)。这是存储在共享类缓存中的数据量。
  • RES 略微增加了(从 54MB 增加到 56MB),这是由支持共享类所需的基础结构(对象库等)造成的。
  • VIRT 增加了,因为它是 SHR 和 RES 增加的值之和。

在启动第二个 Tomcat 实例并使用 top检查内存使用量时,可以看到第二个实例(清单 21 中的进程 24674)的共享内存量是相同的(都是 14MB SHR),但是 RES 减少了 5MB(从 56MB 减少到 51MB),虚拟内存也减少了。与 Windows 上的 vadump 一样,top会正确地识别出可能被共享的内存,但是并不显示实际连接到共享内存的其他进程。在这个示例中,两个 Tomcat 实例使用同一个共享类缓存,所以它们的总内存使用量会减少。在这个测试中,Tomcat 服务器只使用了缓存中不到一半的可用空间。清单 22 显示放在缓存中的可共享 ROMClass 数据有 5911028 字节(略微少于 6MB),这说明通过共享缓存中的类有可能进一步减少内存使用量。

Tomcat 性能:启动

启用共享类还会减少 Tomcat 的启动时间。为了度量启动时间,我们使用日志文件 catalina.out(位于 <TOMCAT_HOME>/logs)中记录的时间。为了给比较提供一个基准,先在不启用共享类的情况下启动 Tomcat。清单 23 给出报告的 Tomcat 启动时间(为简单起见,省略了在启动过程中记录的其他日志行):

清单 23. Tomcat 启动时间,不启用共享类
 24-Apr-2008 13:01:08 org.apache.catalina.startup.Catalina 
 start INFO: Server startup in 1138 ms

然后,将这一时间与启用共享类时 Tomcat 的启动时间(见清单 24)进行比较。

清单 24. Tomcat 启动时间,启用共享类,使用 AOT 代码
 24-Apr-2008 13:06:57 org.apache.catalina.startup.Catalina 
 start INFO: Server startup in 851 ms

可以看到,共享类使 Tomcat 启动时间从 1138ms 减少到了 851ms,这表示启动时间减少了 25%。这一改进是由启用类共享和使用 AOT 代码共同造成的。为了看到 AOT 代码产生多大的好处,可以使用命令行选项 -Xnoaot禁止使用 AOT 代码(见清单 25),然后再测试启动时间:

清单 25. 不使用 AOT 造成启动时间增加
 24-Apr-2008 13:03:50 org.apache.catalina.startup.Catalina 
 start INFO: Server startup in 950 ms

可以看到清单 25 显示的时间增加了,这说明在共享类缓存中存储 AOT 代码对减少 Tomcat 启动时间有很大好处。

结束语

本文展示了共享类在改进 Java 应用程序的启动时间和减少内存使用量两方面的效果。我们以 Tomcat 和 Eclipse 为例,演示了如何量化共享类特性对内存使用量和启动提供的好处。当然,应用程序的运行方式各不相同,因此获得的收益也不相同。但是,即使对于像这里提供的示例这样简单的配置,也会显著减少启动时间。

请记住,当多个应用程序运行同一级别的 IBM SDK 时,会获得最大的收益,因为它们有最多的东西可以共享。但是,即使是单一应用程序,也可以通过使用共享类缓存改进启动时间。

另外,我们讲解了如何通过工具(比如 Windows 上的 vadump 和 Linux 上的 top)重复计算共享内存,从而更准确地度量类共享所节省的内存量。尽管这些工具提供的内存使用量视图并不完美,但是我们讲解了如何读懂数据的含义。


下载

描述名字大小
源代码示例j-sharedclasses.jar6KB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=349526
ArticleTitle=使用类共享提高性能
publish-date=11032008