代码之家  ›  专栏  ›  技术社区  ›  Rickard von Essen

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

  •  27
  • Rickard von Essen  · 技术社区  · 14 年前

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

    关于我们的情况:

    1. 堆转储大于1 GB
    2. 我们有5次堆垃圾。
    3. 我们没有任何测试案例来引发这种情况。它只在使用了至少一周后的(大规模)系统测试环境中发生。
    4. 该系统是建立在一个内部开发的遗留框架之上的,它有太多的设计缺陷,以至于无法将它们全部计算在内。
    5. 没有人深入了解这个框架。它已经转移到 印度一个几乎跟不上回复邮件的人。
    6. 我们已经完成了一段时间内的快照堆转储,并得出结论,随着时间的推移,没有单个组件在增加。一切都在缓慢地成长。
    7. 上面指出,正是这些框架自行开发的ORM系统在不受限制的情况下增加了其使用量。(此系统将对象映射到文件?!所以不是真正的ORM)

    问题: 帮助您成功查找企业级应用程序中泄漏的方法是什么?

    7 回复  |  直到 8 年前
        1
  •  55
  •   Will Hartung    14 年前

    如果不理解底层代码,几乎是不可能的。如果您了解底层代码,那么您可以更好地从堆转储中获取的海量信息中对小麦进行分类。

    另外,你不知道什么东西是泄漏还是不泄漏,不知道为什么一开始就有这个类。

    我刚刚花了几周的时间来做这个,我使用了一个迭代过程。

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

    相反,我几乎完全依赖 jmap 柱状图。

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

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

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

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

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

    我编写了一个脚本来获取两个柱状图,并消除它们之间的差异。所以,如果JavaLang.Stand在第一个转储中是10,第二个是15,我的脚本会吐出“5 Java.Lang.Stand”,告诉我它上升了5。如果下降了,这个数字就会是负数。

    然后,我将处理其中的几个差异,去掉从一个运行到另一个运行的所有类,并对结果进行联合。最后,我会列出在特定时间段内不断增长的类的列表。显然,这些都是泄漏类的主要候选者。

    但是,一些类保留了一些类,而其他类是GC'D类。这些类可以很容易地整体上下移动,但仍然会泄漏。因此,他们可能会脱离“总是在上升”的阶级范畴。

    为了找到这些,我将数据转换成一个时间序列,并将其加载到数据库postgres中。Postgres很方便,因为它提供 statistical aggregate functions ,所以你可以做简单的 linear regression analysis 在数据上,找到趋势向上的类,即使它们并不总是在图表上。我使用了regr_slope函数,查找具有正斜率的类。

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

    现在,所能做的就是识别潜在的泄漏类。

    这就是理解如何使用这些类,以及它们是否应该是它们的参与方。

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

    除非您只是在缓存字符串,否则事实是这些系统类,而“罪犯”可能不是“问题”。如果您正在缓存一些应用程序类,那么该类更能指示问题所在。如果不缓存com.app.yourbean,则不会将关联的map.entry绑定到它。

    一旦拥有了一些类,就可以开始对代码库进行爬行,以查找实例和引用。由于您有自己的ORM层(无论是好是坏),所以至少可以很容易地查看它的源代码。如果您的ORM正在缓存东西,那么它很可能正在缓存包装应用程序类的ORM类。

    最后,您可以做的另一件事是,一旦您了解了类,就可以用一个更小的堆和更小的数据集启动服务器的本地实例,并使用其中一个配置文件来反对它。

    在这种情况下,您可以进行单元测试,只影响您认为可能泄漏的1个(或少数)东西。例如,您可以启动服务器,运行柱状图,执行单个操作,然后再次运行柱状图。你的漏课应该增加1(或者你的工作单元是什么)。

    探查器可能会帮助您跟踪“现在泄漏”类的所有者。

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

        2
  •  13
  •   rogerdpack    8 年前

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

    我过去用它来帮助查出可疑的泄漏。

        3
  •  5
  •   joseph    8 年前

    这个答案扩展到了@will hartung's。我应用了相同的过程来诊断我的一个内存泄漏,并认为分享细节可以节省其他人的时间。

    其想法是让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
    

    斜率是每秒添加的字节数(因为epoch的单位是秒)。如果使用实例而不是大小,那么这就是每秒添加的实例数。

    创建这个joe.schmoe.businessobject的一行代码负责内存泄漏。它正在创建对象,将其附加到数组中,而不检查它是否已经存在。其他对象也与泄漏代码附近的BusinessObject一起创建。

        4
  •  3
  •   Fortyrunner    14 年前

    你能加快时间吗?也就是说,你能写一个虚拟测试客户机,迫使它在几分钟或几小时内完成数周的呼叫/请求等吗?这些是你最大的朋友,如果你没有-写一个。

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

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

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

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

        5
  •  2
  •   rogerdpack    8 年前

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

        6
  •  1
  •   Brian Agnew    14 年前

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

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

        7
  •  0
  •   LB40    14 年前

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