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

Posted

技术标签:

【中文标题】在大型 Java 堆转储中查找内存泄漏的方法【英文标题】:Method for finding memory leak in large Java heap dumps 【发布时间】:2011-01-31 11:08:34 【问题描述】:

我必须在 Java 应用程序中找到内存泄漏。我有这方面的一些经验,但想就此提出方法/策略的建议。欢迎任何参考和建议。

关于我们的情况:

    堆转储大于 1 GB 我们有 5 次堆转储。 我们没有任何测试案例可以引发这种情况。它仅在使用至少一周后发生在(大规模)系统测试环境中。 该系统建立在内部开发的遗留框架之上,存在许多设计缺陷,无法全部计算在内。 没有人深入了解该框架。它已被转移给印度的一个人,他几乎无法跟上回复电子邮件的速度。 随着时间的推移,我们已经完成了快照堆转储,并得出结论,没有一个组件会随着时间的推移而增加。它是慢慢成长的一切。 以上内容为我们指明了方向,即框架本土化的 ORM 系统无限制地增加了其使用率。 (这个系统将对象映射到文件?!所以不是真正的 ORM)

问题: 什么方法可以帮助您成功找出企业级应用程序中的漏洞?

【问题讨论】:

@LB 我们有 64 GB,但这个应用程序的预算只有 2 GB,如果不开始蚕食其他子系统,我们不能合理地增加超过几个 GB。 堆转储是 JVM 特定的,因此您需要使用适合相关 JVM 的工具。是吗? @Thorbjørn Ravn Andersen 我们使用 Sun Java SDK 1.6 好的,您是否考虑过使用 visualvm 附加到长期运行的进程,以了解随着时间的推移情况如何演变?有一个独立的版本和JDK中的一个。 @Thorbjørn Ravn Andersen 我已经简要地考虑过它,但推迟它,因为我预计它会非常慢。但也许是时候尝试一下了。 【参考方案1】:

如果不了解底层代码,这几乎是不可能的。如果您了解底层代码,那么您可以更好地从堆转储中获得的无数信息的谷壳中分拣出小麦。

此外,如果不知道类为什么存在,您就无法知道某事是否存在泄漏。

在过去的几周里,我正是这样做的,我使用了一个迭代过程。

首先,我发现堆分析器基本上没用。他们无法有效地分析巨大的堆。

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

我想你对这些很熟悉,但对于那些不熟悉的人:

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

创建活动堆的直方图。简而言之,它会告诉您类名,以及每个类在堆中的实例数。

我每天 24 小时每 5 分钟定期倒垃圾。这对你来说可能过于细化,但要点是一样的。

我对这些数据进行了几次不同的分析。

我编写了一个脚本来获取两个直方图,并输出它们之间的差异。所以,如果 java.lang.String 在第一次转储中是 10,而在第二次转储中是 15,我的脚本会吐出“5 java.lang.String”,告诉我它上升了 5。如果它下降了,数字将是负数。

然后,我会采用其中的几个差异,去掉所有运行中出现的类,然后对结果进行联合。最后,我会有一个在特定时间跨度内不断增长的课程列表。显然,这些是泄漏类的主要候选者。

但是,有些类保留了一些,而另一些则是 GC'd。这些类总体上可以很容易地上下波动,但仍然会泄漏。因此,他们可能会脱离“不断上升”的类别。

为了找到这些,我将数据转换为时间序列并将其加载到数据库中,特别是 Postgres。 Postgres 很方便,因为它提供了statistical aggregate functions,因此您可以对数据执行简单的linear regression analysis,并找到趋势上升的类,即使它们并不总是在图表顶部。我使用了 regr_slope 函数,寻找具有正斜率的类。

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

现在,这一切可以做的就是识别潜在的泄漏类。

这是了解如何使用类以及它们是否应该成为它们的作用的地方。

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

除非您只是简单地缓存 String,否则这些系统类(虽然可能是“违规者”)并不是“问题”。如果您正在缓存某个应用程序类,那么该类可以更好地指示您的问题所在。如果您不缓存 com.app.yourbean,那么您将不会有关联的 Map.Entry 与之关联。

一旦你有了一些类,你就可以开始爬取代码库来寻找实例和引用。由于您有自己的 ORM 层(无论好坏),您至少可以轻松查看它的源代码。如果你的 ORM 正在缓存东西,它可能会缓存包装你的应用程序类的 ORM 类。

最后,您可以做的另一件事是,一旦您知道类,您就可以启动服务器的本地实例,使用更小的堆和更小的数据集,并使用其中一个分析器来处理。

在这种情况下,您可以进行单元测试,只影响您认为可能泄漏的 1 个(或少量)事物。例如,您可以启动服务器、运行直方图、执行单个操作,然后再次运行直方图。您泄漏的课程应该增加 1(或任何您的工作单元)。

分析器可能能够帮助您跟踪“现已泄露”类的所有者。

但是,最后,您将不得不对您的代码库有所了解,以便更好地理解什么是泄漏,什么不是,以及为什么一个对象存在于堆中,更不用说为什么它可能被保留为堆中的泄漏。

【讨论】:

【参考方案2】:

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

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

【讨论】:

我昨天用它成功地分析了一个 ~180 兆的堆转储,就像一个魅力。 Eclipse MAT 非常棒,尤其是它的内存泄漏检测器。 是的,这就是我们主要使用的。它在 64 位 Linux 上与至少 1.5 GB 堆转储一起工作得很好(当然 Win 32 位失败很快)。唯一的缺点是我没有从其中的自动分析中获得非常有用的帮助。 似乎使用与堆文件一样多的 RAM [不幸的是,这在我的情况下太多了]。比 jhat 更好...对于具有大型 hprof 文件的关注者,请参阅 ***.com/questions/7254017/…【参考方案3】:

这个答案扩展了@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 ;

针对大小(字节数)查询运行 slop 查询:

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 一起创建。

【讨论】:

嘿,很好的答案。你认为什么斜率(字节/时间)是一个“好的”斜率。例如,如果某个对象的斜率为 3.45,您会认为这是内存泄漏吗?【参考方案4】:

你能加速时间吗?即,您能否编写一个虚拟测试客户端,强制它在几分钟或几小时内完成数周的通话/请求等?这些是你最大的朋友,如果你没有,那就写一个吧。

我们不久前使用过 Netbeans 来分析堆转储。它可能有点慢,但很有效。 Eclipse 刚刚崩溃,32 位 Windows 工具也崩溃了。

如果您可以访问 64 位系统或 3GB 或更多的 Linux 系统,您会发现更容易分析堆转储。

您是否有权访问更改日志和事件报告?大型企业通常会有变更管理和事件管理团队,这可能有助于跟踪问题何时开始发生。

什么时候开始出错?与人交谈并尝试了解一些历史。您可能会听到有人说,“是的,在他们在 6.43 补丁中修复了 XYZ 之后,我们才发生了奇怪的事情”。

【讨论】:

我们认为这应该是一个好主意,但在我们的情况下,它在洞中是不可行的。我们只能更频繁地执行一些测试用例。系统测试仅每 6 个月左右执行一次,也是他们最后一次决定使其更加激烈。在此之后,我们发现了问题。我们尝试将框架和应用程序降级到之前通过测试的版本。所有三个测试都失败了,这告诉我们故障是系统中另一个组件中的以太或已经存在很长时间了。另一个组件不太可能。【参考方案5】:

我在 IBM Heap Analyzer 上取得了成功。它提供了堆的多个视图,包括对象大小的最大下降、最常出现的对象以及按大小排序的对象。

【讨论】:

【参考方案6】:

有 Eclipse MAT 和 Heap Hero 等出色的工具可以分析堆转储。但是,您需要为这些工具提供以正确格式和正确时间点捕获的堆转储。

本文为您提供了多种捕获堆转储的选项。但是,在我看来,前 3 个是可以使用的有效选项,其他是需要注意的好选项。 1.jmap 2. HeapDumpOnOutOfMemoryError 3.jcmd 4. JVisualVM 5.JMX 6. 程序化方法 7. IBM 管理控制台

7 Options to capture Java Heap dumps

【讨论】:

【参考方案7】:

如果它是在使用一周后发生的,并且您的应用程序如您所描述的那样拜占庭式,也许您最好每周重新启动它?

我知道这并不能解决问题,但它可能是一个有效的解决方案。是否有时间窗口可以中断?您能否在保持第二个实例正常运行的同时对一个实例进行负载平衡和故障转移?也许您可以在内存消耗超过某个限制时触发重启(可能通过 JMX 或类似方式进行监控)。

【讨论】:

windows解决方案! (我们的 IT 部门将其用于我们的 Windows 服务器)我们不自己运行系统,它出售给无法接受重启(计划或计划外)的公司。不稳定的迹象将导致罚款的威胁。 我不喜欢它,但在某些情况下它很实用。不过,我注意到你关于向公司销售的观点。 我同意在某些情况下它可以是一个(临时)解决方案。【参考方案8】:

我用过jhat,这有点苛刻,但取决于你拥有的框架类型。

【讨论】:

我们没有设法在 jhat 中加载这么大的堆转储,这似乎是一个常见问题。另外,我记得两年前使用它时,它在更大的数据集上有点慢。 作为另一个轶事,我也遇到了 jhat 和 largeish (1G) 堆的加载问题 什么样的问题?运行 jhat 的 jvm 上的堆空间问题?

以上是关于在大型 Java 堆转储中查找内存泄漏的方法的主要内容,如果未能解决你的问题,请参考以下文章

如何查找 Java 内存泄漏

为啥我的 Java 堆转储大小比已用内存小得多?

如何在eclipse中使用MAT查找内存泄漏

分析大型 Java 堆转储 - 内存错误

内存泄漏的java堆和线程分析

Java内存泄漏分析工具Memory Analyzer Tool