我如何找出未释放对象的原因?

时间:2020-03-06 14:56:20  来源:igfitidea点击:

我们的程序之一有时会在一个用户的计算机上出现" OutOfMemory"错误,但是当我测试该错误时当然不会。我只是使用JProfiler(因为我从未使用过,所以使用了10天的评估许可证)运行了它,并过滤了我们的代码前缀,所以在总大小和实例数上最大的块是8000+个特定简单类的实例。

我单击了JProfiler上的"垃圾收集"按钮,大多数其他类的实例都消失了,但是这些特殊的实例却没有。我仍然在同一实例中再次运行测试,它创建了该类的4000多个实例,但是当我单击"垃圾收集"时,那些实例消失了,剩下8000多个原始实例。

这些实例确实在各个阶段都陷入了各种Collection中。我假设它们不是被垃圾收集的事实,这意味着某些内容正在保留对集合之一的引用,从而保留了对对象的引用。

关于如何找出参考的任何建议?我正在寻找有关在代码中查找内容的建议,以及在JProfiler中找到此内容的方法。

解决方案

注意静态容器。只要加载了类,静态容器中的任何对象都将保留。

编辑:删除了有关WeakReference的错误注释。

如果我们使用垃圾收集语言收到OOM错误,则通常意味着收集器没有处理一些内存。也许对象拥有非Java资源?如果是这样,那么即使没有足够及时地收集Java对象,他们也应具有某种"关闭"方法以确保释放资源。

一个显而易见的候选对象是带有终结器的对象。他们可以在调用finalize方法时徘徊。需要先收集它们,然后将其完成(通常仅使用一个终结器线程),然后再次进行收集。

还应注意,尽管gc收集了足够的内存,尽管实际上有足够的内存来创建对象请求,但我们可以得到一个OOME。否则性能会磨碎。

那里没有灵丹妙药,我们必须使用探查器来识别包含那些不需要的对象的集合,并在代码中找到应该删除它们的位置。正如JesperE所说,静态集合是第一个要研究的地方。

我会在类中查看Collections(尤其是静态的Collections)(HashMaps是一个不错的起点)。以下面的代码为例:

Map<String, Object> map = new HashMap<String, Object>(); // 1 Object
String name = "test";             // 2 Objects
Object o = new Object();          // 3 Objects
map.put(name, o);                 // 3 Objects, 2 of which have 2 references to them

o = null;                         // The objects are still being
name = null;                      // referenced by the HashMap and won't be GC'd

System.gc();                      // Nothing is deleted.

Object test = map.get("test");    // Returns o
test = null;

map.remove("test");               // Now we're down to just the HashMap in memory
                                  // o, name and test can all be GC'd

只要HashMap或者其他某个集合具有对该对象的引用,就不会对其进行垃圾回收。

我刚刚读了一篇关于此的文章,但对不起,我不记得在哪里了。我认为可能已经在"有效Java"一书中了。如果找到参考,我将更新答案。

它概述了两个重要的课程:

1)最终方法告诉gc剔除对象时该怎么做,但是它没有要求它这样做,也没有要求它这样做的方法。

2)被遗忘的引用是当今在非托管内存环境中"内存泄漏"的等效形式。如果在完成操作时未将对对象的所有引用都设置为null,则该对象将永远不会被剔除。在实现我们自己的Collection类型或者管理Collection的包装器时,这是最重要的。如果我们有池,堆栈或者队列,并且在从集合中"删除"对象时未将存储桶设置为null,则该对象所在的存储桶将使该对象保持活动状态,直到将该存储桶设置为引用另一个对象。

免责声明:我知道其他答案也提到了这一点,但我想提供更多细节。

尝试使用Eclipse Memory Analyzer。它将为我们显示每个对象与GC根的连接方式,该对象不是垃圾回收的对象,因为该对象由JVM保留。

有关Eclipse MAT工作原理的更多信息,请参见http://dev.eclipse.org/blogs/memoryanalyzer/2008/05/27/automated-heap-dump-analysis-finding-memory-leaks-with-one-click/。

转储并检查堆。

我敢肯定有多种方法可以做到这一点,但这是一个简单的方法。该说明适用于MS Windows,但是可以在其他操作系统上执行类似的步骤。

  • 如果尚未安装JDK,请安装它。它带有许多简洁的工具。
  • 启动应用程序。
  • 打开任务管理器,然后找到java.exe(或者正在使用的任何可执行文件)的进程ID(PID)。如果默认情况下未显示PID,请使用"视图">"选择列..."添加它们。
  • 使用jmap转储堆。
  • 在生成的文件上启动jhat服务器,然后将浏览器打开到http:// localhost:7000(默认端口为7000)。现在,我们可以浏览感兴趣的类型以及诸如实例数,引用实例的信息等信息。

这是一个例子:

C:\dump>jmap -dump:format=b,file=heap.bin 3552

C:\dump>jhat heap.bin
Reading from heap.bin...
Dump file created Tue Sep 30 19:46:23 BST 2008
Snapshot read, resolving...
Resolving 35484 objects...
Chasing references, expect 7 dots.......
Eliminating duplicate references.......
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

为了解释这一点,了解Java使用的某些数组类型命名法(例如了解该类[Ljava.lang.Object;实际上是指Object []类型的对象。

我已经使用Yourkit Java profiler(http://www.yourkit.com)在Java 1.5上进行了性能优化。它有一节介绍如何处理内存泄漏。我觉得这很有用。

http://www.yourkit.com/docs/75/help/performance_problems/memory_leaks/index.jsp

我们可以获取15天的评估:http://www.yourkit.com/download/yjp-7.5.7.exe

BR
〜A

已经提到收藏。另一个很难找到的位置是如果我们使用多个ClassLoader,因为在所有引用都消失之前,旧的classloader可能无法被垃圾回收。

还要检查静力学,这些都是令人讨厌的。日志记录框架可以保持开放状态,从而可以将引用保留在自定义添加程序中。

我们解决问题了吗?

一些建议:

  • 无限地图用作缓存,尤其是静态时
  • 服务器应用中的ThreadLocals,因为线程通常不会死,所以不会释放ThreadLocal
  • 内部字符串(Strings.intern()),这会在PermSpace中产生一堆字符串