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

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

关于我们的情况:

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

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

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

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

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

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

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

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

jmap -histo:live  > dump.out 

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

我每隔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类。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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