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

Ric*_*sen 29 java enterprise methodology memory-leaks legacy-code

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

关于我们的情况:

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

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

Wil*_*ung 59

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

而且,如果不知道为什么班级在那里,你就不知道是否有泄漏.

我刚刚花了几个星期来完成这个,我使用了一个迭代过程.

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

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

我想你是熟悉这些,但对于那些不是:

jmap -histo:live <pid> > dump.out
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

但是,有些类保留了一些,而其他类是GC'd.这些类总体上很容易上下,但仍然会泄漏.因此,他们可能会脱离"不断上升"的类别.

为了找到这些,我将数据转换为时间序列并将其加载到数据库Postgres中.Postgres很方便,因为它提供了统计聚合函数,因此您可以对数据进行简单的线性回归分析,并找到趋势向上的类,即使它们并不总是位于图表之上.我使用了regr_slope函数,寻找具有正斜率的类.

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

现在,所有这一切都可以识别潜在泄漏的类.

这是了解如何使用类,以及它们应该或不应该是它们的用法.

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

除非你只是简单地缓存String,否则事实上这些系统类可能是"违法者",而不是"问题".如果您正在缓存某些应用程序类,那么该类可以更好地指示您的问题所在.如果你不缓存com.app.yourbean,那么你就不会将相关的Map.Entry绑定到它.

一旦有了一些类,就可以开始抓取代码库来查找实例和引用.既然你有自己的ORM层(无论好坏),你至少可以随时查看它的源代码.如果您正在缓存ORM,它可能会缓存包装您的应用程序类的ORM类.

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

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

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

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


mat*_*t b 13

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

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


jos*_*eph 7

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

我们的想法是让postgres'plot'时间与每个类的内存使用情况相对应,画一条线来总结增长并识别增长最快的对象:

    ^
    |
s   |  Legend:
i   |  *  - data point
z   |  -- - trend
e   |
(   |
b   |                 *
y   |                     --
t   |                  --
e   |             * --    *
s   |           --
)   |       *--      *
    |     --    *
    |  -- *
   --------------------------------------->
                      time
Run Code Online (Sandbox Code Playgroud)

将堆转储(需要多个)转换为一种格式,这样便于堆转储格式的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  
...
Run Code Online (Sandbox Code Playgroud)

到具有每个堆转储的日期时间的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
...
Run Code Online (Sandbox Code Playgroud)

使用此脚本:

# 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);
Run Code Online (Sandbox Code Playgroud)

创建一个表来放入数据

CREATE TABLE heap_histogram (
    histwhen timestamp without time zone NOT NULL,
    class character varying NOT NULL,
    instances integer NOT NULL,
    bytes integer NOT NULL
);
Run Code Online (Sandbox Code Playgroud)

将数据复制到新表中

\COPY heap_histogram FROM 'heap.csv'  WITH DELIMITER ',' CSV ;
Run Code Online (Sandbox Code Playgroud)

针对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
    ;
Run Code Online (Sandbox Code Playgroud)

解释结果:

         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
Run Code Online (Sandbox Code Playgroud)

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

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