龙空技术网

Java 故障诊断 - 查找应用程序执行中的内存相关问题

粥屋与包屋 107

前言:

现在看官们对“java中内存执行程序的原理是什么”大约比较关注,我们都想要剖析一些“java中内存执行程序的原理是什么”的相关资讯。那么小编同时在网摘上搜集了一些有关“java中内存执行程序的原理是什么””的相关文章,希望兄弟们能喜欢,看官们快快来学习一下吧!

每个应用程序都要处理数据,为此,应用程序需要在处理数据时将数据存储在某处。 应用程序分配部分系统内存来处理数据,但内存不是无限资源。 在一个系统上运行的所有应用程序共享系统提供的有限的内存空间。 如果一个应用程序没有明智地管理它分配的内存,它可能会耗尽内存,导致它无法继续工作。 即使应用程序没有耗尽内存,使用过多也会使应用程序变慢,因此错误的内存分配可能会导致性能问题。

如果不优化数据在内存中的分配,应用程序可能会运行得更慢。 如果应用程序需要比系统提供的更多的内存,应用程序将停止工作并抛出错误。 因此,糟糕的内存管理的副作用是执行缓慢,甚至整个应用程序崩溃。 我们在编写应用程序功能时,充分利用其分配的内存是至关重要的。

如果应用程序没有以优化的方式分配它处理的数据,它可能会迫使GC运行得更频繁,因此应用程序将变得更占用 CPU。

应用程序应该尽可能高效地管理其资源。 当我们讨论应用程序的资源时,我们主要考虑CPU(处理能力)和内存。 前面的几篇文章中,我们讨论了如何研究CPU消耗问题。 本文我们将专注于识别与应用程序在内存中分配数据有关的问题。

本文首先讨论内存使用统计的执行采样和性能分析。 您将了解如何识别应用程序是否有内存使用问题,以及如何查找应用程序的哪个部分导致的问题。

然后,我们将讨论如何获得已分配内存的完整 dump(即 heap dump),以分析其内容。 在某些情况下,当应用程序完全因为错误的内存管理而崩溃时,你无法分析执行情况。 但是,在问题出现时获取并分析应用程序分配的内存的内容可以帮助您识别问题的根本原因。

内存问题的采样和分析

我们将使用一个小型应用程序来模拟一个错误实现的功能,该功能使用了太多已分配的内存。 我们使用这个应用程序来讨论调查技术, 您可以使用这些技术来识别内存分配问题或代码中可以优化以更有效地使用系统内存。

假设您有一个真实的应用程序,并且您注意到某些功能运行得很慢。 你使用我们在第6章中讨论的技术来分析资源消耗,发现虽然应用程序不经常“工作”(消耗CPU资源),但它使用了大量内存。 当应用程序使用太多内存时,JVM 会触发垃圾收集器(GC),这也会进一步消耗CPU资源。

请记住,GC 是一种自动从内存释放不需要的数据的机制(请参阅附录E,以复习相关内容)。

请看图 1。在《讨Java 故障诊断 - 使用剖析技术识别资源消耗问题》我们论如何分析资源消耗时,我们使用了 VisualVM 的 Monitor 选项卡来观察应用程序使用了哪些资源。 您可以使用此选项卡中的内存 widget 来查找应用程序何时使用大量内存。

本章使用的应用程序位于 项目 da-ch11-ex1 中。 这个小型 web 应用程序公开了一个端点。当调用这个端点时,我们提供一个数字,这个端点会创建很多对象实例。 基本上,我们发起一个请求来创建100万个对象(对于我们的实验来说,这是一个足够大的数字), 然后看看 profiler 告诉我们关于此请求执行的信息。 这个端点的执行模拟了在实际情况下, 当一个给定的应用程序能力消耗了应用程序的大量内存资源时发生的情况(如图 2 所示)。

图 2

要启动项目,请遵循以下步骤:

启动项目 da-ch11-ex1.启动 VisualVM.在 VisualVM 中为项目 da-ch11-ex1选择一个进程。进入 VisualVM 的 Monitor 选项卡。调用 /products/1000000 端点.在VisualVM的 Memory 选项卡中观察内存 widget。

在 内存widget 的 Monitor 选项卡中,您可以看到应用程序使用了大量内存资源。 这个 widget 看起来类似于图 1。 当我们怀疑某些应用程序功能没有优化使用内存资源时,我们应该怎么做?调查过程主要有两个步骤:

使用内存采样来获取应用程序存储中对象实例的详细信息。使用内存剖析(instrumentation)来获取执行中的特定代码部分的额外细节。

我们使用前面文章中介绍的 CPU 资源消耗方法:从宏观上看,使用采样会发生什么。 要对应用程序执行的内存使用情况进行采样,请选择 VisualVM 中的 Sampler 选项卡。 然后选择 Memory 按钮开始内存使用采样会话。调用端点并等待执行结束。 VisualVM 屏幕将显示应用程序分配的对象。

我们要找的是什么占据了大部分内存。在大多数情况下,这将是以下两种情况之一:

创建了许多特定类型的对象实例并填充内存(这就是在我们的场景中发生的情况)。某种类型的实例不多,但每个实例都非常大。

大量实例填满分配的内存是有道理的,但少量实例怎么可能做到这一点呢?想象一下这个场景:你的应用程序处理大型视频文件。 应用程序一次可能加载两个或三个文件,但由于它们很大,它们会填满分配的内存。 开发人员可以分析是否可以优化功能。 也许应用程序不需要将整个文件加载到内存中,而只需要每次加载其中的一部分。

当我们开始调查时,我们不知道我们会陷入哪种情况。 我通常按占用内存的数量降序排序,然后再按实例的数量排序。 注意在图 3 中,VisualVM 显示了每种采样类型的内存消耗和实例数量。 用户需要对表中的第二列和第三列按降序排序。

在图 3 中,你可以清楚地看到我按活动字节(占用的空间)降序对表进行了排序。 然后,我们可以在我们的应用程序代码库中查找表格中出现的第一个类型。 不要寻找基本类型、字符串、基本类型数组或字符串数组。 这些通常在顶部,因为它们是作为副作用创建的。 然而,在大多数情况下,它们不会提供任何有关问题的线索。

图 3

在图 3 中,我们可以清楚地看到类型 Product 正在造成麻烦。 它占据了分配的内存的很大一部分,在活动对象列中,我们可以看到应用程序创建了100万个这种类型的实例。

如果您需要在整个执行过程中创建类型的实例总数,则必须使用分析(instrumentation)技术。本文后面会介绍。 这个应用程序只是一个示例,但在现实世界的应用程序中,简单地按占用空间排序可能是不够的。 我们需要弄清楚问题是存在大量实例,还是每个实例占用了大量空间。 我知道你在想什么:这件事不是很清楚吗?是的,但在实际应用中可能不是, 所以我总是建议开发人员也按实例数量降序排序以确保这一点。 图 4 展示了按应用程序为每种类型创建的实例数量降序排列的采样数据。 同样,类型 Product 在顶部。

图 4

有时采样足以帮助你识别问题。 但如果你不知道应用程序的哪个部分创建了这些对象呢? 如果仅通过对执行进行采样无法找到问题,那么下一步就是分析(instrumentation)了。 分析提供了更多的细节,包括代码的哪一部分创建了潜在的问题实例。 但请记住经验法则:在使用分析时,首先需要知道要分析什么。这就是为什么我们总是从采样开始。

因为我们知道问题出在 Product 类型上,所以我们将对其进行分析。 和前面的文章一样,必须使用表达式指定要分析应用程序的哪个部分。 在图 5 中,我只分析了 Product 类型。 我通过在窗口右侧的 Memory settings 文本框中使用类的完全限定名(包名和类名)来做到这一点。

图 5

就像分析CPU的例子一样,你可以一次分析更多的类型,甚至指定整个包。下面是一些最常用的表达式:

严格类型的全限定名称 (e.g., com.example.model.Product) — 只搜索特定的类型给定包中的类型 (e.g., com.example.model.*)— 只搜索 com.example.model 包中声明的类型,不搜索其子包给定包及其子包中的类型 (e.g., com.example.**)—在给定包及其所有子包中搜索

一定要记住尽可能地限制你配置的类型。如果你知道问题出在 Product 上,那么只分析这个类型是有意义的。

除了活动对象(仍然存在于内存中的该类型的实例),您将获得应用程序创建的该类型实例的总数。 此外,您将看到这些实例在GC中“幸存”了多少次(我们称之为 generations)。

这些细节很有价值,但找出代码的哪一部分创建对象通常更有用。 如图 6 所示,对于每种分析类型,工具会显示实例创建的位置。 点击表格中行左侧的加号(+)。 此功能可以快速向您显示问题的根本原因。

图 6

使用 heap dump 来查找内存泄漏

如果应用程序正在运行,您可以分析以确定任何可以优化的功能。 但是,如果应用程序崩溃,并且您怀疑这是由于内存分配问题发生的,该怎么办?在大多数情况下, 应用程序崩溃是由内存分配问题(如内存泄漏)引起的——即使应用程序不需要它们,也不会释放它在内存中创建的对象。 由于内存不是无限的,持续分配对象会在某个时候填满内存,导致应用程序崩溃。 在JVM应用程序中,这个错误会在运行时抛出 OutOfMemoryError

如果应用没有运行,你就无法附加 profiler 来调查执行情况。 但是,即使如此,你还有其他的选择来研究这个问题。 您可以使用 heap dump,它是应用程序崩溃时堆内存的快照。 虽然你可以随时收集 heap dump,但当你由于某些原因无法分析应用程序时,它是最有用的。 这些原因可能是应用程序崩溃,或者只是因为无法分析进程,而你想确定它是否受到内存分配问题的影响。

在下面,我们将讨论获得 heap dump 的三种可能方法, 向您展示如何使用 heap dump 来识别内存分配问题及其根源。 我们将讨论使用名为对象查询语言(Object query language, OQL)的查询语言读取 heap dump 的更高级方法。 OQL类似于SQL,但不是查询数据库,而是使用OQL查询 heap dump 中的数据。

获取 heap dump

我们将讨论三种生成 heap dump 的方法:

配置应用程序,使其在应用程序因内存问题崩溃时在给定位置自动生成一个。使用分析工具(例如 VisualVM)。使用命令行工具(例如 jcmdjmap)。

您甚至可以通过编程方式获得 heap dump。 一些框架具有生成 heap dump 的功能,这允许开发人员集成应用程序监控工具。 要了解有关此主题的更多信息,请参阅Java官方API文档()中的 HotSpotDiagnosticMXBean 类。

项目 da-ch11-ex1 实现了一个端点,您可以使用 HotSpotDiagnosticMXBean 类来生成 heap dump。 使用 cURL 或 Postman 调用这个端点会创建一个 dump 文件:

curl 

配置应用程序,使其在遇到内存问题时生成 heap dump

当开发人员怀疑错误的内存分配导致了问题时,他们通常使用 heap dump 来调查应用程序崩溃。 出于这个原因,应用程序通常配置为在应用程序崩溃时生成内存的 heap dump。 当应用程序由于内存分配问题而停止时,您应该始终将其配置为生成 heap dump。 幸运的是,配置很简单。你只需要在应用程序启动时添加几个JVM参数:

-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=heapdump.bin

第一个参数是 -XX:+HeapDumpOnOutOfMemoryError,它告诉应用程序在遇到 OutOfMemoryError (堆已满)时生成一个 heap dump。 第二个参数 -XX:HeapDumpPath=heapdump.bin 指定了文件系统中存储 dump 文件的路径。 在本例中,包含 heap dump 的文件将被命名为 heapdump.bin,位于可执行应用程序附近, 在 classpath 的根目录下(因为我们使用了相对路径)。 请确保进程对该路径具有 “write” 权限,以便能够将文件存储到给定位置。

下面的代码片段展示了运行应用程序的完整命令:

java -jar -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump.bin app.jar

我们将使用一个名为 da-ch11-ex2 的演示应用程序来演示这种方法。 你可以在本书提供的项目中找到这个应用程序。 下列代码清单中的应用程序不断地将 Product 类型的实例添加到list中,直到内存填满。

清单 1 生成大量无法释放的实例

public class Main {  private static List<Product> products = new ArrayList<>();  public static void main(String[] args) {    Random r = new Random();    while (true) {                        // 这个循环永远迭代下去。      Product p = new Product();      p.setName("Product " + r.nextInt());      products.add(p);   // 向list中添加实例,直到内存被填满。    }  }}

下面的代码片段展示了简单的 Product 类型:

public class Product {  private String name;  // Omitted getters and setters}

你可能想知道为什么 product 实例有一个随机名称。 呆会讨论读取 heap dump 时,我们会用到它。 目前,我们只对如何生成 heap dump 感兴趣,以弄清楚为什么这个应用程序可以在几秒钟内填满它的堆内存。

你可以使用IDE来运行应用程序并设置参数。 图 7 展示了如何在IntelliJ中设置JVM参数。 我还添加了 -Xmx 参数,以限制应用程序的堆内存仅为100 MB。 这将使 heap dump 文件更小,我们的示例更容易。

图 7

当你运行应用程序时,稍等片刻,应用程序将崩溃。 只有 100 MB 的堆空间,内存应该不会超过几秒钟就会被填满。 项目文件夹中有一个名为 heapdump.bin 的文件,其中包含了应用程序停止时堆中数据的所有详细信息。 你可以用 VisualVM 打开这个文件进行分析,如图 8 所示。

图 8

使用 profiler 获取 heap dump

有时需要为运行中的进程获取 heap dump。 在这种情况下,最简单的解决方案是使用 VisualVM(或类似的分析工具)来生成 dump。 使用VisualVM获取 heap dump 非常简单,只需点击一个按钮。 只需使用 Monitor 选项卡中的 Heap Dump 按钮,如图 9 所示。

图 9

使用命令行获取 heap dump

如果你需要为一个正在运行的进程获取 heap dump,但你的应用程序部署在一个无法连接 profiler 的环境中,不要惊慌; 你还是有选择的。 可以使用JDK提供的命令行工具 jmap 来生成 heap dump。

使用 jmap 收集 heap dump 有两个步骤:

找到您希望获得 heap dump 的正在运行的应用程序的进程ID (PID)。使用 jmap 将 dump 保存到文件中。

要找到运行进程的PID,可以像《Java 故障诊断 - 使用 thread dump 来调查死锁问题》那样使用 jps:

jps -l25320 main.Main132 jdk.jcmd/sun.tools.jps.Jps25700 org.jetbrains.jps.cmdline.Launcher

第二步是使用 jmap。要调用 jmap,请指定PID和保存 heap dump 文件的位置。 您还必须使用 -dump:format=b 参数指定输出为二进制文件。 图 10 展示了在命令行中如何使用这个工具。

图 10

复制下面的代码来方便地使用这个命令:

jmap -dump:format=b,file=C:/DA/heapdump.bin 25320

现在可以在 VisualVM 中打开用 jmap 保存的文件进行研究了。

阅读 heap dump

这里我们将重点讨论如何使用 heap dump 来研究内存分配问题。 heap dump 就像生成 dump 时内存的 “picture”。 它包含了堆中应用程序的所有数据,这意味着你可以使用它来检查数据及其结构。 通过这种方式,您可以确定哪些对象占用了大部分已分配的内存,并了解为什么应用程序无法释放它们。

请记住,在 “picture” (heap dump) 中,您可以看到所有内容。 如果内存中有未加密的密码或任何类型的私有数据,使用 heap dump 的人将能够获得这些详细信息。

与 thread dump 不同,不能将 heap dump 分析为纯文本。 相反,您必须使用 VisualVM(或任何一般的分析工具)。 这里我们将使用 VisualVM 来分析前面介绍的项目 da-ch11-ex2 生成的 heap dump 文件。 你将学习如何利用这种方法找出 OutOfMemoryError 的根本原因。

在VisualVM中打开 heap dump 时,分析工具会显示堆转储的摘要视图(如图 11 所示), 其中提供了 heap dump 文件的详细信息(例如,文件大小、类的总数、dump 中的实例总数)。 您可以使用这些信息来确保您有正确的 dump,以防您不是提取它的人。

图 11

有时候,我不得不调查能够访问应用程序运行环境的支持团队的 heap dump。 但是,我自己无法访问这些环境,所以我必须依赖其他人为我获取数据。 我不止一次感到惊讶,我得到了错误的 heap dump。 我可以通过查看转储文件的大小并将其与我所知道的进程配置的最大值进行比较, 甚至通过查看操作系统或Java版本来识别错误。

我的建议是首先快速检查总结页面,并确保你有正确的文件。 在摘要页面上,您还会发现占用大量空间的类型。 我通常不依赖这个总结,而是直接进入对象视图,从那里开始我的研究。 在大多数情况下,总结不足以让我得出结论。

要切换到对象视图,在 heap dump 选项卡左上角的下拉菜单中选择对象(如图 12 所示)。 这将允许您调查 heap dump 中的对象实例。

图 12

就像内存采样和分析一样,我们正在搜索使用最多内存的类型。 最好的方法是根据实例和占用的内存进行降序排序,并查找应用程序代码库中的第一个类型。 不要查找基本类型、字符串或基本类型和字符串组成的数组。 通常会有很多,而且它们不会给你太多关于哪里出了问题的线索。

在图 13 中可以看到,排序之后,问题似乎与 Product 类型有关。 Product 类型是应用程序代码库的第一个类型,它使用了很大一部分内存。 我们需要弄清楚为什么会创建这么多实例,以及为什么GC不能从内存中删除它们。

图 13

你可以选择左侧的小加号(+)来获取该类型的所有实例的详细信息。 我们已经知道有超过100万个 Product 实例,但我们仍然需要查找

代码的哪一部分创建了这些实例为什么GC不能及时删除它们以避免应用程序的失败

您可以找到每个实例(通过字段)引用的内容以及引用该实例的内容。 因为我们知道除非实例没有引用,否则 GC 不能从内存中删除实例,所以我们寻找引用实例的对象, 以确定处理上下文中是否仍然需要它,或者应用程序是否忘记删除它的引用。

图 14 显示了一个 Product 实例的详细信息的展开视图。 可以看到,这个实例引用的是一个 String(product 名称), 而这个引用保存在一个 Object 数组中,而这个 Object 数组是 ArrayList 实例的一部分。 此外,ArrayList 实例似乎保存了大量的引用(超过100万个)。 这通常不是一个好迹象,因为要么应用程序实现了未优化的功能,要么我们发现了内存泄漏。

图 14

要理解哪一种情况,我们需要使用前面文章中讨论的调试和日志记录技术来研究代码。 幸运的是,profiler 会告诉你在代码中的哪里可以找到这个list。 在我们的例子中,list 被声明为 Main 类中的静态变量。

使用 VisualVM,我们可以很容易地理解对象之间的关系。 通过将这种技术与你在本系列文章中学到的其他调查技术结合起来,你就拥有了解决这类问题所需的所有工具。 复杂的问题(和应用程序)可能仍然需要大量的工作,但使用这种方法将节省你大量的时间。

使用OQL控制台查询 heap dump

现在我们将讨论研究 heap dump 的一种更高级的方法。 我们使用类似于SQL的查询语言从 heap dump 中检索详细信息。 我们在前面讨论的简单方法通常足以找出内存分配问题的根本原因。 但是,当我们需要比较两个或更多 heap dump 的细节时,它们是不够的。

假设您想比较应用程序的两个或多个版本提供的 heap dump ,以确定在两个版本之间是否实现了错误或未优化的内容。 您可以逐个手动调查它们。但我将教你如何编写可以轻松地在每个类上运行的查询,这将节省你的时间。 这就是OQL的出色之处。图 15 展示了如何将视图切换到 OQL 控制台,在那里可以运行查询来调查 heap dump 。

图 15

我们将讨论一些我认为最有用的例子,但请记住, OQL 更复杂。(你可以在上找到有关其功能的更多信息。)

让我们从一个简单的例子开始:选择给定类型的所有实例。 假设我们想从 heap dump 中获取所有 Product 类型的实例。 要使用 SQL 查询从关系数据库的一张表中获取所有 product 记录,我们可以这样写:

select * from product

要使用 OQL 查询 heap dump 中的所有 Product 实例,你需要这样写:

select p from model.Product p

注意

对于OQL,“select”、“from” 或 “where” 等关键字总是用小写字母书写。类型总是带有完全限定名(包名+类名)。

图 16 展示了执行简单查询从 heap dump 中检索所有 Product 实例的结果。

图 16

注意

在学习OQL时,使用小 heap dump。实际的 heap dump 通常很大(4 GB或更大)。 OQL 查询会很慢。如果你只是在学习,可以像本章一样创建和使用小型 heap dump。

您可以选择任何被查询的实例以获取其详细信息。 你可以找到是什么保存了对这个实例的引用,这个实例引用了什么,以及它的值(如图 17 所示)。

图 17

您还可以选择从某些实例引用的值或引用。 例如,如果想获取所有的 product 名称,而不是 product 实例,可以编写如下查询(如图 18 所示):

select p.name from model.Product p

图 18

使用 OQL,您可以同时提取多个值。为此,需要将它们格式化为 JSON,如下面的代码清单所示。

清单 2 使用JSON投影

select{  -- Curly braces surround the JSON object representation   name: p.name,  -- Attribute name takes the value of the product name   name_length: p.name.value.length  -- Attribute name_length takes the value of the product name number of characters}from model.Product p

图 19 展示了运行这个查询的结果。

图 19

您可以更改此查询,例如,为一个或多个所选值添加条件。 假设你只想选择名称大于15个字符的实例。 你可以像下面这样编写一个查询:

select { name: p.name, name_length: p.name.value.length}from model.Product pwhere p.name.value.length > 15

让我们来看一些更高级的东西。在研究内存问题时,我经常使用 referers() 方法来获取引用特定类型实例的对象。 通过使用像下面这样的内置 OQL 函数,你可以做很多有用的事情:

查找或查询实例 referees—可以告诉你应用程序是否有内存泄漏查找或查询实例引用 referrals—能否告诉您特定实例是否是内存泄漏的原因查找实例中的重复项—可以告诉您是否可以优化特定的功能以使用更少的内存查找特定实例的子类和超类—让你无需查看源代码就能了解应用程序的类设计Identify long life paths—可以帮助您识别内存泄漏吗

要获取类型为 Product 的实例的所有唯一引用,可以使用以下查询:

select unique(referrers(p)) from model.Product p

图 20 显示了运行这个查询的结果。 在这个例子中,我们可以看到所有的 product 实例都被一个对象引用——一个list。 通常,当大量实例具有少量引用时,这是内存泄漏的迹象。 在我们的例子中,一个list保存对所有 Product 实例的引用,防止GC从内存中删除它们。

图 20

如果结果不是唯一的,您可以使用 next 查询按实例计算引用数量,以找到可能涉及内存泄漏的实例:

select { product: p.name, count: count(referrers(p))} from model.Product p

OQL查询提供了很多机会,一旦您编写了一个查询,您可以根据需要在不同的 heap dump 中运行它多次。

结语

没有针对内存分配进行优化的应用程序可能会导致性能问题。 优化应用程序以明智地分配(避免花费不必要的内存空间)内存中的数据对应用程序的性能至关重要。

分析工具允许您对应用程序执行期间内存的占用情况进行采样和分析。 这可以帮助您识别应用程序中未优化的部分,并为您提供可以改进的细节。

如果在执行期间不断地将新的对象实例添加到内存中,但应用程序从未删除对新实例的引用,那么GC将无法删除引用并释放内存。 当内存被完全占用时,应用程序无法继续执行并停止。 在停止之前,应用程序抛出了一个 OutOfMemoryError

要调查 OutOfMemoryError,我们使用 heap dump 。 heap dump 收集应用程序堆内存中的所有数据,并允许您分析它以找出哪里出了问题。

您可以使用几个 JVM 参数启动应用程序,以指示它在给定路径上生成 heap dump ,如果应用程序以 OutOfMemoryError 失败。

您还可以使用分析工具或命令行工具(如 jmap)来获得 heap dump 。

要分析 heap dump ,请将其加载到一个分析工具中,例如 VisualVM,它允许您研究转储中的实例及其关系。 通过这种方式,您可以确定应用程序的哪个部分没有优化或存在内存泄漏。

VisualVM 提供了更高级的方法来分析 heap dump ,例如OQL查询。 OQL是一种类似于SQL的查询语言,用于从 heap dump 中检索数据。

标签: #java中内存执行程序的原理是什么 #java用户管理的代码中查询不了是哪出问题了 #怎么查看java文件保存在哪里 #java建包后出现错误 #java中表格各项数据弹到文本框里