在 VS Code 中使用 GraalVM 和 VisualVM 进行性能和内存分析原创
Jetbrain最近的一项调查显示,大约五分之一的Java开发人员使用了visual vm,这使得它成为生态系统中使用最广泛的性能分析工具。
在最近发布的GraalVM 21.2中,我们改进了VS Code的工具支持,现在VS Code与VisualVM紧密集成。它实际上不仅仅是一个分析器,更确切地说,它是一个集Java监视和故障排除工具于一身的工具。这意味着现在从VS Code中更容易、更舒适地分析Java项目的性能和内存!
本文提供了一个使用Java的GraalVM扩展包开发和分析代码的示例,重点介绍VisualVM集成特性。使用一个非常简单的场景,您将学习如何与您的项目一起启动VisualVM,并立即使用自动生成的设置对其进行配置。您还将看到,一旦发现问题,从VisualVM中的分析结果导航到VS Code编辑器中的源代码是多么容易。
如果您想遵循本文中的步骤,您需要安装必要的工具:
VS Code
如果你还没vs code 那么就去下载一个吧!
所需的扩展
使用Extensions活动安装用于Java扩展的gralvm扩展包。通过这种方式,您将在VS Code中获得Java 8+开发所需的一切,包括一些用于GraalVM和Micronaut的很酷的东西。有关扩展的更多细节,请参阅市场入口。
请禁用或卸载您可能已经安装的用于Java开发的任何其他扩展,以确保您能够一步一步地遵循本文的要求。
额外的软件
你需要在VS Code中安装和设置一个最新的GraalVM版本。切换到Gr活动并单击下载和安装GraalVM按钮,或添加一个现有的GraalVM 21.2或更新版本的安装。当下载一个新的GraalVM实例时,您可以选择您所选择的发行版——可以是针对所有目的免费的Community Edition,也可以是针对评估和开发免费的Enterprise Edition。访问GraalVM网站graalvm.org了解更多关于GraalVM发行版和特性的信息。
一旦下载并安装了GraalVM,确保它被标记为活动的,最终使用该安装的Set active GraalVM Installation操作(“home”图标)使它处于活动状态。这设置了VS Code环境来使用特定的GraalVM(不仅仅如此!)Java开发。
到目前为止还不错,但是VisualVM在哪里呢?这难道不是一切吗?!事实上,你只有一个。VisualVM与每个GraalVM安装捆绑在一起,配置为在其上运行,并测试使用它。不需要从visualvm.github下载独立版本。不过我们还是欢迎你来参观,以便学习一些新的和有用的东西。
创建项目
让我们为实验准备一个示例项目!我们将使用一个基于Micronaut的项目——这是一个易于使用,但功能非常强大的微服务和无服务器框架,它与GraalVM非常配合。您可以在Micronaut .io了解更多关于Micronaut的信息。
生成项目
使用View | command palette…打开命令面板并输入“Micronaut”显示可用的与Micronaut相关的命令。调用Micronaut:创建Micronaut Project命令并提供以下输入:最新稳定的Micronaut版本,将Micronaut Application作为应用程序类型,将活动的GraalVM实例作为项目Java,将FibonacciDemo作为项目名称,com.example作为基础包,Java作为项目语言,没有额外的项目功能,Gradle作为项目构建工具,JUnit作为测试框架,并选择项目父文件夹位置。此时,创建了一个新的Micronaut项目,可以使用了。
或者,您可以使用Micronaut Launch服务生成项目,然后在VS Code中手动提取并打开它。
实现逻辑
现在,让我们向项目添加一些业务逻辑!我们将实现一个简单的斐波那契数生成器,它易于理解,并为我们的实验提供正确的行为。下面简要回顾一下斐波那契数列的知识。
确保_Explorer_ activity显示,并展开src、main和java节点以查看java.com.example包。右键单击示例部分并调用New from Template…操作。选择Java作为模板类型,Java Class作为要使用的模板,并输入FibonacciController作为要创建的类名。最终,一个FibonacciController.java文件会在与项目一起生成的Application.java旁边创建。
在编辑器中打开FibonacciController.java文件,输入以下实现,并保存它:
package com.example;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
@Controller("/")
public class FibonacciController {
private static final StringBuilder LOG = new StringBuilder();
@Get(uri = "/nthFibonacci/{nth}", produces = MediaType.TEXT_PLAIN)
public String nthFibonacci(Integer nth) {
long[] counter = new long[] {0};
long start = System.currentTimeMillis();
LOG.append("Fibonacci number #").append(nth).append(" is ");
LOG.append(computeNthFibonacci(nth, counter));
LOG.append(" (computed in ").append(System.currentTimeMillis() - start).append(" ms, ").append(counter[0]).append(" steps)\n");
return LOG.toString();
}
private static long computeNthFibonacci(int nth, long[] counter) {
counter[0]++;
if (nth == 0 || nth == 1) return nth;
return computeNthFibonacci(nth - 1, counter) + computeNthFibonacci(nth - 2, counter);
}
}
如您所见,代码非常简单。nthFibonacci方法是请求的入口点,使用computeNthFibonacci方法计算实际结果,并将其存储到一个全局缓冲区中,包括计算所需的时间和步骤数。
项目分析
使VS Code中的分析变得非常容易的关键特性是Gr活动视图中的VISUALVM部分。该部分包含要分析的流程句柄,以及控制和调用最有用的VisualVM特性的操作。可以使用process:节点的Select流程操作提前选择要分析的流程,也可以调用任何需要具体流程上下文的VisualVM操作。第三种设置流程句柄的方法是让VisualVM支持在项目启动时自动选择流程。用于分析的VisualVM实例由活动的gralvm安装定义。
设置VisualVM
要使VS Code项目与VisualVM顺利集成,请切换到Run和Debug活动,单击创建启动。json文件链接,选择Java 8+环境,并添加一个特殊的启动配置,名为launch VisualVM & Java 8+ Application using the add configuration…启动按钮。json编辑器。不要忘记保存修改后的文件!运行和调试活动视图被更新,现在显示一个定义活动启动配置的运行和调试选择器。单击它并选择Launch VisualVM & Java 8+ Application配置。
性能分析
现在可以运行和分析项目了!使用“运行|开始调试”或“运行|不进行调试就运行”操作来构建和启动项目。在某个时候,您会注意到VisualVM与项目一起启动,最终显示它的GUI并在Monitor选项卡上打开项目流程。这是默认设置——你可以在Gr | VISUALVM窗格中使用More Actions…菜单。注意,在VisualVM应用程序窗格中,项目过程使用VS Code项目名称FibonacciDemo显示。
此时,Fibonacci数字生成器已经启动并运行,并准备在localhost:8080/nthFibonacci/上接受请求。要计算的第n个斐波那契数的定义是通过将该数附加到生成器地址来完成的。让我们运行这三个请求:
- localhost:8080/nthFibonacci/35
- localhost:8080/nthFibonacci/40
- localhost:8080/nthFibonacci/45
最终你会看到生成的日志如下:
Fibonacci number #35 is 9227465 (computed in 61 ms, 29860703 steps)
Fibonacci number #40 is 102334155 (computed in 573 ms, 331160281 steps)
Fibonacci number #45 is 1134903170 (computed in 6367 ms, 3672623805 steps)
您可能已经注意到计算最后一个斐波那契数花费了大量的时间。同时,VisualVM Monitor显示如下CPU峰值:
这是使用分析器的正确时机!切换到VS Code Gr活动,并在VISUALVM部分展开CPU采样器节点来配置分析会话。单击“Filter:”节点的“Configure”动作,并选择“仅包括项目类作为CPU采样过滤器”。单击“配置采样速率:节点动作”,选择CPU采样速率为20ms。配置完成!
现在调用CPU采样节点的Start CPU采样动作(“Start”/“triangle”图标)。在VisualVM中启动项目进程的CPU采样会话,并在VisualVM中显示项目进程的“Sampler”选项卡。采样器的结果是空的,因为还没有执行项目代码。
让我们使用http://localhost:8080/nthFibonacci/45再次调用第45个斐波那契数的计算,现在采样器开始工作。基于项目类筛选器,它显示包含项目类的所有堆栈跟踪。通过单击列标题,按Total Time (CPU)对结果进行排序,然后右键单击工作线程default-nioEventLoopGroup-X-Y,并在上下文菜单中调用Expand / Collapse | Expand Topmost Path来扩展占用大部分时间的执行路径。正如您所看到的,几乎所有的时间都花在com. example.fibonaccicontrollor . computenthfibonacci()方法中,它似乎在重复调用自己。使用VisualVM GUI中的Sampler选项卡中的Stop按钮或VS Code中的CPU Sampler节点的Stop sampling动作停止采样会话。
此时,最好的方法是检查com. instance . fibonaccicontroller . computenthfibonacci()的源代码。在采样器结果中右键单击该方法,并调用上下文菜单中的Go to Source操作。这将VS Code窗口再次呈现在你的眼前,并在编辑器中打开类源代码,将光标放在方法定义的右边。看一下实现,很明显性能受到了影响,因为使用了效率非常低、时间复杂度呈指数级的递归算法。这段代码真可耻!
有比递归更好的方法来计算斐波那契数。事实上,递归可能是所有可能的方法中最糟糕的一种。但是为了简单起见,让我们尝试通过为已经计算的结果添加一个简单的缓存来修复递归算法的性能。将
FibonacciController.java内容更改为改进的版本并保存文件:
package com.example;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import java.util.HashMap;
import java.util.Map;
@Controller("/")
public class FibonacciController {
private static final StringBuilder LOG = new StringBuilder();
private static final Map<Integer, Long> CACHE = new HashMap();
@Get(uri = "/nthFibonacci/{nth}", produces = MediaType.TEXT_PLAIN)
public String nthFibonacci(Integer nth) {
long[] counter = new long[] {0};
long start = System.currentTimeMillis();
LOG.append("Fibonacci number #").append(nth).append(" is ");
LOG.append(computeNthFibonacci(nth, counter));
LOG.append(" (computed in ").append(System.currentTimeMillis() - start).append(" ms, ").append(counter[0]).append(" steps)\n");
return LOG.toString();
}
private static long computeNthFibonacci(int nth, long[] counter) {
counter[0]++;
if (nth == 0 || nth == 1) return nth;
Long result = CACHE.get(nth);
if (result == null) {
result = computeNthFibonacci(nth - 1, counter) + computeNthFibonacci(nth - 2, counter);
CACHE.put(nth, result);
}
return result;
}
}
这个小小的改变会有助于提高性能吗?让我们再次使用CPU采样器来验证它!
显示VS Code Gr活动,并在VISUALVM部分中单击When started:节点的Configure动作。该节点负责配置在VS Code中使用Launch VisualVM & Java 8+ Application启动配置启动进程时发生的情况。默认值为Open进程;这就是为什么当第一次运行项目时,流程会在VisualVM中自动打开。现在选择Start CPU sampler选项,让采样会话尽快开始,而不需要任何额外的操作:
如果原始项目进程仍在运行,则使用Run | Stop Debugging操作终止它,并使用Run | start Debugging或Run | Run Without Debugging操作再次启动它。注意,这一次VisualVM在Sampler选项卡上打开了新的项目流程,并自动启动了采样会话。你可以使用sampler视图右上角的settings复选框来验证sampler设置——这个过滤器应该由VS Code集成来配置,只包含项目类com.example。*,采样频率为20ms。
再次运行这三个请求以收集可比结果:
- localhost:8080/nthFibonacci/35
- localhost:8080/nthFibonacci/40
- localhost:8080/nthFibonacci/45
这一次,你应该立即得到结果,并看到一个更合理的报告,从生成器类似于:
Fibonacci number #35 is 9227465 (computed in 0 ms, 69 steps)
Fibonacci number #40 is 102334155 (computed in 0 ms, 11 steps)
Fibonacci number #45 is 1134903170 (computed in 0 ms, 11 steps)
同时,CPU采样器没有显示结果。为什么?这是因为采样的性质,它在定义的间隔内周期性地检查实际的堆栈跟踪。如果要包含在结果中的方法的执行速度比采样速度快,采样者就无法看到它们。这实际上是非常有用的一课——Sampler是发现性能瓶颈的好工具,但在优化算法细微差别方面帮助不大。它也不能用于调查调用计数——这就是为什么在应用程序代码中直接实现计算步骤的原因。
虽然仍然不如其他算法有益,但我们已经设法将递归算法的性能提高到可以接受的水平,并使用VisualVM对其进行了验证。但是如果我们通过修复引入了内存泄漏呢?性能和内存消耗通常是一种权衡,所以这种场景是完全可能的。让我们在下一节研究它。
分析内存消耗
确保您已经停止了CPU采样会话,但不要关闭VisualVM,并保持项目进程运行。再次切换到VS Code窗口,并在Gr活动视图的VISUALVM部分中为堆转储节点调用Take堆转储操作。这将生成一个.hprof内存快照,描述在堆上分配的所有类和实例,以及它们之间的引用。VisualVM在一个特殊的堆查看器组件中显示此快照,该组件允许以一种可理解的和性能好的方式浏览和分析其内容——这是一个查找内存泄漏的好工具!
加载堆转储之后,您将看到进程堆的Summary概述。单击Heap Dump工具栏中的Summary按钮,切换到Objects视图,该视图显示类及其各自实例的直方图。在视图底部找到Class Filter并提交com.example。过滤器,因为我们只对我们的类感兴趣。现在您可以看到项目中的几个类:最后两个类是直接实现的,而其他类是由Micronaut框架生成的。展开com.example. fibonaccicontroller类,右键单击单个实例com.example。FibonacciController#1,并在上下文菜单中调用“在新标签中打开”动作,以在一个新视图中打开它:
现在单击Retained列标题以计算保留的大小。保留大小是一个指标,描述具体实例及其引用的对象占用了多少内存——或者更具体地说,如果从堆中删除实例,将释放多少内存。对于大型堆,计算保留大小最初可能需要一些时间,但结果将在后续会话中重用。
计算了保留的大小之后,您就可以分析为改进算法而引入的缓存的内存占用情况了。展开节点以查看实例的内存表示,并找到静态字段CACHE。堆查看器显示它是一个java.util.HashMap实例,包含44个元素,占用大约3KB的堆。看起来绝对值得加速!估计它不会以我们应该担心的方式增长。如果你仍然想要改进一些东西,请记住,你总是可以很容易地在VS Code编辑器中使用go to Source动作返回实现,即使是从堆查看器:
让我们快速查看一下静态字段LOG,它表示结果的全局缓冲区。它实际上并不占用堆上的太多空间,但请注意,在堆查看器中发现它的内容是多么容易!存储文本的预览可直接在节点名中获得。如果您需要查看更多内容,只需单击堆转储工具栏的详细信息部分中的Preview按钮。全文显示在一个常规文本组件中,可以很容易地阅读、复制,甚至保存到文件中。这对于基于其值/内容搜索和识别具体实例非常有用:
至此,我们可以得出结论,我们成功地使用递归算法实现了斐波那契数生成器,并基于VisualVM的见解对其进行了改进,使其执行速度相当快,并从内存管理的角度验证了它的行为是否正确。
其他功能
到目前为止,我们只使用了vscode中可用的VisualVM特性的一部分:CPU采样器和堆转储。实际上,我们还启用了与项目一起启动VisualVM,配置了项目启动时的动作,并使用了从VisualVM到VS Code编辑器的Go to Source回调。没有提到的其他功能是什么?
- 与 Heap dump 类似,线程转储可以从VS Code中调用并显示在VisualVM中。
- 与CPU采样器类似,内存采样器会话可以在VS Code中配置和控制。
- VS Code现在也允许启动和停止项目进程的JFR会话,并在VisualVM中转储和显示收集的事件。
要了解VS Code和VisualVM集成特性的全图,请参阅用于Java扩展的gralvm工具以及VisualVM和VS Code集成文档。
在这篇文章中,我们展示了如何使用VS Code中的最新改进来让你的代码执行得更好,让你的工作更舒适,从而交付更好的软件。
还有更多关于VS Code工具的内容:语言服务器、调试、测试支持等等。了解Visual Studio Code Extensions文档中的所有细节。
所有这些开发都是由Oracle实验室领导的GraalVM工作的一部分。如果您对关于GraalVM的更酷的东西感兴趣——比如用本机映像将Java应用程序编译成二进制,在Java应用程序中运行脚本语言,或多语言编程——请查看本博客中的GraalVM文档和其他文章。
我们应该强调,本文中描述的实际分析和概要分析是由VisualVM提供的,VisualVM是最著名的Java工具之一。VisualVM与您的gralvm发行版绑定在一起,因此您可以开箱即用。您还可以在VisualVM项目页面上了解更多信息。