我如何找出未释放对象的原因?
我们的程序之一有时会在一个用户的计算机上出现" 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中产生一堆字符串