大型Java堆转储中查找内存泄漏的方法

37

我需要查找 Java 应用程序中的内存泄漏问题。我有一些经验,但希望能获得此方面的方法策略建议。欢迎提供任何参考和建议。

关于我们的情况:

  1. 堆转储文件大于 1GB
  2. 我们有来自 5 次内存泄漏问题的堆转储文件。
  3. 我们没有任何测试用例来引发这个问题。仅在(大规模的)系统测试环境中使用至少一周后才会发生。
  4. 该系统是建立在一个内部开发的老框架之上,存在着太多的设计缺陷,无法数清。
  5. 没有人深入了解这个框架。它已被转移到了印度的一个人手中,他几乎跟不上回复电子邮件的速度。
  6. 我们随时间对快照堆转储文件进行了分析,得出结论:没有单个组件随着时间推移而增加。它是所有东西都在缓慢增长。
  7. 以上使我们认为,是框架自制的 ORM 系统无限增加其使用量。 (此系统将对象映射到文件?所以并不是真正的 ORM)

问题:在企业级应用程序中查找泄漏问题时,哪种方法论帮助您成功?


@LB 我们有64GB,但是这个应用程序的预算只有2GB,而且我们不能合理地增加超过几个GB,否则就要开始侵蚀其他子系统。 - Rickard von Essen
堆转储是特定于JVM的,因此您需要使用适用于所涉及JVM的工具。这是吗? - Thorbjørn Ravn Andersen
@Thorbjørn Ravn Andersen 我们使用Sun Java SDK 1.6。 - Rickard von Essen
2
好的,你考虑过使用VisualVM连接到长时间运行的进程来查看随着时间的推移事物的发展吗?有一个独立版本和JDK中的版本。 - Thorbjørn Ravn Andersen
@Thorbjørn Ravn Andersen 我曾经简要考虑过这个问题,但是因为我预计它会非常慢而推迟了。但也许现在是时候尝试一下了。 - Rickard von Essen
请参见https://dev59.com/12w05IYBdhLWcg3wahJM。 - rogerdpack
8个回答

68

如果没有对底层代码的一定了解,这几乎是不可能完成的任务。如果您了解底层代码,则可以更好地从堆转储中获取大量信息并进行筛选。

此外,如果不知道某个类最初存在的原因,就无法确定是否存在泄漏。

我过去几周花了大量时间来做这件事,并使用了一个迭代过程。

首先,我发现堆分析工具基本上没用。它们无法高效地分析巨大的堆。

相反,我几乎完全依赖于jmap直方图。

我想您已经熟悉这些内容,但对于那些不熟悉的人:

jmap -histo:live <pid> > histogram.out

创建一个活动堆的直方图。简单来说,它告诉您类名称以及堆中每个类的实例数量。

我定期转储堆,每5分钟一次,24小时连续不断。这对你来说可能过于详细了,但核心内容是相同的。

我在这些数据上运行了几种不同的分析。

我编写了一个脚本,用于获取两个直方图并输出它们之间的差异。因此,如果java.lang.String在第一个转储中为10,在第二个转储中为15,则我的脚本将输出“5 java.lang.String”,告诉我它增加了5。如果下降了,数字将为负数。

然后,我会获取几个这些差异,删除从一个运行到另一个运行下降的所有类,并对结果进行合并。最终,我会得到一个特定时间跨度内不断增长的类的列表。显然,这些是潜在的泄漏类的主要候选者。

但是,有些类已经保存,而其他类已被回收。这些类整体上容易上升和下降,但仍可能泄漏。因此,它们可能会从“始终上升”的类别中掉出来。

为了找到这些类,我将数据转化为时间序列并将其加载到数据库中,具体来说是Postgres。Postgres很方便,因为它提供了统计聚合函数,因此您可以在数据上进行简单的线性回归分析,并查找趋势向上的类,即使它们并不总是排在榜首。我使用regr_slope函数,寻找斜率为正的类。

我发现这个过程非常成功,而且效率很高。直方图文件并不特别大,从主机下载它们很容易。它们在生产系统上运行成本也不是特别高(它们会强制进行大型GC,并可能会阻塞VM一段时间)。我在一个具有2G Java堆的系统上运行此操作。

现在,所有这些操作只能识别潜在的泄漏类。

这就是理解类的使用方式以及它们是否应该出现的时候的作用。

例如,您可能会发现有很多Map.Entry类或其他系统类。

除非您仅仅缓存字符串,否则事实是这些系统类虽然可能是“罪犯”,但不是“问题所在”。如果您缓存了一些应用程序类,那么该类是您问题所在的更好指标。如果您没有缓存com.app.yourbean,那么将不会有与之相关联的Map.Entry。

一旦您拥有了一些类,就可以开始爬行代码库以查找实例和引用。由于您拥有自己的ORM层(无论好坏),因此至少可以轻松地查看其源代码。如果您的ORM正在缓存数据,它很可能正在缓存包装您的应用程序类的ORM类。

最后,您可以做的另一件事是一旦知道类别,就可以启动具有较小堆和较小数据集的本地服务器实例,并使用其中


13

可以看一下 Eclipse Memory Analyzer。这是一个很棒的工具(且自包含,不需要安装Eclipse本身),它能够快速地打开非常大的堆,并具备一些相当不错的自动检测工具。虽然后者不是完美的,但EMA提供了许多非常好的方法来浏览和查询转储中的对象,以查找可能的泄漏。

我过去使用它来帮助寻找可疑的泄漏。


我昨天用这个成功地分析了一个大约180兆的堆转储,非常好用。 - Esko
Eclipse MAT 很棒,特别是它的内存泄漏检测器。 - Pascal Thivent
1
是的,这是我们主要使用的。它在64位Linux上至少可以很好地处理1.5 GB堆转储(当然Win 32位会快速失败)。唯一的缺点是我从其中没有得到非常有用的自动分析帮助。 - Rickard von Essen
似乎使用的RAM与堆文件一样多[在我的情况下,这太多了]。比jhat更好...对于有大型hprof文件的用户,请参见https://dev59.com/12w05IYBdhLWcg3wahJM - rogerdpack

9
这篇答案是对@Will-Hartung的补充。我也使用了同样的方法来诊断我的一个内存泄漏问题,想分享细节以帮助其他人节省时间。
核心思路是让Postgres绘制每个类别的时间和内存使用情况图表,并绘制总增长率线条,识别增长最快的对象。
    ^
    |
s   |  Legend:
i   |  *  - data point
z   |  -- - trend
e   |
(   |
b   |                 *
y   |                     --
t   |                  --
e   |             * --    *
s   |           --
)   |       *--      *
    |     --    *
    |  -- *
   --------------------------------------->
                      time

将您的堆转储(需要多个)转换为适合Postgres消费的格式,以从堆转储格式中获得方便:

 num     #instances         #bytes  class name 
----------------------------------------------
   1:       4632416      392305928  [C
   2:       6509258      208296256  java.util.HashMap$Node
   3:       4615599      110774376  java.lang.String
   5:         16856       68812488  [B
   6:        278914       67329632  [Ljava.util.HashMap$Node;
   7:       1297968       62302464  
...

将每个堆转储的日期时间保存到csv文件中:
2016.09.20 17:33:40,[C,4632416,392305928
2016.09.20 17:33:40,java.util.HashMap$Node,6509258,208296256
2016.09.20 17:33:40,java.lang.String,4615599,110774376
2016.09.20 17:33:40,[B,16856,68812488
...

使用此脚本:

使用此脚本:

# Example invocation: convert.heap.hist.to.csv.pl -f heap.2016.09.20.17.33.40.txt -dt "2016.09.20 17:33:40"  >> heap.csv 

 my $file;
 my $dt;
 GetOptions (
     "f=s" => \$file,
     "dt=s" => \$dt
 ) or usage("Error in command line arguments");
 open my $fh, '<', $file or die $!;

my $last=0;
my $lastRotation=0;
 while(not eof($fh)) {
     my $line = <$fh>;
     $line =~ s/\R//g; #remove newlines
     #    1:       4442084      369475664  [C
     my ($instances,$size,$class) = ($line =~ /^\s*\d+:\s+(\d+)\s+(\d+)\s+([\$\[\w\.]+)\s*$/) ;
     if($instances) {
         print "$dt,$class,$instances,$size\n";
     }
 }

 close($fh);

创建一个表格来储存数据。
CREATE TABLE heap_histogram (
    histwhen timestamp without time zone NOT NULL,
    class character varying NOT NULL,
    instances integer NOT NULL,
    bytes integer NOT NULL
);

将数据复制到您的新表中

\COPY heap_histogram FROM 'heap.csv'  WITH DELIMITER ',' CSV ;

运行倾斜查询来对字节数量进行查询:

SELECT class, REGR_SLOPE(bytes,extract(epoch from histwhen)) as slope
    FROM public.heap_histogram
    GROUP BY class
    HAVING REGR_SLOPE(bytes,extract(epoch from histwhen)) > 0
    ORDER BY slope DESC
    ;

解读结果:

         class             |        slope         
---------------------------+----------------------
 java.util.ArrayList       |     71.7993806279174
 java.util.HashMap         |     49.0324576155785
 java.lang.String          |     31.7770770326123
 joe.schmoe.BusinessObject |     23.2036817108056
 java.lang.ThreadLocal     |     20.9013528767851

斜率是每秒增加的字节数(因为时间单位为秒)。如果使用实例代替大小,则表示每秒添加的实例数。

我其中一行创建joe.schmoe.BusinessObject的代码导致了内存泄漏。它在创建对象并将其附加到数组时未检查该对象是否已存在。其他对象也与BusinessObject一起在泄漏的代码附近创建。


1
嘿,答案很好。你认为什么斜率(字节/时间)可以算作是“可以接受”的斜率呢?例如,如果某个对象的斜率为3.45,你会认为这是一个内存泄漏吗? - Kumar Shubham
1
有许多因素需要考虑。增长是否预期?例如,您的服务正在不断添加客户,因此您所需的内存量会增加。如果增长是意外的,那么您可以优先考虑。斜率告诉您失去内存的速度。您可以使用它来除以服务器的内存容量。现在,您已经估计出服务器故障之前的时间。假设服务器将在2个月内耗尽内存。也许您不需要解决问题,因为您每两周要升级一次服务器,因为有双周发布。 - joseph

3
你能加快时间吗?例如,你能编写一个虚拟的测试客户端,在几分钟或几小时内完成一周的调用/请求等吗?这些是你最好的朋友,如果你没有,就自己写一个。
我们曾经使用Netbeans分析堆转储。它可能有点慢,但很有效。Eclipse刚刚崩溃了,32位Windows工具也是如此。
如果你有一个64位系统或一个Linux系统,拥有3GB或更多内存,你将更容易地分析堆转储。
你有更改日志和事故报告的权限吗?大型企业通常会有变更管理和事故管理团队,这可能有助于追踪问题开始出现的时间。
什么时候开始出问题的?与人交谈并尝试获取一些历史记录。你可能会听到某个人说:“是的,在修补程序6.43中修复XYZ后,我们得到了奇怪的东西。”

我们认为这是一个好主意,但在我们的情况下不可行。我们只能更频繁地执行一些测试用例。系统测试每6个月左右才执行一次,上一次他们决定加强测试。之后我们发现了问题。我们尝试将框架和应用程序降级到之前通过测试的版本。所有三个测试都失败了,这告诉我们故障可能在系统中的另一个组件中,或者已经存在于我们的组件中很长时间了。另一个组件的可能性很小。 - Rickard von Essen

3

我已经成功地使用IBM的Heap Analyzer。它提供了堆的几个视图,包括对象大小最大降幅、最常出现的对象以及按大小排序的对象。


这个链接已经失效了。可能要使用MAT代替? - joseph
https://www.ibm.com/support/pages/ibm-heapanalyzer - FreshMike

2
有很棒的工具,例如Eclipse MAT和Heap Hero,可以分析堆转储。但是,您需要提供正确格式和正确时间点捕获的堆转储文件给这些工具。
本文提供了多种选项来捕获堆转储。然而,在我看来,前三个选项是使用的有效选项,其他选项则是要注意的好选项。 1. jmap 2. HeapDumpOnOutOfMemoryError 3. jcmd 4. JVisualVM 5. JMX 6. 编程方法 7. IBM管理控制台
最初的回答:7个捕获Java堆转储的选项

1

如果这种情况发生在使用一周后,而且您的应用程序像您描述的那样复杂,也许每周重新启动它会更好?

我知道这并没有解决问题,但这可能是一个时间有效的解决方案。您是否有时间窗口可以进行停机维护?您能否负载均衡和故障转移一个实例,同时保持第二个实例运行?也许当内存消耗超过某个限制时(例如通过JMX或类似方式进行监控),您可以触发重新启动。


Windows解决方案!(我们的IT部门用于我们的Windows服务器)我们不自己运行系统,而是将其销售给无法接受重新启动(计划或非计划)的公司。任何不稳定的迹象都会导致罚款威胁。 - Rickard von Essen
我不喜欢它,但在某些情况下它是务实的。然而,我注意到你关于向公司销售的观点。 - Brian Agnew
我同意在某些情况下这可能是一个(临时)解决方案。 - Rickard von Essen

0

我曾经使用过jhat,这有点严厉,但它取决于你所使用的框架类型。


我们无法在jhat中加载如此大的堆转储文件,这似乎是一个普遍存在的问题。而且我记得两年前使用它时,在更大的数据集上速度有点慢。 - Rickard von Essen
作为另一个轶事,我也曾经遇到过使用jhat和大型堆(1G)的加载问题。 - matt b
什么问题?JVM运行jhat时出现堆空间问题? - LB40

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接