相关示例
介绍完性能调优的三个维度后, 我们来进行实际的操作以达成GC性能指标。
请看下面的代码:
//imports skipped for brevity
public class Producer implements Runnable {
private static ScheduledExecutorService executorService
= Executors.newScheduledThreadPool(2);
private Deque<byte[]> deque;
private int objectSize;
private int queueSize;
public Producer(int objectSize, int ttl) {
this.deque = new ArrayDeque<byte[]>();
this.objectSize = objectSize;
this.queueSize = ttl * 1000;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
deque.add(new byte[objectSize]);
if (deque.size() > queueSize) {
deque.poll();
}
}
}
public static void main(String[] args)
throws InterruptedException {
executorService.scheduleAtFixedRate(
new Producer(200 * 1024 * 1024 / 1000, 5),
0, 100, TimeUnit.MILLISECONDS
);
executorService.scheduleAtFixedRate(
new Producer(50 * 1024 * 1024 / 1000, 120),
0, 100, TimeUnit.MILLISECONDS);
TimeUnit.MINUTES.sleep(10);
executorService.shutdownNow();
}
}
这段程序代码, 每 100毫秒 提交两个作业(job)来。每个作业都模拟特定的生命周期: 创建对象, 然后在预定的时间释放, 接着就不管了, 由GC来自动回收占用的内存。
在运行这个示例程序时,通过以下JVM参数打开GC日志记录:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps
还应该加上JVM参数 -Xloggc
以指定GC日志的存储位置,类似这样:
-Xloggc:C:\\Producer_gc.log
在日志文件中可以看到GC的行为, 类似下面这样:
2015-06-04T13:34:16.119-0200: 1.723: [GC (Allocation Failure)
[PSYoungGen: 114016K->73191K(234496K)]
421540K->421269K(745984K),
0.0858176 secs]
[Times: user=0.04 sys=0.06, real=0.09 secs]
2015-06-04T13:34:16.738-0200: 2.342: [GC (Allocation Failure)
[PSYoungGen: 234462K->93677K(254976K)]
582540K->593275K(766464K),
0.2357086 secs]
[Times: user=0.11 sys=0.14, real=0.24 secs]
2015-06-04T13:34:16.974-0200: 2.578: [Full GC (Ergonomics)
[PSYoungGen: 93677K->70109K(254976K)]
[ParOldGen: 499597K->511230K(761856K)]
593275K->581339K(1016832K),
[Metaspace: 2936K->2936K(1056768K)],
0.0713174 secs]
[Times: user=0.21 sys=0.02, real=0.07 secs]
基于日志中的信息, 可以通过三个优化目标来提升性能:
- 确保最坏情况下,GC暂停时间不超过预定阀值
- 确保线程暂停的总时间不超过预定阀值
- 在确保达到延迟和吞吐量指标的情况下, 降低硬件配置以及成本。
为此, 用三种不同的配置, 将代码运行10分钟, 得到了三种不同的结果, 汇总如下:
堆内存大小(Heap) | GC算法(GC Algorithm) | 有效时间比(Useful work) | 最长停顿时间(Longest pause) |
---|---|---|---|
-Xmx12g | -XX:+UseConcMarkSweepGC | 89.8% | 560 ms |
-Xmx12g | -XX:+UseParallelGC | 91.5% | 1,104 ms |
-Xmx8g | -XX:+UseConcMarkSweepGC | 66.3% | 1,610 ms |
使用不同的GC算法,和不同的内存配置,运行相同的代码, 以测量GC暂停时间与 延迟、吞吐量的关系。实验的细节和结果在后面章节详细介绍。
注意, 为了尽量简单, 示例中只改变了很少的输入参数, 此实验也没有在不同CPU数量或者不同的堆布局下进行测试。
Tuning for Latency(调优延迟指标)
假设有一个需求, 每次作业必须在 1000ms 内处理完成。我们知道, 实际的作业处理只需要100 ms,简化后, 两者相减就可以算出对 GC暂停的延迟要求。现在需求变成: GC暂停不能超过900ms。这个问题很容易找到答案, 只需要解析GC日志文件, 并找出GC暂停中最大的那个暂停时间即可。
再来看测试所用的三个配置:
堆内存大小(Heap) | GC算法(GC Algorithm) | 有效时间比(Useful work) | 最长停顿时间(Longest pause) |
---|---|---|---|
-Xmx12g | -XX:+UseConcMarkSweepGC | 89.8% | 560 ms |
-Xmx12g | -XX:+UseParallelGC | 91.5% | 1,104 ms |
-Xmx8g | -XX:+UseConcMarkSweepGC | 66.3% | 1,610 ms |
可以看到,其中有一个配置达到了要求。运行的参数为:
java -Xmx12g -XX:+UseConcMarkSweepGC Producer
对应的GC日志中,暂停时间最大为 560 ms
, 这达到了延迟指标 900 ms
的要求。如果还满足吞吐量和系统容量需求的话,就可以说成功达成了GC调优目标, 调优结束。
Tuning for Throughput(吞吐量调优)
假定吞吐量指标为: 每小时完成 1300万次操作处理。同样是上面的配置, 其中有一种配置满足了需求:
堆内存大小(Heap) | GC算法(GC Algorithm) | 有效时间比(Useful work) | 最长停顿时间(Longest pause) |
---|---|---|---|
-Xmx12g | -XX:+UseConcMarkSweepGC | 89.8% | 560 ms |
-Xmx12g | -XX:+UseParallelGC | 91.5% | 1,104 ms |
-Xmx8g | -XX:+UseConcMarkSweepGC | 66.3% | 1,610 ms |
此配置对应的命令行参数为:
java -Xmx12g -XX:+UseParallelGC Producer
可以看到,GC占用了 8.5%的CPU时间,剩下的 91.5%
是有效的计算时间。为简单起见, 忽略示例中的其他安全点。现在需要考虑:
- 每个CPU核心处理一次作业需要耗时
100ms
- 因此, 一分钟内每个核心可以执行 60,000 次操作(每个job完成100次操作)
- 一小时内, 一个核心可以执行 360万次操作
- 有四个CPU内核, 则每小时可以执行: 4 x 3.6M = 1440万次操作
理论上,通过简单的计算就可以得出结论, 每小时可以执行的操作数为: 14.4 M * 91.5% = 13,176,000
次, 满足需求。
值得一提的是, 假若还要满足延迟指标, 那就有问题了, 最坏情况下, GC暂停时间为 1,104 ms
, 最大延迟时间是前一种配置的两倍。
Tuning for Capacity(调优系统容量)
假设需要将软件部署到服务器上(commodity-class hardware), 配置为 4核10G
。这样的话, 系统容量的要求就变成: 最大的堆内存空间不能超过 8GB
。有了这个需求, 我们需要调整为第三套配置进行测试:
堆内存大小(Heap) | GC算法(GC Algorithm) | 有效时间比(Useful work) | 最长停顿时间(Longest pause) |
---|---|---|---|
-Xmx12g | -XX:+UseConcMarkSweepGC | 89.8% | 560 ms |
-Xmx12g | -XX:+UseParallelGC | 91.5% | 1,104 ms |
-Xmx8g | -XX:+UseConcMarkSweepGC | 66.3% | 1,610 ms |
程序可以通过如下参数执行:
java -Xmx8g -XX:+UseConcMarkSweepGC Producer
测试结果是延迟大幅增长, 吞吐量同样大幅降低:
- 现在,GC占用了更多的CPU资源, 这个配置只有
66.3%
的有效CPU时间。因此,这个配置让吞吐量从最好的情况 13,176,000 操作/小时 下降到 不足 9,547,200次操作/小时. - 最坏情况下的延迟变成了 1,610 ms, 而不再是 560ms。
通过对这三个维度的介绍, 你应该了解, 不是简单的进行“性能(performance)”优化, 而是需要从三种不同的维度来进行考虑, 测量, 并调优延迟和吞吐量, 此外还需要考虑系统容量的约束。
原文链接: GC Tuning: Basics