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

时间:2010-03-24 20:53:28

标签: java methodology enterprise legacy-code memory-leaks

我必须在Java应用程序中发现内存泄漏。我对此有一些经验,但希望就此采用方法/策略方面的建议。欢迎任何参考和建议。

关于我们的情况:

  1. 堆转储大于1 GB
  2. 我们有5次堆转储。
  3. 我们没有任何测试案例可以引发这种情况。它只发生在(大规模)系统测试环境中,至少使用一周后。
  4. 该系统建立在内部开发的遗留框架之上,存在许多设计缺陷,无法统计这些缺陷。
  5. 没有人深入了解这个框架。它已被转移到印度的一个家伙,他几乎没有及时回复电子邮件。
  6. 我们已经完成了快照堆转储,并得出结论,没有一个组件随着时间的推移而增加。一切都在缓慢增长。
  7. 以上指出了我们的框架是本土的ORM系统,它可以无限制地增加其使用。 (这个系统将对象映射到文件?!所以不是真正的ORM)
  8. 问题: 帮助您成功捕获企业级应用程序泄漏的方法是什么?

8 个答案:

答案 0 :(得分:56)

如果不了解底层代码,几乎是不可能的。如果您了解底层代码,那么您可以更好地从堆中获取您在堆转储中获得的大量信息。

另外,如果不知道为什么班级在那里,你就无法知道某件事是否泄漏。

我刚刚花了几个星期来做这件事,我使用了一个迭代过程。

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

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

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

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

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

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

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

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

然后我会考虑其中的几个差异,删除从运行到运行的所有类,并取结果的并集。最后,我有一个在特定时间跨度内不断增长的类列表。显然,这些是泄漏课程的主要候选人。

但是,有些课程保留了一些,而其他课程则保留了GC。这些类总体上很容易上下,但仍然会泄漏。因此,它们可能会脱离“不断上升”的类别。

为了找到这些,我将数据转换为时间序列并将其加载到数据库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(或者你的工作单位是什么)。

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

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

答案 1 :(得分:13)

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

我过去曾用它来帮助追捕可疑的泄漏。

答案 2 :(得分:5)

这个答案扩展到了@ Will-Hartung的。我申请了相同的程序来诊断我的一个内存泄漏,并认为共享细节可以节省其他人的时间。

这个想法是让postgres&#39; plot&#39;时间与每个类的内存使用情况,画一条线来总结增长并识别增长最快的对象:

    ^
    |
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 ;

对size(num of bytes)查询运行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 :(得分:3)

你能加快时间吗?即你能写一个虚拟测试客户端,迫使它在几分钟或几小时内做几周的电话/请求等吗?这些是你最大的朋友,如果你没有 - 写一个。

我们前一段时间使用Netbeans来分析堆转储。它可能有点慢,但它很有效。 Eclipse刚刚崩溃,32位Windows工具也一样。

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

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

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

答案 4 :(得分:2)

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

答案 5 :(得分:1)

如果在使用一周后发生这种情况,并且您的应用程序就像您描述的拜占庭那样,或许您最好每周重新启动它?

我知道这不是解决问题的方法,但它可能是一个时间有效的解决方案。你有停电的时候有窗口吗?你可以在保持第二个实例的同时对一个实例进行负载平衡和故障转移当内存消耗超出某个限制时(或许通过JMX或类似的监视),也许你可以触发重启。

答案 6 :(得分:1)

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

本文为您提供了捕获堆转储的多个选项。但是,我认为前三个是有效的选择,而其他三个则是个不错的选择。 1. jmap 2. HeapDumpOnOutOfMemoryError 3. jcmd 4. JVisualVM 5. JMX 6.程序化方法 7. IBM管理控制台

7 Options to capture Java Heap dumps

答案 7 :(得分:0)

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