阅读 67

如何调优 Java 垃圾收集

概述

在第一篇文章简单理解 Java 垃圾收集器,我们了解了不同 GC 算法的流程,GC 是如何工作的,什么是年轻代和老年代,你应该了解的 JDK 7 中的 5 种类型的 GC。

在第二篇文章如何监控 Java 垃圾回收,已经解释了如何监控 GC,以及我们可以使用哪些工具来使这个过程更快更有效。

在这篇文章中将展示一些可用于 GC 调优的最佳选项,阅读这篇文章的前提是你已经理解了本系列的前几篇文章。因此,为了你的进一步理解,如果你还没有阅读前两篇文章,请在阅读本文之前先阅读。

是否需要 GC 调优?

或者更准确地说 基于 Java 的服务 是否需要 GC 调优?事实上并不是所有基于 Java 的服务都需要GC 调优,也就是说运行中的基于 Java 的系统具有以下选项和操作通常不需要进行 GC 调优:

  • 已使用-Xms–Xmx选项指定内存大小。

  • 系统中没有超时日志。

换句话说,如果你没有设置内存大小,并且打印了太多的Timeout日志,你就需要对你的系统进行GC调优了。

但是,要记住一件事:GC 调优是最后一个要完成的任务。

想想GC调优的根本原因:垃圾收集器清除在 Java 中创建的对象,需要清除的对象数量以及要执行的 GC 次数取决于已创建的对象数量。因此要控制系统执行的 GC,首先应该减少创建的对象数量

  • 我们可以用 StringBuilderStringBuffer 来代替 String,减少创建字符串对象。

  • 并且最好尽可能少地积累日志。

我们知道在某些情况下我们无能为力,我们已经看到 XML 和 JSON 解析过程中会使用比较多的内存,尽管我们减少 String 的使用,并尽可能地处理日志,但仍会使用巨大的临时内存来解析 XML 或 JSON,大约 10-100 MB。我们很难不使用 XML 和 JSON,只要明白它需要太多的内存。

如果应用程序内存使用率在反复调优后有所改善,则可以开始 GC 调优,我们将 GC 调优的目的分为两种:

  1. 尽量减少传递到老年代的对象数量

  2. 减少 Full GC 的执行时间

尽量减少传递到老年代的对象数量

分代 GC 是 Oracle JVM 提供的 GC,不包括 JDK 7 及更高版本可以使用的 G1 GC。换句话说,在 Eden 区域中创建一个对象并从 Survivor0 区域转移和转移到 Survivor1 区域,达到一定的次数之后,剩下的对象被移动到老年代。有些对象是在Eden区创建的,因为体积大,直接传递到Old区。Old 区的 GC 比 New 区的 GC 花费的时间相对更多。因此减少传递到 Old 区域的对象数量可以降低 full GC 的频率,减少传递到 Old 区的对象数量可能会被误解为选择将对象留在 New 区,然而并不是这样,相反,你可以调整新生代区域的大小

减少 Full GC 时间

Full GC 的执行时间比 Minor GC 的执行时间相对较长,因此如果Full GC 执行时间过长(1 秒或以上),可能会导致多个连接部分发生超时。

  • 如果尝试减小 Old 区域大小以减少 Full GC 执行时间,则可能会发生 OutOfMemoryError 或 Full GC 的次数可能会增加。

  • 如果尝试增加 Old 区域大小来减少 Full GC 的次数,则会增加执行时间。

因此,我们需要将 Old 区域大小设置为“适当”的值

影响 GC 性能的选项

前面我们提到,有人在设置某个GC选项时性能很好,为什么我们不像他那样使用那个选项? 原因是不同 web 服务中的对象大小和生命周期是不同的

简单想一下,如果在A、B、C、D、E的条件下执行一项任务,而在只有 A 和 B 的条件下执行相同的任务,那么哪个任务完成得更快?从常识的角度来看,答案将是在 A 和 B 条件下执行的任务。Java GC 选项是相同的,设置多个选项并不会提高执行 GC 的速度,相反它可能会使 GC 变慢。

GC 调优的基本原理是将不同的 GC 选项应用到两个或多个服务器上并进行比较,然后通过性能分析选择表现出增强的性能或更好的 GC 时间的服务器设置的选项

下表显示了可能影响性能的 GC 选项中与内存大小相关的选项。

表 1:要为 GC 调整检查的 JVM 选项。

分类选项描述
堆区大小-Xms启动 JVM 时的堆区大小

-Xmx最大堆区大小
新生代区域大小-XX:NewRatio新生代与老年代比例

-XX:NewSize新生代大小

-XX:SurvivorRatioEden 区与 Survivor 区的比例

我经常使用 -Xms-Xmx-XX:NewRatio 选项进行 GC 调整。  -Xms-Xmx选项是特别需要的,并且如何设置 NewRatio 选项会也对 GC 性能产生重大影响。

有人问 如何设置Perm区域大小?可以使用-XX:PermSize-XX:MaxPermSize选项设置 Perm 区域大小,但仅当OutOfMemoryError发生且原因是 Perm 区域大小时才需要设置。

另一个可能影响 GC 性能的选项是GC 类型。下表按 GC 类型显示了可用选项(基于 JDK 6.0)。

表 2:GC 类型的可用选项。

分类选项说明
Serial GC-XX:+UseSerialGC
Parallel GC-XX:+UseParallelGC
-XX:ParallelGCThreads=value

Parallel Compacting GC-XX:+UseParallelOldGC
CMS GC-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX: CMSInitiatingOccupancyFraction=value
-XX:+UseCMSInitiatingOccupancyOnly

G1-XX:+UnlockExperimentalVMOptions
-XX:+UseG1GC
在 JDK 6 中,这两个选项必须一起使用。

除了 G1 GC,GC 类型是通过在每个 GC 类型的第一行设置选项来更改的。

有很多选项会影响 GC 性能,但是你可以通过设置上面提到的选项来获得显着的效果,但是请记住设置太多选项并不能保证减少 GC 执行时间。

GC 调优程序

GC 调优的过程类似于一般的性能改进过程,以下是我使用的 GC 调整过程。

1.监控GC状态

需要监控 GC 状态以检查运行中系统的 GC 状态。请参阅如何监控 Java 垃圾回收。

2. 分析监测结果后决定是否需要 GC 调优

检查GC状态后,你应该分析监控结果并决定是否调整 GC。

如果分析显示执行 GC 所花费的时间仅为 0.1-0.3秒,那么无需将时间浪费在调整 GC 上。

如果 GC 执行时间在 1-3 秒,或者超过 10 秒,则需要进行 GC 调优

但是,如果你分配了大约 10GB 的 Java 内存并且无法减少内存大小,则无法调整 GC。在调优 GC 之前,你需要考虑为什么需要分配大内存大小。如果你分配了1GB或2GB的内存并OutOfMemoryError发生了,则应执行 heap dump 来验证并排除原因。

注意:

Heap dump 是一个内存文件,用于检查 Java 内存中的对象和数据,可以使用 JDK 中包含的 jmap 命令创建此文件。创建文件时,Java 进程停止,因此不要在系统运行时创建此文件。

3.设置GC类型/内存大小

如果你已决定进行 GC 调优,请选择 GC 类型并设置内存大小。这时候如果你有多台服务器,通过为每个服务器设置不同的GC选项来检查每个GC选项的差异很重要。

4. 分析结果

在设置 GC 选项后收集数据至少 24 小时后开始分析结果,如果幸运的话,你会找到最适合系统的 GC 选项。如果不是,则应分析日志并检查内存是如何分配的。然后你需要通过更改 GC 类型/内存大小来找到系统的最佳选项。

5. 如果结果令人满意,则将该选项应用于所有服务器并终止 GC 调整。

如果 GC 调优结果令人满意,则将该选项应用于所有服务器并终止 GC 调优。

监控 GC 状态和分析结果

检查运行中的 Web 应用程序服务器 (WAS) 的 GC 状态的最佳方法是使用 jstat 命令。在如何监控 Java 垃圾回收解释了 jstat 命令,所以我将在本文中描述要检查的数据。

下面的例子展示了一个没有做GC调优的JVM(但是它不是操作服务器)。

-gcutil :以百分比的形式显示每个堆区域的使用率,以及GC的次数和累计GC时间

$ jstat -gcutil 21719 1s
S0    S1    E    O    P    YGC    YGCT    FGC    FGCT GCT
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673
48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673复制代码

在这里检查 YGC 和 YGCT 的值,将 YGCT 除以 YGC,然后你得到 0.050 秒(50 毫秒),这意味着在 Young 区域执行 GC 平均需要 50 ms,有了这个结果,你就不需要关心 Young 区域的 GC 调整了。

现在检查 FGCT 和 FGC 的值,将 FGCT 除以 FGC,然后你得到 19.68 秒,这意味着执行 GC 平均需要 19.68 秒,需要进行 GC 调整。

我们可以使用 jstat 命令轻松检查 GC 状态,但是分析 GC 的最佳方法是使用 –verbosegc 选项生成日志,关于如何生成日志和分析日志的工具的详细描述,在上一篇文章中已经解释过了。在用于分析日志的工具中,HPJMeter 是我最喜欢的用来分享 -verbosegc 日志的工具,它易于使用和分析,使用 HPJmeter可以轻松检查 GC 执行时间的分布和 GC 发生的频率。

如果 GC 执行时间满足以下所有条件,则不需要进行 GC 调优。

  • Minor GC 处理速度很快(50 毫秒内)。

  • Minor GC 不经常执行(大约 10 秒)。

  • Full GC 处理速度很快(1 秒内)。

  • Full GC 不经常执行(每 10 分钟一次)。

括号内的数值不是绝对值,它们因服务状态而异,有些服务可能会满足 0.9 秒的 Full GC 处理速度,但有些服务可能不会,因此请检查这些值并通过考虑每个服务的实际情况来决定是否执行 GC 调整。

当检查 GC 状态时应该注意一件事,不要只检查 Minor GC 和 Full GC 的时间,还必须检查 GC 执行次数,如果 New area size 太小,会导致Minor GC执行过于频繁(有时每1秒一次或多次),此外传递到Old区的对象数量增加,导致Full GC执行次数增加,因此应用jstat –gccapacity  命令中的选项来检查该区域被占用了多少。

设置 GC 类型/内存大小

设置 GC 类型

Oracle JVM 有五种 GC 类型,但是如果不是 JDK 7,则应选择 Parallel GCParallel Compacting GCCMS GC 之一,没有原则或规则来决定选择哪一个。

如果是这样,我们如何选择合适的 GC 类型? 最推荐的方法是应用所有三个。然而,有一点很清楚——CMS GC 比其他并行 GC 更快。这时候,如果是这样,只需应用CMS GC。然而 CMS GC 并不总是更快,通常 CMS GC 的 Full GC 速度很快,但是当发生并发模式失败时,它比其他 Parallel GC 慢。

并发模式失败

让我们更深入地研究并发模式失败。

Parallel GCCMS GC 最大的区别在于压缩任务,压缩任务是通过压缩内存来消除内存碎片,以去除分配的内存区域之间的空白空间。

在 Parallel GC 类型中,每次执行 Full GC 时都会执行压缩,花费太多时间。但是在执行 Full GC 后,内存可以以更快的方式分配,因为可以顺序分配下一个内存。

相反,CMS GC 不伴随压缩,因此 CMS GC 的执行速度更快。但是,当不执行compaction时,内存中会产生一些空白空间,就像执行磁盘碎片整理程序之前一样,可能没有空间容纳大对象。例如 Old 区还剩 300 MB,但有些 10 MB 的对象无法依次保存在该区中,因为没有连续的 10 MB 的内存空间,在这种情况下会出现 并发模式失败 警告并执行压缩。如果使用 CMS GC,则执行压缩比其他 Parallel GC 需要更长的时间

总之,你应该为你的系统找到最佳的 GC 类型。

每个系统都需要合适的 GC 类型,因此你需要为你的系统找到最佳的 GC 类型。如果你正在运行六台服务器,我建议你为两台服务器中的每台设置相同的选项,添加-verbosegc选项,然后分析结果。

设置内存大小

下面展示了内存大小、GC执行次数、GC执行时间之间的关系:

  • 大内存

    • 减少 GC 执行的次数。

    • 增加 GC 执行时间。

  • 小内存

    • 减少 GC 执行时间。

    • 增加 GC 执行的次数。

将内存大小设置为多大没有正确的答案。如果是这样,我们应该如何设置内存大小? 通常我建议使用 500 MB。但请注意,这并不意味着你应该使用–Xms500m–Xmx500m选项设置 web 应用的内存。根据GC调优前的当前状态,查看Full GC后剩余的内存大小。如果Full GC后还剩300MB左右,最好将内存设置为1GB(300MB(默认使用)+500MB(Old区最小)+200MB(空闲内存)),这意味着你应该为 Old 区域设置超过 500 MB 的内存空间。

如果你有三台操作服务器,请将一台服务器设置为1 GB,一台设置为1.5 GB,一台设置为2 GB,然后查看结果。理论上,GC 将按照 1 GB > 1.5 GB > 2 GB 的顺序快速完成,因此 1 GB 将是执行 GC 的最快速度。但是不能保证在 1 GB 时执行 Full GC 需要 1 秒,在 2 GB 时执行 Full GC 需要 2 秒。时间取决于服务器性能和对象大小,因此创建测量数据的最佳方法是设置尽可能多的数据并对其进行监控。

你应该再设置一个参数:NewRatioNewRatio是 New 区和 Old 区的比值。如果XX:NewRatio=1New 区:Old 区是 1:1,对于 1 GB,新区:旧区为 500MB:500MB。如果NewRatio是 2,则New 区:Old 区是 1:2,因此该值越大,Old 区域大小越大,New 区域大小越小。

NewRatio 会显着影响整个 GC 性能,如果New area 内存太小,就会有很多内存分配给 Old area,导致Full GC频繁,处理时间长。

你可能只是认为那NewRatio 1 是最好的,然而事实可能并非如此,当NewRatio设置为 2 或 3 时,整个 GC 状态可能会更好。

完成 GC 调优的最快方法是什么? 比较性能测试的结果是获得结果的最快方法。要为每个服务器设置不同的选项并监控状态,建议至少在一两天后检查数据。但是,通过性能测试执行GC调优时,你应该准备好与操作情况相同的负载。并且提供负载的URL等请求比例必须与操作情况一致。但是,对于专业的性能测试人员来说,给出准确的负载并不容易,而且准备时间太长。

分析 GC 调优结果

应用GC选项并设置该 -verbosegc 选项后,使用tail命令检查日志是否按需要累积。如果该选项没有准确设置并且没有累积日志,你将浪费你的时间。如果日志按需要累积,请在收集数据一两天后查看结果。最简单的方法是将日志移动到本地 PC 并使用HPJMeter分析数据。

在分析中,重点关注以下内容,决定 GC 选项的最重要的一项是 Full GC 执行时间。

  • Full GC 执行时间

  • Minor GC 执行时间

  • Full GC 执行间隔

  • Minor GC 执行间隔

  • 整个 Full GC 执行时间

  • 整个 Minor GC 执行时间

  • 整个GC执行时间

  • Full GC 执行次数

  • Minor GC 执行次数

找到最合适的 GC 选项是一个非常幸运的,而在大多数情况下,事实并非如此。执行 GC 调优时要小心,因为如果你尝试一次完成 GC 调优,可能会发生OutOfMemoryError这种情况。

调优示例

到目前为止,我们已经在没有任何示例的情况下从理论上讨论了 GC 调优。现在我们来看看 GC 调优的例子。

示例 1

以下示例是 Service S 的 GC 调整。对于新开发的Service S,执行Full GC花费了太多时间。

查看结果jstat –gcutil

S0     S1    E    O     P   YGC YGCT  FGC FGCT GCT
12.16 0.00 5.18 63.78 20.32 54  2.047 5  6.946  8.993复制代码

执行 Minor GC 和 Full GC 一次的平均值计算如下:

表 3:为服务 S 执行 Minor GC 和 Full GC 所需的平均时间。

GC 类型GC 执行时间GC 执行时间平均数
Minor GC542.04737 毫秒
Full GC56.9461,389 毫秒

37 ms 对于 Minor GC 来说还不错。但是Full GC的 1.389秒 意味着在 DB Timeout 设置为1秒的系统中发生GC时,可能会频繁发生超时,在这种情况下,系统需要 GC 调优。

首先,你应该在开始 GC 调整之前检查内存是如何使用的,使用该 jstat –gccapacity 选项检查内存使用情况。从该服务器检查的结果如下。

NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC
212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5复制代码

关键值如下。

  • New 区使用大小:212,992 KB

  • Old 区使用大小:1,884,160 KB

因此,总分配的内存大小为 2 GB,不包括 Perm 区域,New area:Old area 为 1:9。为了以比jstat更详细的方式检查状态,-verbosegc 添加了日志并为三个实例设置了三个选项,如下所示。没有添加其他选项。

  • NewRatio=2

  • NewRatio=3

  • NewRatio=4

一天后,系统的GC日志已经检查过了。幸运的是,这个系统在NewRatio设置后没有发生Full GC 。

为什么? 原因是系统创建的大部分对象很快就会被销毁,所以这些对象没有传递到Old区而是在New区销毁。

在此状态下,无需更改其他选项,只需为选择最佳值NewRatio。那么我们如何确定最佳值呢? 要得到它,需要分析每个 Minor GC 的平均执行时间 。

Minor GC对于每个选项的平均执行时间如下:

  • NewRatio=2: 45 ms

  • NewRatio=3: 34 ms

  • NewRatio=4: 30 ms

我们得出的结论是 NewRatio=4 是最好的选择,因为 New 区域大小最小,GC 时间也最短,应用GC选项后,服务器没有Full GC。

以下是jstat –gcutil 在服务的 JVM 启动后几天执行的结果。

S0 S1 E O P YGC YGCT FGC FGCT GCT
8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219复制代码

很多人认为 GC 不经常发生,因为服务器的请求很少。但是,Full GC 没有被执行,而 Minor GC 已经执行了 2,424 次。

示例 2

这个例子是针对服务A的。我们在公司的应用程序性能管理器(APM)中发现JVM有很长一段时间(8秒或更长时间)没有定期运行。我们一直在寻找原因,发现执行Full GC需要很长时间,所以我们决定执行GC调优。

作为GC调优的开始阶段,我们添加了-verbosegc选项,结果如下。

duration_graph_before_gc_tuning.jpeg

图 1:GC 调整前的持续时间图。

上图显示了持续时间,是HPJMeter分析后自动提供的图表之一, X轴 显示了JVM之后的时间开始和 Y轴 显示出了每个GC的响应时间。绿点表示  CMS Full GC 结果,蓝点表示 Parallel Scavenge  Minor GC 结果。

以前说过 CMS GC 将是最快的,但是上面的结果表明,有些情况需要长达 15 秒。**是什么造成了这样的结果?**请记住之前说过的:执行压缩时 CMS 会变慢。另外,使用 –Xms1g 和设置了服务 –Xmx4g 的内存,最大分配的内存为4GB。

接下来将 GC 类型从 CMS GC 更改为 Parallel GC,将内存大小更改为2 GB,然后将 NewRatio 设置为 3。jstat –gcutil几个小时后的结果如下。

S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890复制代码

与 4 GB 的 15 秒相比,Full GC 时间更快,每次 3 秒。然而,3秒仍然没有那么快。所以我创建了以下六个案例。

  • 情况1: -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2

  • 案例2: -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3

  • 案例3: -XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3

  • 案例4: -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2

  • 案例5: -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3

  • 案例6: -XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3

哪一个会最快? 结果表明,内存越小,效果越好。下图显示了案例 6 的持续时间图,其中显示了最高的 GC 改进,最慢的响应时间为 1.7 秒,平均值已更改为 1 秒以内,显示出改进的结果。

duration_graph_after_applying_case_6.png

图 2:应用案例 6 后的持续时间图。

结果,我把服务的所有GC选项都改成了Case 6。但是这个变化导致每天晚上都会引起 OutOfMemoryError,这里很难详细说明原因,但简而言之,批处理数据处理导致 JVM 内存不足,目前相关问题正在处理中。

分析短时间积累的GC日志并将结果应用到所有服务器作为执行GC调优是非常危险的,请记住只有在分析服务操作和 GC 日志时,才能无故障地执行 GC 调优。

我们已经查看了两个 GC 调优示例,以了解 GC 调优是如何执行的。正如我所提到的,示例中设置的 GC 选项可以为具有相同 CPU、操作系统版本和 JDK 版本的服务器与执行相同功能的服务设置相同。但是不要将我所做的选项应用于你正在运行的服务,因为它们可能对你不起作用。

结论

我根据我的经验执行 GC 调优,没有执行堆转储和详细分析内存,精确的内存状态分析可能会得出更好的 GC 调优结果,但是如果该服务被大量使用并且存在大量内存使用模式,则可能建议基于可靠的先前经验进行 GC 调优。

已经通过在一些服务器上设置G1 GC选项来执行性能测试,G1 GC 选项显示出比任何其他 GC 类型更快的结果。在JDK 7稳定后(这并不意味着它不稳定)并且WAS(web application service)针对JDK 7进行了优化,使 G1 GC 的稳定应用可能最终按预期工作,有一天我们可能不需要GC调优。


作者:惜鸟
链接:https://juejin.cn/post/7023690149573705758

文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐