35

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

关于我们的情况:

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

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

4

8 回答 8

65

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

此外,如果不知道课程为何存在,您就无法知道是否存在泄漏。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

于 2010-03-24T21:56:40.170 回答
13

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

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

于 2010-03-24T20:58:57.693 回答
8

这个答案扩展了@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 一起创建。

于 2016-10-28T16:01:16.230 回答
3

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

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

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

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

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

于 2010-03-24T21:08:27.790 回答
2

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

于 2010-03-24T21:39:29.717 回答
2

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

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

捕获 Java 堆转储的 7 个选项

于 2019-06-24T05:48:40.010 回答
1

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

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

于 2010-03-24T20:59:24.073 回答
0

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

于 2010-03-24T21:50:56.420 回答