JVM 调优 (1) -- 基础知识

  1. 最简单的 JVM 调优方式就是重启
  2. Linux 生产环境可以使用阿里开源的 Arthas 来进行调优,安装下载自行百度
  3. 调优重点考虑两个问题:CPU 飚高怎么办,内存飚高怎么办
  4. 确定了你的程序需要用多少资源了就将 最小堆大小和最大堆大小设置成一样,因为如果设置成不一样,那么 JVM 会一直去计算,不断扩展堆大小,而且如果堆多余了一些空间还需要缩容,这个过程是比较耗时间和性能的
  5. Capacity: 系统容量,意为硬件配置。

1. 调优前需要了解的一些基本概念

我们先来看一家工厂的装配流水线。工人在流水线将现成的组件按顺序拼接,组装成自行车。通过实地观测,我们发现从组件进入生产线,到另一端组装成自行车需要4小时。
在这里插入图片描述
继续观察,我们还发现,此后每分钟就有 1 辆自行车完成组装,每天 24 小时,一直如此。将这个模型简化,并忽略维护窗口期后得出结论:这条流水线每小时可以组装 60 辆自行车。

时间窗口/窗口期,类比车站卖票的窗口,是一段规定做某件事的时间段。

通过这两种测量方法,就知道了生产线的相关性能信息:延迟与吞吐量

  1. 生产线的延迟:4 小时
  2. 生产线的吞吐量:60 辆/小时

请注意,衡量延迟的时间单位根据具体需要而确定 —— 从纳秒 (nanosecond) 到几千年 (millennia) 都有可能。系统的吞吐量是每个单位时间内完成的操作。操作 (Operations) 一般是特定系统相关的东西。在本例中,选择的时间单位是小时,操作就是对自行车的组装。

掌握了延迟和吞吐量两个概念之后, 让我们对这个工厂来进行实际的调优。自行车的需求在一段时间内都很稳定,生产线组装自行车有 4 个小时延迟,而吞吐量在几个月以来都很稳定:60 辆/小时。假设某个销售团队突然业绩暴涨,对自行车的需求增加了 1 倍。客户每天需要的自行车不再是 60 * 24 = 1440辆,而是 2*1440 = 2880 辆/天。老板对工厂的产能不满意,想要做些调整以提升产能。

看起来总经理很容易得出正确的判断,系统的延迟没法子进行处理 —— 他关注的是每天的自行车生产总量。得出这个结论以后,假若工厂资金充足,那么应该立即采取措施,改善吞吐量以增加产能。

我们很快会看到,这家工厂有两条相同的生产线。每条生产线一分钟可以组装一辆成品自行车。可以想象,每天生产的自行车数量会增加一倍。达到 2880 辆/天。要注意的是, 不需要减少自行车的装配时间 —— 从开始到结束依然需要 4 小时。
在这里插入图片描述
巧合的是,这样进行的性能优化,同时增加了吞吐量和产能。一般来说,我们会先测量当前的系统性能,再设定新目标,只优化系统的某个方面来满足性能指标。

在这里做了一个很重要的决定 —— 要增加吞吐量,而不是减小延迟。在增加吞吐量的同时, 也需要增加系统容量。比起原来的情况,现在需要两条流水线来生产出所需的自行车。在这种情况下,增加系统的吞吐量并不是免费的,需要水平扩展,以满足增加的吞吐量需求。

在处理性能问题时,应该考虑到还有另一种看似不相关的解决办法。假如生产线的延迟从 1 分钟降低为 30 秒,那么吞吐量同样可以增长 1 倍。

或者是降低延迟,或者是客户非常有钱。软件工程里有一种相似的说法 —— 每个性能问题背后,总有两种不同的解决办法。可以用更多的机器,或者是花精力来改善性能低下的代码。

1. 延迟

GC 的延迟指标由一般的延迟需求决定。延迟指标通常如下所述:

  1. 所有交易必须在 10 秒内得到响应
  2. 90% 的订单付款操作必须在 3 秒以内处理完成
  3. 推荐商品必须在 100 ms 内展示到用户面前

面对这类性能指标时,需要确保在交易过程中,GC 暂停不能占用太多时间,否则就满足不了指标。“不能占用太多” 的意思需要视具体情况而定,还要考虑到其他因素,比如外部数据源的交互时间 (round-trips),锁竞争 (lock contention),以及其他的安全点等等。

假设性能需求为:90% 的交易要在 1000ms 以内完成,每次交易最长不能超过 10 秒。根据经验,假设 GC 暂停时间比例不能超过10%。也就是说,90% 的 GC 暂停必须在 100ms 内结束,也不能有超过 1000ms 的 GC 暂停。为简单起见,我们忽略在同一次交易过程中发生多次 GC 停顿的可能性。

有了正式的需求,下一步就是检查暂停时间。有许多工具可以使用,我们通过查看 GC 日志,检查一下 GC 暂停的时间。相关的信息散落在不同的日志片段中,看下面的数据:

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 暂停,在 2015-06-04T13:34:16 这个时刻触发,对应于 JVM 启动之后的 2,578 ms,此事件将应用线程暂停了 0.0713174 秒。虽然花费的总时间为 210 ms,但因为是多核 CPU 机器,所以最重要的数字是应用线程被暂停的总时间,这里使用的是并行 GC,所以暂停时间大约为 70 ms 。 这次 GC 的暂停时间小于 100ms 的阈值,满足需求。继续分析,从所有 GC 日志中提取出暂停相关的数据,汇总之后就可以得知是否满足需求。

2. 吞吐量

吞吐量和延迟指标有很大区别。当然两者都是根据一般吞吐量需求而得出的。一般吞吐量需求 (Generic requirements for throughput) 类似这样:

  1. 解决方案每天必须处理 100 万个订单
  2. 解决方案必须支持 1000 个登录用户,同时在 5-10 秒内执行某个操作:A、B 或 C
  3. 每周对所有客户进行统计,时间不能超过 6 小时,时间窗口为每周日晚 12 点到次日 6 点之间

可以看出,吞吐量需求不是针对单个操作的,而是在给定的时间内,系统必须完成多少个操作。和延迟需求类似,GC 调优也需要确定 GC 行为所消耗的总时间。每个系统能接受的时间不同,一般来说,GC 占用的总时间比不能超过 10%。

现在假设需求为:每分钟处理 1000 笔交易。同时, 每分钟 GC 暂停的总时间不能超过 6 秒(即 10%)。
有了正式的需求,下一步就是获取相关的信息。依然是从 GC 日志中提取数据,可以看到类似这样的信息:

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

此时我们对用户耗时 (user) 和系统耗时 (sys) 感兴趣,而不关心实际耗时 (real)。在这里,我们关心的时间为 0.23s(user + sys = 0.21 + 0.02 s),这段时间内, GC 暂停占用了 CPU 资源。 重要的是,系统运行在多核机器上,转换为实际的停顿时间 (stop-the-world) 为 0.0713174 秒,下面的计算会用到这个数字。

提取出有用的信息后,剩下要做的就是统计每分钟内 GC 暂停的总时间。看看是否满足需求,每分钟内总的暂停时间不得超过 6000毫秒 (6秒)。

3. 系统容量

系统容量 (Capacity) 需求,是在达成吞吐量和延迟指标的情况下,对硬件环境的额外约束。这类需求大多是来源于计算资源或者预算方面的原因。例如:

  1. 系统必须能部署到小于 512 MB 内存的 Android 设备上
  2. 系统必须部署在 Amazon EC2 实例上,配置不得超过 c3.xlarge(4核8GB)。
  3. 每月的 Amazon EC2 账单不得超过 $12,000

因此,在满足延迟和吞吐量需求的基础上必须考虑系统容量。可以说,假若有无限的计算资源可供挥霍,那么任何 延迟和吞吐量指标 都不成问题,但现实情况是,预算(budget)和其他约束限制了可用的资源。

4. 案例分析

介绍完性能调优的三个维度后,我们来进行实际的操作以达成 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 -Xloggc:C:\\Producer_gc.log
-Xloggc:C:\Producer_gc.log:指定 GC 日志的存储位置
在日志文件中可以看到 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]

基于日志中的信息,可以通过三个优化目标来提升性能:

  1. 确保最坏情况下,GC 暂停时间不超过预定阀值
  2. 确保线程暂停的总时间不超过预定阀值
  3. 在确保达到延迟和吞吐量指标的情况下,降低硬件配置以及成本。

为此,用三种不同的配置,将代码运行 10 分钟,得到了三种不同的结果,汇总如下:
在这里插入图片描述
使用不同的 GC 算法,和不同的内存配置,运行相同的代码,以测量 GC 暂停时间与延迟、吞吐量的关系。

注意,为了尽量简单,示例中只改变了很少的输入参数,此实验也没有在不同 CPU 数量或者不同的堆布局下进行测试。

2. GC 调优简介

GC 调优 (Tuning Garbage Collection) 和其他性能调优是同样的原理。初学者可能会被 200 多个 GC 参数弄得一头雾水,然后随便调整几个来试试结果,又或者修改几行代码来测试。其实只要参照下面的步骤,就能保证你的调优方向正确:

  1. 列出性能调优指标 (State your performance goals)
  2. 执行测试 (Run tests)
  3. 检查结果 (Measure the results)
  4. 与目标进行对比 (Compare the results with the goals)
  5. 如果达不到指标,修改配置参数,然后继续测试 (go back to running tests)

第一步, 我们需要做的事情就是:制定明确的 GC 性能指标。对所有性能监控和管理来说,有三个维度是通用的:

  1. Latency (延迟)
  2. Throughput (吞吐量)
  3. Capacity (系统容量)

1. Tuning for Latency(调优延迟指标)

假设有一个需求,每次作业必须在 1000ms 内处理完成。我们知道,实际的作业处理只需要 100 ms,简化后,两者相减就可以算出对 GC 暂停的延迟要求。现在需求变成:GC 暂停不能超过 900ms。这个问题很容易找到答案,只需要解析 GC 日志文件,并找出 GC 暂停中最大的那个暂停时间即可。

再来看测试所用的三个配置:
在这里插入图片描述
可以看到,其中有一个配置达到了要求。运行的参数为:java -Xmx12g -XX:+UseConcMarkSweepGC Producer

对应的 GC 日志中,暂停时间最大为 560 ms,这达到了延迟指标 900 ms 的要求。如果还满足吞吐量和系统容量需求的话,就可以说成功达成了 GC 调优目标,调优结束。

2. Tuning for Throughput(吞吐量调优)

假定吞吐量指标为:每小时完成 1300 万次操作处理。同样是上面的配置,其中有一种配置满足了需求:
在这里插入图片描述
此配置对应的命令行参数为:java -Xmx12g -XX:+UseParallelGC Producer

可以看到,GC 占用了 8.5% 的 CPU 时间,剩下的 91.5% 是有效的计算时间。为简单起见,忽略示例中的其他安全点。现在需要考虑:

  1. 每个 CPU 核心处理一次作业需要耗时 100ms
  2. 因此,一分钟内每个核心可以执行 60,000 次操作(每个 job 完成 100 次操作)
  3. 一小时内,一个核心可以执行 360 万次操作
  4. 有四个 CPU 内核,则每小时可以执行 4 x 3.6M = 1440 万次操作

理论上,通过简单的计算就可以得出结论,每小时可以执行的操作数为 14.4 M * 91.5% = 13,176,000 次,满足需求。

值得一提的是,假若还要满足延迟指标,那就有问题了,最坏情况下,GC 暂停时间为 1,104 ms,最大延迟时间是前一种配置的两倍。

3. Tuning for Capacity(调优系统容量)

假设需要将软件部署到服务器上 (commodity-class hardware),配置为 4 核 10G。这样的话,系统容量的要求就变成,最大的堆内存空间不能超过 8GB。有了这个需求,我们需要调整为第三套配置进行测试:
在这里插入图片描述
此配置对应的命令行参数为:java -Xmx8g -XX:+UseConcMarkSweepGC Producer

测试结果是延迟大幅增长,吞吐量同样大幅降低:

  1. 现在,GC 占用了更多的 CPU 资源,这个配置只有 66.3% 的有效 CPU 时间。因此,这个配置让吞吐量从最好的情况 13,176,000 操作/小时下降到不足 9,547,200 次操作/小时
  2. 最坏情况下的延迟变成了 1,610 ms,而不再是 560ms。

通过对这三个维度的介绍,你应该了解,不是简单的进行“性能(performance)”优化,而是需要从三种不同的维度来进行考虑、测量,并调优延迟和吞吐量,此外还需要考虑系统容量的约束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值