JVM系列之:GC调优基础以及初识jstat命令

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: JVM系列之:GC调优基础以及初识jstat命令

1.jpg

本文为《深入学习 JVM 系列》第二十二篇文章


影响垃圾收集性能有三个主要的属性,垃圾收集调优又有三个基本原则,以及垃圾收集调优时需要采集的信息。如果想要对垃圾收集进行调优,则需要根据实际场景对不同属性做出取舍,理解调优的原则以及收集什么信息。


性能属性

吞吐量


吞吐量是评价垃圾收集器能力的重要指标之一,指不考虑垃圾收集引起的停顿时间或内存消耗,每单位时间可以执行的工作量的指标。通常,吞吐量的增加是以延迟增加和/或内存占用增加为代价的。


一般吞吐量需求(Generic requirements for throughput) 类似这样:


  • 每天必须处理 100万个订单;
  • 每秒应执行 2500 个事务


可以看出,吞吐量需求不是针对单个操作的,,而是在给定的时间内,系统必须完成多少个操作。


延迟


延迟是衡量应用程序接收到执行某些工作的激励与该工作完成之间经过的时间。延迟要求忽略吞吐量。通常,提高响应能力或降低延迟是以降低吞吐量和/或增加内存占用为代价的。


延迟指标通常如下所述:


  • 应用程序应在 60 毫秒内执行交易请求以完成
  • 90%的订单付款操作必须在3秒以内处理完成


这类性能指标要确保程序在工作过程中,GC 暂停不能暂用太长时间,否则就满足不了指标。


内存占用


内存占用量是在某种吞吐量、某种延迟和/或某种可用性和可管理性水平上运行应用程序所需的内存量的度量。内存占用通常表示为运行应用程序所需的 Java 堆数量和/或运行应用程序所需的内存总量。通常,通过增加 Java 堆大小来增加内存占用可以提高吞吐量或减少延迟,或两者兼而有之。随着应用程序可用内存的减少,通常会牺牲吞吐量或延迟。


例如系统部署到小于1G 内存的服务器上,因为计算资源不可随意挥霍,所以在满足延迟和吞吐量需求的基础上必须要满足内存占用。


原则


  • Minor GC 回收原则。每次 Minor GC 都尽可能多地收集垃圾对象,减少 Full GC 的频率。Full GC 持续时间总是最长的,会影响吞吐量和延迟的性能。


  • GC 内存最大化原则。在预算以内,堆内存越大越好,吞吐量和延迟性能都会越好。这也意味着新生代可以适当调整大小,以更好应对短寿命对象的创建率,减少提升到老年代的分配数量。


  • 三选二原则。在上述三个性能属性(吞吐量、延迟、内存占用)中任意选择两个进行 JVM 垃圾收集调优。


关于 GC 调优需要收集什么信息,以及怎么查看,我们将先通过命令行查看 GC 日志,后续通过可视化工具来获取关键信息。


GC日志分析


我们需要通过 JVM 参数(默认为 JDK8)预先设置 GC 日志,通常有以下几种 JVM 参数设置:


-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-XX:+PrintReferenceGC  输出的内容包含系统的软引用、弱引用、虚引用和Finallize队列
-Xloggc:../logs/gc.log 日志文件的输出路径
复制代码


这里使用如下参数来打印日志,基于 JDK8,默认使用 Parallel Scavenge 收集器和 Parallel Old 收集器。


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Goods {
  private String name;
  private Double price;
  private String[] types;
}
//-Xms60M -Xmx60M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
public class OutOfMemoryTest {
  public static void main(String[] args) throws InterruptedException {
    Map<String, Goods> map = new HashMap<>();
    int counter = 1;
    while (true) {
      Thread.sleep(10);
      Goods goods = new Goods();
      String[] types = new String[counter];
      for (int i = 0; i < types.length; i++) {
        types[i] = "type" + i;
      }
      goods.setName("hresh" + counter);
      goods.setPrice(Double.valueOf(counter));
      goods.setTypes(types);
      map.put(goods.getName(), goods);
//      if (counter % 100 == 0) {
//        System.out.println("put" + counter);
//      }
      counter++;
    }
  }
}
复制代码


截取 Full GC 的部分日志内容如下:


16.619: [Full GC (Ergonomics) [PSYoungGen: 15359K->15359K(17920K)] [ParOldGen: 40924K->40924K(40960K)] 56284K->56284K(58880K), [Metaspace: 3768K->3768K(1056768K)], 0.0320617 secs] [Times: user=0.31 sys=0.00, real=0.03 secs] 
16.651: [Full GC (Ergonomics) [PSYoungGen: 15359K->15359K(17920K)] [ParOldGen: 40926K->40926K(40960K)] 56286K->56286K(58880K), [Metaspace: 3768K->3768K(1056768K)], 0.0337364 secs] [Times: user=0.31 sys=0.00, real=0.03 secs] 
16.685: [Full GC (Ergonomics) [PSYoungGen: 15359K->15359K(17920K)] [ParOldGen: 40927K->40927K(40960K)] 56287K->56287K(58880K), [Metaspace: 3768K->3768K(1056768K)], 0.0322237 secs] [Times: user=0.30 sys=0.00, real=0.03 secs] 
16.717: [Full GC (Ergonomics) [PSYoungGen: 15359K->15359K(17920K)] [ParOldGen: 40929K->40929K(40960K)] 56289K->56289K(58880K), [Metaspace: 3768K->3768K(1056768K)], 0.0314023 secs] [Times: user=0.30 sys=0.01, real=0.04 secs] 
16.749: [Full GC (Ergonomics) [PSYoungGen: 15359K->15359K(17920K)] [ParOldGen: 40931K->40931K(40960K)] 56291K->56291K(58880K), [Metaspace: 3768K->3768K(1056768K)], 0.0327116 secs] [Times: user=0.31 sys=0.00, real=0.03 secs] 
复制代码


关于 GC 日志,Minor GC 和 Full GC 的含义可以参考本文


分析上述输出结果,可以得知:


  • 从 16.619 到 16.749 发生了 4次 Full GC,大概耗费了 130 ms,有4次 GC 暂停;
  • 比如说 16.749 这一刻的 GC,花费总时长为 310 ms,暂停时间为 0.0327116 s,不过因为是多核 CPU 机器,而且使用的是并行 GC,所以暂停时间大约为 30ms;
  • 这四次 GC 暂停总时长大约为 130ms,这里存在疑惑,按理说暂停时长应该小于运行时长,个人猜测可能是 real 的值不够那么精确;
  • 在 GC 完成后,几乎所有的老年代空间(40960K)依然被占用(40931K)。


通过上述日志可以发现老年代空间几乎被占满了,而且 GC 之后也没有释放,那么过不了多久,就会抛出 OOM 异常。


老年代内存占用经过 GC 后没有改善,仍然接近全满,那么会造成性能瓶颈,可能是内存太小,又或者存在内存泄漏。除了可以通过观察老年代的内存使用情况,还可以观察每次 GC 的暂停时间(即我们之前学习的 STW,在日志中可以参考 real所表示的时间),如果暂停总时长占比特别高,那么会损坏系统的吞吐量。如果个别 GC 暂停时间过长,则会影响系统的延迟指标。


除了在程序启动时配置相关参数,还可以在程序运行时使用 JDK 自带的命令来查看 GC 信息。


jstat命令


jstat 命令(帮助文档)可用来打印目标 Java 进程的性能数据。它包括多条子命令,如下所示:


% jstat -options
-class            //打印类加载相关的数据
-compiler         //打印即时编译相关的数据
// 以gc开头的子命令,打印垃圾回收相关的数据
-gc               //显示和gc相关的堆信息
-gccapacity       //显示各个代的容量以及使用情况
-gccause          //显示垃圾回收的相关信息(通-gcutil),同时显示最后一次或当前正在发生的垃圾回收的诱因
-gcmetacapacity   //显示metaspace的大小
-gcnew            // 显示新生代信息
-gcnewcapacity    //显示新生代大小和使用情况
-gcold            //显示老年代和永久代的信息
-gcoldcapacity    //显示老年代的大小
-gcutil           //显示垃圾收集信息
-printcompilation //打印即时编译相关的数据
复制代码


默认情况下,jstat 只会打印一次性能数据。


// JDK8
% jstat -gc 67537
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
2560.0 2560.0  0.0   1223.1 15360.0   3956.4   40960.0      8.0     4864.0 3055.9 512.0  322.2       1    0.001   0      0.000    0.001
复制代码


实际应用中,我们肯定是想观察某个进程一段时间内性能数据,这时可以这样做:将它配置为每隔一段时间打印一次,直至目标 Java 进程终止,或者达到我们所配置的最大打印次数。具体示例如下所示:


//JDK8
% jstat -gc 67558 1s 4
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
2560.0 2560.0  0.0   1255.1 15360.0   3956.4   40960.0      8.0     4864.0 3055.9 512.0  322.2       1    0.004   0      0.000    0.004
2560.0 2560.0  0.0   1255.1 15360.0   3956.4   40960.0      8.0     4864.0 3055.9 512.0  322.2       1    0.004   0      0.000    0.004
2560.0 2560.0  0.0   1255.1 15360.0   3956.4   40960.0      8.0     4864.0 3055.9 512.0  322.2       1    0.004   0      0.000    0.004
2560.0 2560.0  0.0   1255.1 15360.0   3956.4   40960.0      8.0     4864.0 3055.9 512.0  322.2       1    0.004   0      0.000    0.004
复制代码


上述命令格式如下:


jstat -outputOptions [-t] [-hlines] VMID [interval [count]]
复制代码


  • outputOptions:指的是上文提到的子命令,一个或多个输出选项;
  • t:将时间戳列显示为输出的第一列。时间戳是自目标 JVM 启动时间以来的时间。
  • lines:每个样本(输出行)显示一个列标题,其中n是一个正整数。默认值为0,即显示第一行数据的列标题。
  • VMID:虚拟机标识符,它是指示目标 JVM 的字符串。请参阅虚拟机标识符。当监控本地环境的 Java 进程时,VMID 可以简单理解为 PID。
  • interval:以指定单位、秒 (s) 或毫秒 (ms) 为单位的采样间隔。默认单位是毫秒。这必须是一个正整数。
  • count:用于指定输出多少次记录,缺省则会一直打印


在-gc子命令的输出中,前四列分别为两个 Survivor 区的容量(Capacity)和已使用量(Utility)。我们可以看到,这两个 Survivor 区的容量相等,而且始终有一个 Survivor 区的内存使用量为 0,关于这点在讲解 JVM 垃圾回收时有说过原因。


关于 jstat 命令的输出结果,想要知道每列代表什么含义,以及怎么计算的,其实在 JDK lib 包下的 tools.jar 里存在一个文件叫做 jstat_options,这个文件里定义了上面的每种类型的输出结果。比如说 gc。


2.jpg


如上图所示,我们可以在 IDEA 中打开该文件。这里只截取部分内容:


3.jpg


如果垃圾收集器使用的是 G1 收集器,输出结果则变为:


//JDK9
% jstat -gc 31678 1s 4
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
 0.0   3072.0  0.0   3072.0 161792.0  2048.0   97280.0     9290.0   5376.0 5084.5 512.0  433.9       1    0.004   0      0.000    0.004
 0.0   3072.0  0.0   3072.0 161792.0  2048.0   97280.0     9290.0   5376.0 5084.5 512.0  433.9       1    0.004   0      0.000    0.004
 0.0   3072.0  0.0   3072.0 161792.0  2048.0   97280.0     9290.0   5376.0 5084.5 512.0  433.9       1    0.004   0      0.000    0.004
 0.0   3072.0  0.0   3072.0 161792.0  2048.0   97280.0     9290.0   5376.0 5084.5 512.0  433.9       1    0.004   0      0.000    0.004
复制代码


可以看出,S0C 和 S0U 始终为 0,即其中一个 Survivor 容量和使用量始终为 0 。


这是因为,当使用 G1 GC 时,Java 虚拟机不再设置 Eden 区、Survivor 区,老年代区的内存边界,而是将堆划分为若干个等长内存区域(Region)。


每一个 Region 都可以根据需要,扮演新生代的 Eden 空间,Survivor 空间,或者老年代空间,并且可以在不同区域类型之间来回切换。


换句话说,逻辑上我们只有一个 Survivor 区。当需要迁移 Survivor 区中的数据时(即 Copying GC),我们只需另外申请一个或多个内存区域,作为新的 Survivor 区。


因此,Java 虚拟机决定在使用 G1 GC 时,将所有 Survivor 内存区域的总容量以及已使用量存放至 S1C 和 S1U 中,而 S0C 和 S0U 则被设置为 0。


那么实际应用中,我们使用 jstat 命令如何来分析 Java 进程的性能呢?


我们来下面这样一段性能输出结果:


% jstat -gc 33678 1s 9
Timestamp  S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
200.0       8448.0 8448.0 8448.0  0.0   67712.0  67712.0   169344.0   169344.0  21248.0 20534.3 3072.0 2807.7     34    0.720  658   133.684  134.404
201.0      8448.0 8448.0 8448.0  0.0   67712.0  67712.0   169344.0   169343.2  21248.0 20534.3 3072.0 2807.7     34    0.720  662   134.712  135.432
202.0      8448.0 8448.0 8102.5  0.0   67712.0  67598.5   169344.0   169343.6  21248.0 20534.3 3072.0 2807.7     34    0.720  667   135.840  136.559
203.0      8448.0 8448.0 8126.3  0.0   67712.0  67702.2   169344.0   169343.6  21248.0 20547.2 3072.0 2807.7     34    0.720  669   136.178  136.898
204.0      8448.0 8448.0 8126.3  0.0   67712.0  67702.2   169344.0   169343.6  21248.0 20547.2 3072.0 2807.7     34    0.720  669   136.178  136.898
205.0      8448.0 8448.0 8134.6  0.0   67712.0  67712.0   169344.0   169343.5  21248.0 20547.2 3072.0 2807.7     34    0.720  671   136.234  136.954
206.0      8448.0 8448.0 8134.6  0.0   67712.0  67712.0   169344.0   169343.5  21248.0 20547.2 3072.0 2807.7     34    0.720  671   136.234  136.954
207.0      8448.0 8448.0 8154.8  0.0   67712.0  67712.0   169344.0   169343.5  21248.0 20547.2 3072.0 2807.7     34    0.720  673   136.289  137.009
208.0      8448.0 8448.0 8154.8  0.0   67712.0  67712.0   169344.0   169343.5  21248.0 20547.2 3072.0 2807.7     34    0.720  673   136.289  137.009
复制代码


我们先查看第一行的数据:


  • jstat 连接到 JVM 的时间, 是JVM启动后的 200秒。此信息从第一行的 “Timestamp” 列得知。


  • 从第一行的 “YGC” 列得知年轻代共执行了34次GC, 由 “FGC” 列得知整个堆内存已经执行了 658次 full GC。


  • 年轻代的GC耗时总共为 0.720 秒, 显示在“YGCT” 这一列。


  • Full GC 的总计耗时为 133.684 秒, 由“FGCT”列得知。 这立马就吸引了我们的目光, 总的 JVM 运行时间只有 200 秒, 但其中有 66% 的部分被 Full GC 消耗了


再看第二行的数据:


  • 在接下来的一秒内共执行了 4 次 Full GC。参见 “FGC“ 列。


  • 这4次 Full GC 暂停占用了差不多 1秒的时间(根据 FGCT列的差得知)。与第一行相比,Full GC 耗费了928 毫秒,总时长为 1s, 那么 Full GC 则占用了 92.8% 的时间。


  • 根据 “OC 和 “OU” 列得知, 整个老年代的空间169,344.0 KB (“OC“), 在 4 次 Full GC 后依然占用了 169,344.2 KB (“OU“)。用了 928ms 的时间却只释放了 800 字节的内存, 怎么看都觉得很不正常。


通过前两行数据可以发现程序存在严重的问题,后续的数据可以确定该问题依然存在。

通过 jstat 的输出能够快速发现以下问题:


  • 可以比较 Java 进程的启动时间以及总 GC 时间(GCT 列),或者两次测量的间隔时间以及总 GC 时间的增量,来得出 GC 时间占运行时间的比例。就暴露出 GC 开销过大的问题. 不同系统对 GC 开销有不同的容忍度, 由性能需求决定, 一般来讲, 超过 10% 的 GC 开销都是有问题的。


  • YGC” 和 “FGC” 列的快速变化往往也是有问题的征兆。频繁的 GC 暂停会累积,并导致更多的线程停顿(stop-the-world pauses),进而影响吞吐量。


  • 如果看到 “OU” 列中,老年代的使用量约等于老年代的最大容量(OC), 并且不降低的话, 就表示虽然执行了老年代 GC, 但基本上属于无效GC。


后续我们会继续学习日志分析工具,可以更直观的观测 GC 状态,下篇文章我们不见不散。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2天前
|
算法 网络协议 Java
【JVM】——GC垃圾回收机制(图解通俗易懂)
GC垃圾回收,标识出垃圾(计数机制、可达性分析)内存释放机制(标记清除、复制算法、标记整理、分代回收)
|
22天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
29天前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
32 1
|
1月前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
1月前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
55 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
1月前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
1月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
291 1
|
2天前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型