|
共享类基础结构是在 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 代码如何占用缓存空间,以及缓存空间设置如何控制它们使用的可用空间额度。
稍后进一步讨论 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.equals 和 java/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 = 5950936
AOT 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 部分,以及类数据共享如何影响 Private、Shareable 和 Shared 在这个数字中的比例。清单 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:
- 打开 Task Manager 应用程序并选择 Processes 选项卡。
- 找到称为 PID 的列。(如果没有出现这个列,那么单击 View > Select Columns 并选择 PID 复选框,见图 2)。
- 找到希望检查的进程,记下 PID 列中的值。这就是需要传递给 vadump 的进程 ID。
图 2. 在 Task Manager 中寻找进程 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 包。
如果使用 Linux 或 AIX,那么下载单独的 IBM SDK for Java 6 实现,然后从 Eclipse 项目网站下载所需的 Eclipse 版本(参见 参考资料)。按照 Eclipse 安装说明配置 Eclipse,让它能够使用 IBM SDK for Java 6。
在安装 Eclipse 之后,还需要执行以下步骤:
- 为插件启用类共享。把 OSGI 插件适配器(参见 参考资料)安装到 Eclipse 插件目录中。
- 下载 SampleView.jar(参见 下载)并把它安装到 Eclipse 插件目录中。在视图初始化时,这个插件连接 IBM JVM 跟踪并输出一些跟踪点,从而简化了对 Eclipse 启动时间的计时。我们将在下一节中讨论如何使用 IBM JVM 跟踪提供启动统计数据。
- 创建两个工作空间 workspace1 和 workspace2。这样就可以启动两个 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)重复计算共享内存,从而更准确地度量类共享所节省的内存量。尽管这些工具提供的内存使用量视图并不完美,但是我们讲解了如何读懂数据的含义。 |
|