如果我们收集完成每个 TaskHandler.run() 调用的时间统计信息,我们可以看到 JVM 和应用程序设计引入了多少易变性。我们使用具有 8 个物理核心的 IBM xServer e5440,安装了 Red Hat RHEL MRG 实时操作系统。(禁用了超线程。注意,尽管超线程可以在基准测试中提供一些吞吐量改进,但是由于其虚拟核心并不完整,所以在启用超线程的处理器上的操作的物理核心性能可能具有明显不同的计时)。当在 8 核心机器上使用 IBM Java6 SR3 JVM 和 6 个线程运行此服务器时(我们将一个核心保留给 Server 主线程,另一个核心供 GCStressorThread 使用),我们得到了以下(代表性的)结果:
$ java -Xms700m -Xmx700m -Xgcpolicy:optthruput Server 6
10000 operations in 16582 ms
Throughput is 603 operations / second
Histogram of operation times:
9ms - 10ms 9942 99 %
10ms - 11ms 2 0 %
11ms - 12ms 32 0 %
30ms - 40ms 4 0 %
70ms - 80ms 1 0 %
200ms - 300ms 6 0 %
400ms - 500ms 6 0 %
500ms - 542ms 6 0 %
可以看到,几乎所有操作都在 10 毫秒内完成,但是一些操作花了超过半秒(长 50 倍)的时间。这个差异太大了!我们看看如何消除 Java 加载、JIT 本机代码编译、GC 和线程导致的延迟,从而消除这种易变性。
我们首先通过 -verbose:class 完整地运行应用程序,收集应用程序加载的类列表。我们将输出存储到一个文件,然后修改它,使该文件的每行上都具有一个格式正确的名称。我们将一个 preload() 方法包含到 Server 类中,以加载每个类,JIT 编译这些类的所有方法,然后禁用 JIT 编译器,如清单 9 所示:
清单 9. 预加载服务器的类和方法
private void preload(String classesFileName) {
try {
FileReader fReader = new FileReader(classesFileName);
BufferedReader reader = new BufferedReader(fReader);
String className = reader.readLine();
while (className != null) {
try {
Class clazz = Class.forName(className);
String n = clazz.getName();
Compiler.compileClass(clazz);
} catch (Exception e) {
}
className = reader.readLine();
}
} catch (Exception e) {
}
Compiler.disable();
}
在这个简单的服务器中,类加载并不是一个重大问题,因为 TaskHandler.run() 方法非常简单:一旦加载该类,就不会在以后执行 Server 时发生太多类加载操作,这可以通过运行 -verbose:class 来验证。主要的优点源自于在运行任何测试 TaskHandler 操作之前运行方法。尽管我们可以使用预备循环,但此方法是特定于 JVM 的,因为 JIT 编译器用于选择要编译方法的启发机制在各个 JVM 实现之间有所不同。使用 Compiler.compile() 服务器会提供更加可控的编译行为,但正如本文前面提到的,使用此方法应该会使吞吐量下降。使用这些选项运行应用程序的结果如下:
$ java -Xms700m -Xmx700m -Xgcpolicy:optthruput Server 6
10000 operations in 20936 ms
Throughput is 477 operations / second
Histogram of operation times:
11ms - 12ms 9509 95 %
12ms - 13ms 478 4 %
13ms - 14ms 1 0 %
400ms - 500ms 6 0 %
500ms - 527ms 6 0 %
注意,尽管最长的延迟没有多大变化,但直方图比最初小多了。JTI 编译器明确引入了许多较短的延迟,所以较早执行编译,然后禁用 JIT 编译器显然是一大进步。另一个有趣的结果是,普通操作时间变得更长了(从 9 - 10 毫秒增加到了 11 - 12 毫秒)。操作变慢了,原因在于在调用方法之前强制执行 JIT 编译所生成的代码质量比完整编译的代码质量要低很多。这个结果并不奇怪,因为 JIT 编译器的一个最大优势是利用应用程序的动态特征来使其更高效地运行。
我们将在本文余下部分继续使用此类预加载和方法预编译代码。
由于我们的 GCStressThread 生成了一个不断变化的活动数据集,所以使用分代 GC 策略并不能提供太多暂停时间优势。相反,我们尝试使用 IBM WebSphere Real Time for Real Time Linux V2.0 SR1 产品中的实时垃圾收集器。最初的结果令人失望,甚至在添加了 -Xgcthreads8 选项之后也是如此,该选项允许收集器使用 8 个 GC 线程,而不是默认的 1 个线程。(使用一个 GC 线程,收集器无法可靠地跟上此应用程序的分配速度)。
$ java -Xms700m -Xmx700m -Xgcpolicy:metronome -Xgcthreads8 Server 6
10000 operations in 72024 ms
Throughput is 138 operations / second
Histogram of operation times:
11ms - 12ms 82 0 %
12ms - 13ms 250 2 %
13ms - 14ms 19 0 %
14ms - 15ms 50 0 %
15ms - 16ms 339 3 %
16ms - 17ms 889 8 %
17ms - 18ms 730 7 %
18ms - 19ms 411 4 %
19ms - 20ms 287 2 %
20ms - 30ms 1051 10 %
责任编辑:小草