前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >比较 VisualVM、JMC 和异步分析器

比较 VisualVM、JMC 和异步分析器

原创
作者头像
IT千锋教育
发布2023-05-31 17:23:38
5120
发布2023-05-31 17:23:38
举报
文章被收录于专栏:学习Java专栏学习Java专栏

关键要点

  • 分析程序的性能很重要:您是否了解用于分析的开源工具
  • 有两种主要类型的分析器:采样分析器和检测分析器;了解它们的差异将帮助您选择合适的类型
  • 三种主要的开源分析器各有优缺点:一个简单的分析器 (VisualVM),一个具有很多特性的可破解分析器 (async-profiler),以及一个获取大量附加信息的内置分析器 (JMC)
  • 所有这些分析器都是对结果进行近似的采样分析器,这使得它们比其他分析器更快且干扰更少,但也需要 Java 运行时的支持
  • 使用分析器并非没有风险,有时可能会导致性能下降和罕见的崩溃

探查器的目的是获取有关程序执行的信息,以便开发人员可以了解某个方法在给定时间段内执行了多少时间。

但是分析器是如何做到这一点的呢?有两种获取配置文件的方法:检测程序和采样。

检测分析器

获取配置文件的一种方法是记录开发人员感兴趣的每个方法的进入和退出。

当许多开发人员想知道他们程序的特定部分花费了多长时间时,他们已经在做这种检测。

所以下面的方法:

代码语言:javascript
复制
void methodA() {
      // … // do the work
}

修改记录相关信息:

代码语言:javascript
复制
void methodA() {
      long start = System.currentTimeMillis();
      // … // do the work
      long duration = System.currentTimeMillis() - start;
      System.out.println(“methodA took “ + duration + “ms”);
}

这种修改对于基本时间测量是可能的。尽管如此,它在嵌套测量方法时提供的信息很少,因为了解方法之间的关系也很有趣,例如methodB()was executed in seconds by methodA()。因此,我们需要将每次进入和退出记录到相关方法中。这些日志与时间戳和当前线程相关联。

检测分析器的想法是自动执行此代码修改:它将对logEntry()logExit()方法的调用插入到方法的字节码中。这些方法是探查器运行时库的一部分。这种插入通常在运行时完成,当加载类时,使用检测代理。然后分析器将我们的methodA()from before 修改为:

代码语言:javascript
复制
void methodA() {
      logEntry(“methodA”);
      // … // do the work
      logExit(“methodA”);
}

Instrumenting 分析器的优势在于它们可以与所有 JVM 一起工作,因为它们可以用纯 Java 实现。但它们的缺点是插入的方法调用会导致显着的性能损失并严重扭曲结果。因此,近几十年来,纯仪器分析器的流行度已经消退。现在的现代分析器大多是采样分析器。

采样刻画器

另一种类型的分析器是采样分析器,它从分析程序的执行中获取样本。这些分析器定期向 JVM 询问当前正在运行的程序的堆栈,通常是每 10 毫秒到 20 毫秒。然后分析器可以使用此信息来近似配置文件。但这给我们带来了主要的缺点:更短的运行方法可能从配置文件中看不到。

抽样分析器的主要优点是它们以低开销分析未修改的程序,而不会显着扭曲结果。

现代采样分析器通常通过每 10 到 20 毫秒循环运行以下命令来工作:

采样分析器获取每次迭代的当前可用 (Java) 线程列表。然后它选择一个随机的线程子集进行采样。这个子集的大小通常在 5 到 8 之间,因为在每次迭代中采样太多线程会增加运行分析器的性能影响。在分析具有大量线程的应用程序时,请注意这一事实。

然后分析器向每个选定的线程发送一个信号给每个线程,这导致它们停止并分别调用一个信号处理程序。此信号处理程序获取并存储其线程的堆栈跟踪。在每次迭代结束时收集所有堆栈跟踪并进行后处理。

还有其他方法可以实现采样分析器,但我向您展示了使用最广泛、精度最高的技术。

不同的开源分析器

目前存在三个著名的开源分析器:VisualVM、async-profiler 和 JDK Flight Recorder (JFR)。这些分析器正在积极开发中,可用于各种应用程序。它们都是采样分析器。VisualVM 是唯一还支持仪器分析的分析器。

我们可以区分“外部”和“内置”分析器:外部分析器不直接实现到 JVM 中,而是使用 API 来收集特定线程的堆栈跟踪。仅使用 API 的分析器可以针对具有相同分析器版本的不同 JVM 版本和供应商(如 OpenJDK 和 OpenJ9)。

两个最著名的外部分析器是 VisualVM 和 async-profiler;他们的主要区别元素是他们使用的 API。VisualVM 使用官方的Java 管理扩展(JMX) 来获取线程的堆栈跟踪。另一方面,Async-profiler 使用非官方的 AsyncGetCallTrace API。两者各有利弊,但 JMX 和相关 API 通常被认为更安全,AsyncGetCallTrace 更精确。

OpenJDK 和 GraalVM 的唯一内置分析器是 Java Flight Recorder (JFR);它的工作原理与 async-profiler 大致相同,同样精确但稍微更稳定。

我将在下一节介绍不同的分析器及其历史。

虚拟机

此工具是 Netbeans 分析器的独立版本。从 2006 年的 Oracle JDK 6 到 JDK 8,每个 JDK 都包含 Java VisualVM 工具,该工具于 2008 年开源。此分析器后来更名为 VisualVM,而 Oracle 并未将其包含在 JDK 9 中。根据最近的 JetBrains调查, VisualVM 是最常用的开源分析器。

它的使用非常简单;只需在 GUI 中选择运行您要分析的程序的 JVM 并触发分析:

然后,您可以直接在简单的树可视化中查看配置文件。还可以使用以下命令从命令行启动和停止样本分析器:

代码语言:javascript
复制
visualvm --start-cpu-sampler <pid>
visualvm --stop-sampler <pid>

VisualVM 是一个分析器,具有易于使用的简单 UI,但要注意不要使用特定的 JVM API。

异步分析器

最常用的分析器之一是 async-profiler,尤其是因为它嵌入到许多其他工具中,例如 IntelliJ Ultimate Profiler 和 AppIication Performance Monitors。您可以从其项目的 GitHub 页面下载 async-profiler 。它在 Windows 上不受支持,由特定于平台的二进制文件组成。我创建了ap-loader 项目,它将所有 async-profiler 二进制文件包装在一个多平台二进制文件中,使嵌入和使用分析器更加容易。

您可以通过使用嵌入它的许多工具或直接将其用作本机 Java 代理来使用 async-profiler。假设您下载了特定于平台的 libasyncProfiler.so,您可以通过将以下选项添加到 Java 二进制文件的调用来分析您的 Java 应用程序:

代码语言:javascript
复制
java 
-agentpath:libasyncProfiler.so=start,event=cpu,file=flame.html,flamegraph …

此调用将告诉异步分析器生成火焰图,这是一种流行的可视化。 您还可以使用它创建 JFR 文件:

代码语言:javascript
复制
java 
-agentpath:libasyncProfiler.so=start,event=cpu,file=profile.jfr,jfr …

此调用允许您在众多查看器中查看配置文件。

对于现在好奇的异步分析器的一些历史:

2002 年 11 月,Sun(后来被 Oracle 收购)根据JVM(TM) 工具接口规范向 JDK 添加了 AsyncGetStackTrace API 。新的 API 使得从外部分析器获取精确的堆栈跟踪成为可能。Sun 引入了此 API 以将完整的 Java 分析器添加到他们的 Sun Development Studio。两个月后,他们出于不为人知的原因删除了该 API。但是该 API 作为 AsyncGetCallTrace 保留在 JDK 中,直到今天仍然存在,只是没有导出,因此更难使用。

几年后,人们偶然发现这个 API 是实现分析器的好方法。第一次公开提及 AsyncGetCallTrace 作为 Java 分析器的基础是 Jeremy Manson 在他 2007 年的博客文章中,标题为Profiling with JVMTI/JVMPI, SIGPROF and AsyncGetCallTrace。从那时起,许多开源和闭源分析器开始使用它。值得注意的例子是YourKitJProfilerhonest-profiler。async-profiler 的开发始于 2016 年;它是目前使用 AsyncGetCallTrace 的主要开源分析器。

async-profiler 的问题在于它基于非官方的内部 API。此 API 未在官方 OpenJDK 测试套件中经过充分测试,随时可能崩溃。尽管 API 的广泛使用导致准标准化,但这仍然存在风险。为了减轻这些风险,我目前正在研究 JDK 增强提案,该提案将官方 AsyncGetCallTrace 版本添加到 OpenJDK;

async-profiler 的优点是它的许多特性(如堆采样)、可嵌入性、对其他 JVM(如 OpenJ9)的支持,以及它的小代码库,使其易于适应。

JDK 飞行记录器 (JFR)

JRockit 最初开发供内部使用的运行时分析器,但它也越来越受到应用程序开发人员的欢迎。后来在 Oracle 收购了这家开发公司后,这些特性被集成到 Oracle JDK 中。Oracle 最终使用 JDK11 开源了该工具,从那时起,OpenJDK 的 JVM 时间间隔分析工具就没有得到其他 JVM(如 OpenJ9)的支持。

它的工作方式与 async-profiler 相当,主要区别在于它直接使用内部 JVM API。通过将以下选项添加到对 Java 二进制文件的调用中,探查器易于使用:

代码语言:javascript
复制
$ java \
  -XX:+UnlockDiagnosticVMOptions \
  -XX:+DebugNonSafepoints \  # improves precision
  -XX:+FlightRecorder \
  -XX:StartFlightRecording=filename=file.jfr \
  arguments

或者使用 JDK 命令工具启动和停止它jcmd

代码语言:javascript
复制
$ jcmd PID JFR.start
$ jcmd PID JFR.dump filename=file.jfr
$ jcmd PID JFR.stop

JFR 捕获许多分析事件,从采样堆栈跟踪到垃圾收集和类加载统计信息。查看JFR Events网站以获取所有活动的列表。甚至可以添加自定义事件

JFR 相对于 async-profiler 的主要优势在于它包含在所有平台上的 OpenJDK 中,甚至在 Windows 上也是如此。JFR 也被认为稍微更稳定并且记录了更多的事件和信息。JFR 有一个 GUI,称为 JDK Mission Control,它允许您分析 JVM 并查看生成的 JFR 配置文件。

正确性和稳定性

在使用像我介绍过的分析器时请牢记以下几点:它们本身只是软件,与相当大的项目 OpenJDK(或 OpenJ9,就此而言)交织在一起,因此会遇到与他们用来剖析应​​用的典型问题:

  • 测试可以更丰富,尤其是底层API,可以测试得更好;目前只有一个测试。(我在做这个工作)
  • 测试可能会更好:现有测试甚至没有完全测试 API 是否适用于小样本。它只是检查了顶部框架,但没有发现返回的轨迹太短。我发现了这个问题并修复了测试用例。
  • 缺乏自动回归测试:缺乏测试也意味着封闭项目中看似无关部分的变化可能会在没有人注意到的情况下对分析产生不利影响。

结论

用于 Java 的现代基于采样的分析器使得使用开源工具调查性能问题成为可能。您可以选择:

  • 一个略微不精确但易于使用的工具,具有简单的 UI (VisualVM)
  • 包含 GC 等信息的内置工具 (JFR)
  • 一个有很多选项的工具,可以显示 C/C++ 代码的信息(async-profiler)

更多精彩Java精彩内容欢迎B站搜索“千锋教育”或者扫码领取Java 学习全套资料

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 检测分析器
  • 采样刻画器
  • 不同的开源分析器
    • 虚拟机
      • 异步分析器
        • JDK 飞行记录器 (JFR)
          • 正确性和稳定性
          • 结论
          • 更多精彩Java精彩内容欢迎B站搜索“千锋教育”或者扫码领取Java 学习全套资料
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档