扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
引言
企业应用程序中的内存泄漏会导致大量的危急情况。付出的代价包括用于进行分析的时间和资金、生产环境中开销巨大的停机时间、压力以及对应用程序和框架丧失信心。
非代表性测试环境、无效的工作负载标识和不充分的测试周期都可能导致在测试过程中不能检测出内存泄漏。公司通常无法或者不愿意投入大量的必要时间和资金来克服这些缺陷。导致这种情况的原因在于教育、文化和财务中的某个方面出现了问题。本文并不尝试解决上述问题,而是侧重介绍帮助解决由这些原因造成的结果的技术解决方案。
本文是入门文章第 1 部分:内存泄漏概述的后续部分。在第 2 部分中,我们将更详细地介绍 WebSphere Application Server V6.1 中的内存泄漏分析和检测功能以及一些实际的案例研究。本文将介绍 WebSphere Application ServerV6.1 中新引入的内存泄漏检测功能和名为 Memory Dump Diagnostic for Java (MDD4J) 的脱机内存泄漏分析工具。结合使用这两项功能可以确定运行于 WebSphere Application Server上的 Java 和 Java 2 Platform Enterprise Edition (J2EE™) 应用程序中内存泄漏的根源。
本文主要面向 Java 开发人员、应用服务器管理员和处理部署在 WebSphere Application Server 上的应用程序的问题确定顾问。
|
什么是 Java 中的内存泄漏?
在 Java 应用程序中,当对象保留对不再需要的对象的引用时将会发生内存泄漏。尽管 Java 虚拟机 (JVM) 具有内置的垃圾收集机制(请参见参考资料),无需程序员负责任何明确的对象重新分配责任,但是此问题仍会阻止自动 Java 垃圾收集进程释放内存。这些内存泄漏问题表现为,随着时间的推移会增加 Java 堆使用量,并在堆完全耗尽时最终导致 OutOfMemoryError 错误。这种类型的内存泄漏称为 Java 堆内存泄漏。
|
碎片和本机内存泄漏
在本机系统资源(如文件句柄、数据库连接构件等)不再使用后,如果未能清理它们,也会造成 Java 中的内存泄漏。这种类型的内存泄漏称为本机内存泄漏。这些类型的内存泄漏表现为,随着时间的推移会增加进程大小,而在 Java 堆使用量方面没有任何增加。
尽管 Java 堆内存泄漏和本机内存泄漏最终都表现为 OutOfMemoryError 错误,但是并不是所有的 OutOfMemoryError 错误都是由 Java 堆泄漏或本机内存泄漏导致的。在压缩过程中,如果 Java 垃圾收集进程无法释放任何连续的可用内存块来存储新对象,则碎片 Java 堆也可能导致 OutOfMemoryError 错误。在这种情况下,不论是否有大量的可用 Java 堆,都可能发生 OutOfMemoryError 错误。在 IBM 的 SDK 1.4.2 版或更低版本中可能发生碎片问题,原因是在 Java 堆中存在 Pinned 对象或 Dosed 对象。Pinned 对象是在堆压缩过程中由于 JNI(Java 本机接口)对其进行访问而无法移动的那些对象。Dosed 对象是在堆压缩过程中由于线程堆栈的引用而无法移动的对象。常常由于频繁分配较大的对象(超过 1 MB)而加剧了碎片问题。
因碎片问题或本机内存泄漏而导致的 OutOfMemoryError 错误不在本文讨论之列。通过观察 Java 堆随时间推移的使用情况可以将本机内存泄漏和碎片问题与 Java 堆内存泄漏区别开来。可以使用 IBM Tivoli® Performance Viewer 和详细的垃圾收集输出(请参见参考资料)进行区分。不断增加的 Java 堆的使用量可导致堆全部耗尽,这指示存在 Java 堆内存泄漏,而对于本机内存泄漏和碎片问题,堆的使用量不随时间的推移表现出明显的增加。对于本机内存泄漏,进程大小将增加,对于碎片问题,在发生 OutOfMemoryError 错误时,会存在大量的可用堆。
|
在 Java 应用程序中导致内存泄漏的常见原因
如上文所述,Java 中内存泄漏(Java 堆内存泄漏)的最常见原因是意外的(由于程序逻辑错误)对象引用,从而在 Java 堆中保留未使用的对象。在这一部分中,将介绍导致 Java 堆内存泄漏的许多常见的程序逻辑错误类型。
无限缓存
内存泄漏的一个非常简单的例子是充当缓存而又无限增长的 java.util.Collection 对象(例如,HashMap)。清单 1 显示了一个简单的 Java 程序,该程序演示了基本内存泄漏数据结构。
清单 1. 将字符串对象泄漏到静态 HashSet 容器对象的 Java 程序示例
public class MyClass { staticHashSet myContainer = new HashSet(); HashSet myContainer = new HashSet(); public void leak(int numObjects) { for (int i = 0; i < numObjects; ++i) { String leakingUnit = new String("this is leaking object: " + i); myContainer.add(leakingUnit); } } public static void main(String[] args) throws Exception { { MyClass myObj = new MyClass(); myObj.leak(100000); // One hundred thousand } System.gc(); } |
在清单 1 所示的 Java 程序中,有一个名为 MyClass 的类,它含有对名为 myContainer 的 HashSet 的静态引用。在 MyClass 类的 Main 方法中,存在一个子范围(以加粗文本显示),其中实例化了 MyClass 类的实例,并调用了其成员操作 leak。这会导致将十万个字符串对象添加到容器 myContainer 中。程序控制退出子范围后,MyClass 对象的实例作为垃圾被收集,因为在子范围的外面没有对 MyClass 对象实例的任何引用。不过,MyClass 类对象具有对称为 myContainer 的成员变量的静态引用。由于此静态引用,即使 MyClass 对象的单一实例以及 HashSet 被作为垃圾收集之后,myContainer HashSet 仍会继续永久保留在 Java 堆中,HashSet 中所有的字符串对象会继续永久保留,并占用 Java 堆的大部分容量,直到程序退出 Main 方法为止。此程序演示的基本内存泄漏操作涉及缓存对象中的无限增长。使用 Singleton 模式实现的大多数缓存都涉及本例所示的对顶级 Cache 类的静态引用。
未调用的侦听器方法
许多内存泄漏是由于程序错误产生的,此类错误导致清除了未调用的方法。在 Java 程序中,侦听器模式是常用的模式,使用此模式可以实现用于清除不再需要的共享资源的方法。例如,J2EE 程序通常依靠 HttpSessionListener 接口及其 sessionDestroyed 回调方法,来清除用户会话到期时在其中存储的任何状态。有时,由于程序逻辑错误,负责调用侦听器的程序可能无法调用它,或者侦听器方法可能因出现异常而无法完成调用,这都可能导致未使用的程序状态即使不再需要也仍保留在 Java 堆中。
无限循环
有些内存泄漏是由于程序错误发生的,在存在此类错误的情况下,应用程序代码中的无限循环会分配新的对象,并将其添加到可从程序循环范围外面访问的数据结构中。这种类型的无限循环有时是由于多线程访问共享的不同步的数据结构造成的。这些类型的内存泄漏表现为内存泄漏快速增长,其中,如果详细的垃圾收集数据显示在很短的时间内可用堆空间急剧减少,则会导致 OutOfMemoryError 错误。对这种类型的内存泄漏情况,分析短时间内获取的堆转储 (heap dump) 非常重要,这样可以观察正在快速减少的可用内存。在标题为案例研究 3 和案例研究 4 的部分中,讨论了对 IBM 支持中涉及无限循环的两种不同内存泄漏情况的分析结果。
虽然通过分析堆转储能够标识内存泄漏数据结构,但是标识无限循环中的内存泄漏代码并不简单。在观察到可用内存快速减少的过程中,通过查找获取的线程转储中的所有线程的线程堆栈,可以标识陷入无限循环的方法。IBM SDK 实现会生成 Java 核心文件以及堆转储。此文件包含所有活动线程的线程堆栈,并能够用于标识可能陷入无限循环的方法和线程。
过多的会话对象
许多 OutOfMemoryError 错误是由于没有为支持最大用户负载而配置适当的最大堆大小导致的。一个简单示例是 J2EE 应用程序,它使用内存中的 HttpSession 对象存储用户会话信息。如果不对内存中可以保持的会话对象的最大数量设置最大限制,那么在用户加载高峰期,可能有许多会话对象。这可导致 OutOfMemoryError 错误,它实际上不是内存泄漏,而是不适当的配置。
|
WebSphere 解决方案
传统的内存泄漏技术基于已知发生了内存泄漏并希望确定内存泄漏的根源这一思想。这些技术各种各样,但通常包括堆转储分析、附加 Java 虚拟机分析器接口(Java Virtual Machine Profiler Interface,JVMPI)或 Java 虚拟机工具接口(Java Virtual Machine Tools Interface,JVMTI)代理,或者使用字节代码插入来跟踪对集合进行的插入和删除操作。尽管这些分析机制有明显的性能影响,并且不适合于在生产环境中使用,但是它们十分完善。
问题
企业应用程序中的内存泄漏会导致大量的危急情况。付出的代价包括用于进行分析的时间和资金、生产环境中开销巨大的停机时间、压力以及对应用程序和框架丧失信心。
典型的分析解决方案应当尝试将应用程序移到一个隔离的测试环境中,在这个环境中应当可以重现问题并进行分析。由于在这些测试环境中重现问题的难度很大,所以相关内存泄漏的成本增加了。
传统的冗余方法,例如集群技术,只能在一定程度上有所帮助。内存泄漏将在集群成员中传播。因为受到影响的应用服务器的响应速率变慢,所以导致工作负载管理技术将请求路由到正常服务器,这可能导致协调的应用服务器崩溃。
成本构成
典型的内存泄漏情况的分析表明,导致企业应用程序中内存泄漏分析成本的主要因素是,在问题变得危急之前未能发现内存泄漏。除非管理员具有大量的技能、时间和资源来监视内存趋势,否则在应用程序性能下降,应用服务器无法响应管理请求之前,用户通常不知道系统已经出现了问题。通常,与内存泄漏相关的成本由以下三个主要方面决定:测试、检测和分析。
WebSphere Application Server 将内存泄漏的检测和分析看作两个不同的问题,并使用两个相关但又相互独立的解决方案来加以解决。遗憾的是,没有简单的技术解决方案可以用于解决与充分测试相关的成本,本文将不讨论这一主题。
将检测与分析分离
传统技术的问题是既尝试进行检测又尝试进行分析。这会导致解决方案的性能低下,或涉及不适合于许多生产环境(如 JVMPI 代理或字节代码插入)的技术或技巧。
通过将检测问题与分析问题隔离,我们能够在 WebSphere Application Server V6.0.2 中提供轻量级的可用于生产环境的内存泄漏检测机制。此解决方案使用开销很小且普遍可用的通用统计信息来监视内存使用趋势,并在早期提供内存泄漏通知。这使管理员有时间准备适当的备份解决方案,并分析问题的根源,而不会存在在测试环境中进行重现所导致的问题,这些问题的代价很高,而且也难以解决。
|
为便于此分析,WebSphere Support 提供了 Memory Dump Diagnosis Tool for Java (MDD4J),这是一个轻量级脱机内存泄漏分析工具,它将多项成熟技术合并到单个用户界面。
为了在检测和分析之间架起一座桥梁,我们在 IBM JDK 中提供了自动化工具,以生成 HeapDump。此机制将生成与足够内存泄漏协调的多个堆转储,以便于使用 MDD4J 进行比较分析。生成 HeapDump 的开销很大;在缺省情况下禁用此功能,在适当的时间,提供 MBean 操作来启用它。
|
内存泄漏检测
WebSphere Application Server 中的轻量级内存泄漏检测是为了在测试和生产环境中提供内存问题的早期检测而设计的。它最大限度地减少了对性能的影响,并且不需要附带其他代理或使用字节代码插入。尽管将它设计为与脱机分析工具(包括 MDD4J)结合使用,但它不是为分析问题的根源而设计的。
算法
如果应用服务器的状态和工作负载稳定,那么内存使用模式应相对稳定。
图 1. 显示内存泄漏应用程序的可用内存(绿)和已用内存(红)的详细垃圾收集图表
启用详细的垃圾收集是在问题确定过程中用于调试内存泄漏的第一步。有关在 IBM Developer Kit 中启用详细垃圾收集的说明,请参考 IBM Developer Kit 的诊断指南(请参见参考资料)。支持人员和类似于支持人员的客户处理垃圾收集统计信息并绘制详细的图表,以便确定是否由于内存泄露而导致了失败。(请参见参考资料)。如果垃圾收集周期之后的可用内存持续降低,那么存在内存泄漏的可能性较高。图 1 中以图表形式说明了存在内存泄漏的应用程序在垃圾收集周期之后的可用内存(该图表使用内部 IBM 工具)。泄漏是显而易见的,但是,如果不积极地监视数据,则在服务器崩溃之前您是不会知道这一情况的。
我们的内存泄漏检测机制通常会自动执行以下过程:即查找垃圾收集周期之后可用内存不断下降的趋势。我们不能假定详细的垃圾收集信息是可用的,而且 JVMPI 对于生产环境而言开销也太大(并且需要附加代理)。因此,我们限定于 PMI 数据,利用此数据,可以通过 Java API 调用来获得可用内存和总体内存的统计信息。详细的垃圾收集在垃圾收集周期之后直接提供了可用内存统计信息,而 PMI 数据不能提供。通过使用分析可用内存统计信息变化的算法,我们可粗略计算垃圾收集周期之后的可用内存。
泄漏可能非常快或令人难以置信地慢,所以我们将分析短时间间隔和长时间间隔的内存趋势。没有设置最短的时间间隔,而是通过可用内存统计信息的变化得出。
由于我们是在生产服务器中运行的,并尝试检测内存泄漏(而不是造成内存泄漏),所以我们只能存储非常有限的数据量。丢弃了不再需要的原始、概要性的数据点,以便使内存占用空间保持最小。
通过在粗略/概要性的可用内存统计信息中查找下降趋势来分析周期。规则的配置指示了适用什么样的严格条件,尽管其配置了一组良好的普遍适用的缺省值。
除了评估垃圾收集周期之后大致的内存下降趋势之外,我们还可以查看垃圾收集之后平均可用内存小于某些阈值的情况。这种情况既可以是内存泄漏的一种标志,也可能表示正在一个资源过少的应用服务器上运行应用程序。
iSeries
OS/400® 或 iSeries 引入了一些独特的方案。iSeries 计算机通常配置了有效的可用内存池大小。这个池大小指示 JVM 可用的内存量。当 Java 堆超过这个值时,将使用 DASD(磁盘)来容纳该堆。此类解决方案会不可避免地导致极差的性能,原因是尽管应用服务器已经崩溃,但它仍保留一定的响应,所以管理员可能不会意识到该问题的存在。
如果将 Java 堆大小扩展到 DASD,则会发出一个警报,通知管理员将发生此事件,指示有效池大小非常小、资源太少或内存泄漏。
扩展堆
Java 堆通常配置了最小和最大的堆大小。当扩展堆时,分析可用内存趋势会非常困难。当扩展堆时,我们应避免任何下降趋势分析,而是监视和识别堆是否很快用尽资源。此过程通过以下方法完成:监视堆大小是否持续增加,垃圾收集周期之后的可用内存是否在堆的某一阈值内,因此是否正在推动堆扩展,预计当前趋势是否继续,若继续,则 JVM 将很快用尽资源。如果确定是这种情况,我们将通知用户潜在的问题,以便他们可以监视该情况或建立应急计划。
HeapDump 生成
许多分析工具(包括 MDD4J)都可以分析堆转储,以找到内存泄漏的根源。在 IBM JDK 中,作为 OutOfMemoryExceptions 的结果生成的 HeapDump 常常用于此类分析。如果您希望更主动一些,则需要在适当时间生成 HeapDump,以便进行分析。这非常重要,因为如果不在适当时间生成 HeapDump,则会导致错误的分析。例如,如果在工作负载的开始生成 HeapDump,则正在填充的缓存常常被标识为内存泄漏。
根据内存泄漏检测机制,WebSphere Application Server 提供了一种工具来基于内存泄漏趋势生成多个堆转储。这可以确保在确定内存泄漏之后获取堆转储,并通过足够的内存泄漏获得最有效的分析结果。
在缺省的情况下,启用自动化的堆转储生成,或在适当时间使用 MBean 操作启动堆转储。
除了自动化的堆转储生成实用工具外,还可以使用 wsadmin(请参阅 WebSphere Application Server 信息中心)或在设置了某些环境变量(请参见参考资料中的诊断指南)后发送 kill -3 信号(在 Unix® 平台上)来手动生成。
优点
管理员能够在测试或生产环境中运行轻量级内存泄漏检测,并可以在早期接收内存泄漏的通知。这使得管理员能够建立应急计划,在问题再次出现时对其进行分析,并在应用程序响应能力丧失或应用服务器崩溃之前诊断结果。
这样,在企业应用程序中,与内存泄漏相关的成本会明显降低。
固有的局限性
内存泄漏检测规则是按照一种简单的理念设计的。它使用直接可用的数据并进行必要的近似评估,以提供存在内存泄漏的可靠通知。由于分析的数据和所需的近似性评估的固有局限性,所以使用更合适的数据和更完善的算法的现有解决方案应该能够实现更精确的结果(但必须以重大的性能损失为代价才能实现)。不过,我们可以这样说,虽然此规则比较简单,但是我们的实现开销很小,可以使用普遍可用的统计信息来检测内存泄漏。
自主管理器集成
可以完全配置轻量级内存泄漏检测,并将其设计为与高级自主管理器或自定义 JMX 客户机交互。IBM WebSphere Extended Deployment 是这种关系的一个例子。
WebSphere Extended Deployment 提取 WebSphere Application Server 拓扑,并适当部署应用程序,以便在维护应用程序性能标准的同时对更改工作负载做出反应。它还可以合并运行状态管理策略。WebSphere Extended Deployment 内存运行状态策略使用 WebSphere Application Server 内存泄漏检测功能来识别应用服务器何时发生内存泄漏。
WebSphere Extended Deployment 提供了许多配置内存泄漏检测的策略。一种示例策略通过取得多个堆转储(使用工作负载管理来维护应用程序的性能)以进行分析,从而对内存泄漏通知做出反应。另一种策略简单地监视应用服务器的内存水平何时达到临界状态,以在应用服务器崩溃之前对其进行重启。
|
用于生产系统的内存泄漏分析
确定 Java 内存泄漏的根源需要以下两个步骤:
确定内存泄漏的位置。确定对象、无意引用、保留这些无意引用的类和对象以及无意引用指向的对象。
确定泄漏发生的原因。确定导致在程序中的适当点不释放这些无意引用的源代码方法(程序逻辑)。
Memory Dump Diagnostic for Java 工具可以帮助确定内存泄漏在应用程序中发生的位置。不过,该工具不能帮助确定导致内存泄漏的错误源代码。借助该工具确定泄漏数据结构类和包之后,您可以使用任何调试器或日志中的特定跟踪语句来确定错误的源代码方法,并对应用程序代码进行必要的更改以解决内存泄漏问题。
分析技术是 CPU、内存和磁盘空间密集型技术。因此,将分析机制实现为脱机工具。此机制特别适合于在生产或压力测试环境中运行的大型应用程序。此工具可用于分析(脱机)手动获取或结合使用轻量级内存泄漏检测生成的这些转储。
Memory Dump Diagnostic for Java 工具旨在适合以下角色以及满足这些相互联系的目的的目标:
系统管理员
哪一个组件正在泄漏(客户应用程序中的数据结构或 WebSphere Application Server 内部的数据结构)?
根据分析,在泄漏候选列表中标识的对象的包名称和类名称可以标识造成内存泄漏的组件,而无需具备深入的应用程序代码知识。
应用程序开发人员
哪些数据结构正在真正地泄漏而不是有效地缓存?
内存泄漏数据结构仅在以下方面与非泄漏数据结构不同:泄漏数据结构的增长大小没有任何限制,而非泄漏数据结构仅在特定的限制范围内增长。使用 MMD4J 提供的工具可以帮助开发人员确认可疑数据结构是真正的内存泄漏还是适当增长的数据结构。
什么正在导致数据结构泄漏?
确定内存泄漏数据结构之后,下一个问题就是哪些类、对象和对象引用会使内存泄漏对象在预期生命周期之后仍保留在内存中?Memory Dump Diagnostic for Java 工具可以帮助在树视图中浏览和导航堆中的所有对象引用,同时树视图会显示任何所选对象的父对象。这有助于识别导致内存泄漏的潜在无意对象引用。
什么数据类型和数据结构会导致占用大量的空间?
在许多情况下,OutOfMemoryError 错误的发生并不是由于内存泄漏,而是由于导致大量消耗 Java 堆的配置问题。在上述情况下,通常需要检测 Java 应用程序的不同组件中对空间的主要占用者。Memory Dump Diagnostic for Java 工具可以帮助识别占用大量 Java 堆的数据结构和这些占用者之间的所属关系。这可以帮助应用程序开发人员了解不同应用程序组件对 Java 堆的使用量。
此工具支持 IBM Portable Heap Dump (.phd)、IBM Text、HPROF Text 和 SVC Heap Dump 格式。有关格式和受支持的 JDK 版本的更详细列表,请参见附录。
技术概述
Memory Dump Diagnostic for Java 工具提供了对 Java 虚拟机(JVM,该虚拟机可以在各种 IBM 和非 IBM 平台上运行 WebSphere Application Server)的普通格式内存转储的分析功能。内存转储分析的目的是在 Java 堆中标识出可能是内存泄漏根源的区域或数据结构集合。该工具以图形格式显示内存转储的内容,同时突出显示标识为可疑内存泄漏的区域。图形用户界面提供浏览功能,以便验证可疑内存泄漏区域和了解组成这些泄漏区域的数据结构。
此工具提供两种主要类型的分析功能:单个内存转储分析和比较分析。
单个转储分析大多数情况下用于由 IBM Developer Kit Java Technology Edition 通过 OutOfMemoryExceptions 异常自动触发的内存转储。这种类型的分析使用启发式方法来标识可疑数据结构,这样的数据结构通常包含一个带有大量子对象的容器对象(例如,带有一组 HashMap$Entry 对象的 HashMap 对象)。这种启发式方法对于检测泄漏 Java 集合对象非常有效,这些对象使用一个内部数组来存储所包含的对象。在大量由 IBM 支持部门处理的内存泄漏案例中,已经发现这种启发式方法非常有效。
除了查找下降趋势可疑点外,单个转储分析还可以在对象引用图(稍后定义)中标识占用 Java 堆空间最大的聚合数据结构。
比较分析用于比较在运行一个内存泄漏应用程序期间(也就是当可用 Java 堆内存减少时)所取得的两个内存转储。出于分析的需要,在运行泄漏应用程序时先触发的内存转储称为基准内存转储。泄漏应用程序运行一段时间并发生泄漏增长之后触发的内存转储称为主内存转储。在内存泄漏的情况下,与基准内存转储相比,主内存转储会包含更多占用大量 Java 堆的对象。为了获得更好的分析结果,建议按照在总体消耗堆大小中大量增长的情况将主内存转储的触发点与基准内存转储的触发点分开。
比较分析确定一组数据结构,它们在许多组成数据类型的实例数目方面快速增长。通过根据类似的所属权(即,指向对象引用图中的对象的引用链)将每个堆转储中的所有对象分成不同类别的区域,来标识这些可疑数据结构。通过对转储中每个对象所属权上下文中的对象数据类型使用模式匹配技术来完成分类。匹配每个转储中的区域并在主转储和基准转储之间进行比较。在比较分析中标识的区域有以下特征:
泄漏容器:保留在内存中具有大量实例的所有对象的对象;例如,清单 1 的内存泄漏示例中的 HashSet 对象。
泄漏单元:在区域内大量增长或存在的代表性对象的对象类型;例如,在清单 1 的内存泄漏示例中保留泄漏的字符串对象的 HashMap$Entry 对象。
泄漏根:这是在堆中保留泄漏容器的对象引用链中的代表性对象。这通常是一个保留静态引用的类对象;例如,清单 1 的内存泄漏示例中的 MyClass 对象。另外,它也可能是一个在内存中保留该对象并且根位于 Java 堆栈或具有本机引用的对象。
所有者链:这是从泄漏根到泄漏单元的对象引用链中的对象集。所有者链中的对象的数据类型和包名称有助于标识导致内存泄漏的应用程序组件。
分析结果显示在基于 Web 的交互式用户界面中,具有以下特性:
案例研究 1:MyClass 内存泄漏示例的内存泄漏分析
在图 2 中对来自 MyClass(清单 1 中显示的示例代码)的一对堆转储的比较分析显示了以下泄漏可疑点。
图 2. MyClass 内存泄漏示例的可疑点
分析结果的 Suspects 选项卡在四个表中列出了内存泄漏可疑点。Data Structures That Contribute Most to Growth 表列出了由上述比较分析技术标识的数据结构。表的每一行标识了一个内存泄漏可疑数据结构:
通常存在多个数据结构可疑点,这些列中有一个可疑点预示着可能存在内存泄漏。在本例中,仅有一个可疑点,此数据结构占主要转储全部堆大小 84%,从这一点可以看出该数据结构非常可疑。
第二个表 Data Structures with Large Drops in Reach Size 列出了主要堆转储上由单个转储分析标识的数据结构。表中的每行标识了一个数据结构,它们都带有一个包含大量子对象的潜在容器对象。第一个表和第二个表都可以指向同一可疑点。如果在第一个表和第二个表中标识的可疑点相关,那么两个表中相应的行会突出显示。
第三个表 Object Types that contribute most to Growth in Heap Size 列出了经历主要转储和基准转储之后实例数大量增长的不同数据类型。这些数据类型没有根据它们的所属权上下文分为不同种类的数据结构或区域;相反,它们是整个堆中增长最快的数据类型。另外,如果特定的数据类型在所选数据结构或区域中有大量的实例,那么该数据类型的行会突出显示。
第四个表 Packages that contribute most to Growth in Heap Size 列出了经历主要转储和基准转储之后实例数大量增长的数据类型的不同 Java 包名称。导致内存泄漏的应用程序组件通常由构成增长区域的数据类型的包名称和类名称标识。此表标识增长最大的可疑包名称,它可以帮助标识导致内存泄漏的应用程序组件。
也可以使用 MDD4J 分析单个堆转储(通常是通过 OutOfMemory 错误自动生成的堆转储)。图 3 显示了仅自动分析清单 1 中示例的主要堆转储时 Suspects 选项卡的结果。
(在这种情况下,Suspects 选项卡中仅有三个表。因为没有执行比较分析,所以没有任何数据结构可疑点。Object Types that contribute most to Heap Size 和 Packages that contribute most to Heap Size 表不显示任何增长统计信息,仅显示主要转储中的实例总数。)
图 3. 清单 1 中的 MyClass 内存泄漏示例的单个转储分析结果
在 Suspects 选项卡中选择数据结构后,您可以访问 Browse 选项卡,查看保留堆中泄漏容器的对象引用链,如图 4 所示。
图 4. 浏览 MyClass 内存泄漏示例的可疑点
在此示例中,可以看到存在从名为 MyClass 的类对象开始,到 HashSet,再到 HashMap,进而再到带有大量 HashMap$Entry 子对象的 HashMap$Entry 对象数组的引用链。每个 HashMap$Entry 对象保留一个字符串对象。它描述了清单 1 所示的内存泄漏示例中创建的数据结构。
此选项卡中的树视图显示了堆转储中的所有对象引用,对象具有多个父对象的情况除外。左面板中的父表显示了树中任何所选对象的所有父对象。可以选择父表中的任何行,将树扩展到所选父对象的位置。左面板还显示了树中任何所选对象的其他详细信息;例如,对象大小、子对象数和总的到达大小等。
图 5. MyClass 内存泄漏示例的所属权上下文
Ownership Context 和 Contents 选项卡可以帮助回答哪些对象是主要转储中堆空间的主要占用者问题(图 5)。它还可以帮助显示标识的主要占用者和每个主要占用者的组成数据类型之间的所属关系。在此示例中,左面板显示的 OwnershipContext 图表中已将 MyClass 节点标识为主要占用者。在右面板中,列出了显著占用此节点的数据类型。在此集合中显示了 HashMap$Entry 对象,在 HashSet 中每个元素都有一个实例。
还可以将分析结果存储在名为 AnalysisResults.txt 的文本文件中。可以通过 Summary 选项卡的链接查看文本分析结果,也可以通过文件系统中的相应分析结果目录进行访问。清单 2 显示了 AnalysisResults.txt 文件的一个片段,该段内容显示了 MyClass 内存泄漏示例的分析结果。
清单 2. MyClass 内存泄漏示例的文本分析结果
Suspected memory leaking regions: Region Key:0,Leak Analysis Type:SINGLE_DUMP_ANALYSIS,Rank:1.0 Region Size:13MB, !RegionSize.DropSize!13MB Owner chain - Dominator tree: MyClass, class0x300c0110, reaches:13MB), LEAK_ROOT |java/util/HashSet, object0x3036d840, reaches:13MB), LEAK_ROOT_TO_CONTAINER |-java/util/HashMap, object0x3036d8a0, reaches:13MB), LEAK_ROOT_TO_CONTAINER |--java/util/HashMap$Entry, array0x30cf0870, reaches:13MB), LEAK_CONTAINER |---Leaking unit: |----java/util/HashMap$Entry, object0x3028ad18, reaches:480 bytes) |----java/util/HashMap$Entry, object have grown by 72203 instances Region Key:2,Leak Analysis Type:COMPARATIVE_ANALYSIS,Rank:1.0 Region Size:12MB, Growth:12MB, 300001 instances Owner chain - Dominator tree: MyClass, class0x300c0110, reaches:13MB), LEAK_ROOT |java/util/HashSet, object0x3036d840, reaches:13MB), LEAK_ROOT_TO_CONTAINER |-java/util/HashMap, object0x3036d8a0, reaches:13MB), LEAK_ROOT_TO_CONTAINER |--java/util/HashMap$Entry, array0x30cf0870, reaches:13MB), LEAK_ROOT_TO_CONTAINER |---java/util/HashMap$Entry, object0x30e88898, reaches:256 bytes), LEAK_CONTAINER |----Leaking unit: |-----java/util/HashMap$Entry, object have grown by 1 instances |
案例研究 2:由于未调用的侦听器回调方法而导致的内存泄漏的分析结果
图 6 显示了内存泄漏情况的 Explore Context 和 Contents 选项卡,涉及的内容是在 IBM WebSphere Portal 中进行系统测试过程中发现的缺陷。此缺陷的发生是因为在使会话对象失效时,存在某个未被清除的会话状态。此示例中的 Ownership Context 图表显示 MemorySessionContext 节点是堆空间的最大占用者,这是预期的结果,因为 MemorySessionContext 是存储所有内存中会话数据的 WebSphere 对象。
图 6. 涉及未调用的侦听器方法导致的内存泄漏的所属权上下文
要更准确地找到内存泄漏的根源,需要查看图 7 中的 Browse 选项卡,从此选项卡中可以看到有大量的 LayoutModelContainer 对象,它们是存储在用户会话中的 WebSphere Portal Server 对象。通过仔细查看数据结构和 LayoutModelContainer 对象数,可以推断当不再需要 LayoutModelContainer 对象时,并没有删除它们。因此,可以推断没有正确地调用会话无效侦听器代码。稍后会发现,此问题的根源是,当提供多个克隆时,存在与会话失效相关的 WebSphere Application Server 错误,这才是此问题的根源。此问题随后会很快得到解决。
图 7. 存在泄漏的 WebSphere Portal LayoutModelContainer 对象的浏览视图
案例研究 3:因无限循环导致的内存泄漏的分析结果
图 8 中的 Suspects 选项卡显示了从涉及无限循环的内存泄漏案例中获取的两个堆转储的分析结果。详细的垃圾收集日志中表现的症状显示,在很短的时间内可用的堆空闲空间下降得特别快。分析在可用堆下降期间获取的堆转储对了解问题的根源至关重要。OutOfMemoryError 堆转储中没有内存泄漏数据结构,因为内存泄漏数据结构根植于 Java 堆栈中,在生成堆转储之前无法滚动。
从 Suspects 选项卡可以看出,存在大量来自 org.apache.poi.usermodel 包的对象实例和异常多的 org.apache.poi.usermodel.HSSFRow 类的实例。
图 8. Apache Jakarta POI 应用程序中的内存泄漏可疑点
图 9 显示了此分析结果的 Browse 选项卡。从图中可以看出,有一个从类型 org.apache.poi.hssf.usermodel.HSSFWorkbook 的对象开始的引用链,它具有一个包含大量(20,431 个)HSSFSheet 对象的 ArrayList。
图 9. 浏览存在泄漏的 Apache Jakarta POI HSSFSheet 对象
进一步分析发现,HSSFSheet 对象是在陷入无限循环的方法中创建的,并被添加到从 Java 堆栈引用的 HSSFWorkbook 对象中。在同一时间作为主要堆转储获取的线程转储显示了两个 Java 线程堆栈,它们使用创建 HSSFSheet 对象的同一方法。对 Java 源代码(来自开放源代码 Apache 项目的 Jakarta POI)的检测揭示了一些不同步的多线程代码访问模式,此类缺陷在后续版本中得到了修复。通过此分析,可以缩小 Jakarta POI 应用程序组件内存泄漏根源的范围。
案例研究 4:由于无限循环导致的内存泄漏示例
图 10 显示了由于开放源代码应用程序组件 com.jspsmart.upload.SmartUpload 导致的另一个内存泄漏示例。从左面板中,您可以看到有异常多的 com.jspsmart.upload.File 对象,它们都指向 SmartUpload 对象。
图 10. 浏览存在内存泄漏的 jspsmart SmartUpload File 对象
案例研究 5:涉及大量 JMSConnection 对象的内存泄漏的分析结果
图 11 显示了涉及大量 JMSTopicConnection 对象的内存泄漏情况。
图 11. 显示存在泄漏的 JMS 连接构件的可疑点
图 12. 存在泄漏的 JMS 连接对象的所属权上下文
从图 12 中的 Ownership Context 图表可以看到,这些 JMSTopicConnnectionHandle 对象由含有 PartnerLogData 类的另一个重要的 Java 堆占用者节点拥有。此外,PartnerLogData 类还有异常多的 XARecoveryWrapper 对象。更进一步的调查揭示,是由于存在 WebSphere Application Server 错误而导致未使用的 XARecoveryWrapper 对象保留在内存中。这些 XARecoverWrapper 对象接着又在 Java 堆中存放大量的 JMSTopicConnection 对象。这些 JMSTopicConnection 对象也占用了大量的本机堆资源。因此,此问题的根源就是由于 Java 堆被占用而造成本机内存泄漏。
案例研究 6:显示实际没有泄漏的 WebSphere 对象的分析结果
图 13. WebSphere 中的内存中 HTTP 会话构件
图 13 显示了指向 WebSphere MemorySessionContext 对象的内存泄漏分析可疑点。MemorySessionContext 对象具有指向 com.ibm.ws.webcontainer.httpsession.SessionSimpleHashMap 的引用,后者会导致生成 .ibm.ws.webcontainer.httpsession.MemorySessionData 对象的实例。这些对象是内存中 HTTP 会话对象的 WebSphere Application Server 实现。这些对象在 J2EE 应用程序堆中可能大量出现,该堆可以使用 HTTP 会话将用户会话存储在内存中。此类对象并不总是意味着内存泄漏。存在大量的此类对象意味着当前有太多的会话处于活动状态,由于这样会加重用户负载和增加 OutOfMemory 错误机会,所以在这种情况下,可以通过增加最大堆大小或者对任何时候可以在内存中保持的最大活动会话数设置限制来避开此问题。存在大量的此类对象还意味着更深一层的内存泄漏,即由这些会话对象保留的应用程序对象实际上正在泄漏。所以,当在分析结果中显示特定于 WebSphere 的类时,就可以假定在 WebSphere Application Server 中存在内存泄漏。
|
可用分析工具比较
目前,市场上提供两种 Java 内存泄漏检测工具。第一类工具是脱机工具,其附加到运行的应用程序,并通过检测 Java 应用程序或检测 JDK 实现本身从该应用程序获取 Java 堆信息。这种例子有 Wily LeakHunter、Borland Optimizeit、JProbe Weblogic Memory Leak Detector 等。尽管这些工具可以标识在一段时间内数量增加的个别类型的对象,但是它们不能帮助标识这些对象所属的复合数据结构。要了解内存泄漏的根源,不仅需要在较低粒度上标识别个别泄漏对象,而且还要确定泄漏对象上有什么,并在较高粒度上查找导致内存泄漏的整个数据结构。此外,在其中一些工具中使用的概要分析技术还增加了正常的应用程序处理时间的开销,这使它们不适用于生产使用情况。而且,应用程序检测技术还会更改应用程序的行为,这可能也是不希望的。
另一组工具包括 SUN Microsystems® 的 HAT 工具,它也可以分析 Java 堆转储。HAT 工具会对在单个转储中具有大量实例的数据类型生成统计信息,并且还可以比较两个堆转储,以标识在数量上增加的数据类型。不过,该工具缺少对泄漏数据结构的描述。
HeapRoots 是一种用于分析 IBM JDK 堆转储的基于控制台的实验性工具,它类似于 HAT 工具,但是它不能查明内存泄漏的根源。Memory Dump Diagnostic for Java (MDD4J) 改进了 HeapRoots 提供的基本分析功能,增加了比较转储分析和单一转储分析,可以检测导致内存泄漏的根源,并且还提供了交互式图形用户界面。
优点和局限性
可扩展、低开销的内存泄漏分析工具的匮乏导致不能很好地在生产环境和压力测试环境中处理内存泄漏问题。
MDD4J 工具就是为解决这个问题而设计的。通过在运行于 IBM Support Assistant 进程的 MDD4J 工具中脱机分析堆转储,可以使资源密集型模式匹配算法适用于以比较的方式分析转储,进而检测内存泄漏的根源。这些模式匹配算法可以寻找并标识在内存转储之间增长最快的聚合数据结构(按照类似的所属权结构组合在一起)。此方法不仅可以标识有增长现象的低级对象,而且还可以标识各种泄漏对象所属的高级数据结构。这有助于回答以下问题:除了到处存在的低级别对象(如字符串)之外,在较高粒度级别还泄漏了什么内容?
此外,该工具还提供了占用空间分析,它可以大致标识一组对 Java 堆大小的主要占用者、它们的所属关系及其重要组成数据类型。所属关系以及浏览功能还有助于回答以下问题:是由于什么保留在内存中的泄漏对象上而导致了泄漏?所属权上下文中的数据类型和内容还有助于确定整个泄漏应用程序中某个特定高级别组件的不足。这有助于将责任分配到正确的开发团队,以便进行详细分析。
还需要指出的重要一点是,该工具仅指出了可疑点,这些可疑点可能是也可能不是真正的内存泄漏点。这是因为对象的泄漏数据结构和有效对象缓存通常不好区别。
另一个不足是,该工具不能标识导致发生内存泄漏的内存泄漏应用程序中的源代码。要提供此信息,需要为每个对象分配捕获分配堆栈跟踪,此开销很大,并且在许多格式的内存转储中不可用。
|
结束语
与轻量级内存泄漏转储检测结合使用时,Memory Dump Diagnostic for Java 工具提供了一个完整的生产系统,它将生产环境中早期通知的好处与一流的脱机分析结果结合在一起。
|
附录:Supported HeapDump 格式和 JVM 版本
Memory Dump Diagnostic for Java 工具支持内存转储的以下格式:
表 1 列出了承载 WebSphere Application Server JVM 的不同平台上支持的 WebSphere 版本、JDK 版本和 Java 内存转储格式。
表 1. 适用的平台和版本
平台 | WebSphere 版本 | JDK 版本 | Java 内存转储格式 |
---|---|---|---|
AIX®、Windows®、Linux® |
6.1 | IBM J9 SDK 1.5 | Portable Heap Dump (PHD) |
IBM J9 SDK 1.5(64 位) |
Portable Heap Dump (PHD) | ||
6.0.2 |
IBM J9 SDK 1.4.2(64 位) |
Portable Heap Dump (PHD) | |
6.0 - 6.0.2 |
IBM SDK 1.4.2 |
Portable Heap Dump (PHD) | |
5.1 |
IBM SDK 1.4.1 |
IBM Text Heap Dump | |
5.0 - 5.0.2 |
IBM SDK 1.3 |
IBM Text Heap Dump | |
4.0 |
IBM SDK 1.3 |
IBM Text Heap Dump | |
Solaris®、HP® |
6.1 | SUN JDK 1.5 | HPROF(ASCII) |
SUN JDK 1.5(64 位) |
HPROF(ASCII) | ||
6.0.2 |
SUN JDK 1.4.2(64 位) |
HPROF(ASCII) | |
6.0 - 6.02 |
SUN JDK 1.4 |
HPROF(ASCII) | |
5.1 |
SUN JDK 1.4 |
HPROF(ASCII) | |
5.0 - 5.0.2 |
SUN JDK 1.3 |
HPROF(ASCII) | |
4.0 |
SUN JDK 1.3 |
HPROF(ASCII) | |
z/OS® |
6.1 | IBM J9 SDK 1.5 | SVC/PHD |
IBM J9 SDK 1.5(64 位) |
SVC/PHD | ||
6.0.2 |
IBM SDK 1.4.2(64 位) |
SVC/PHD | |
6.0 - 6.0.2 |
IBM SDK 1.4.2 |
SVC/PHD | |
5.1 |
IBM SDK 1.4.1 |
SVC | |
5.0-5.02 |
IBM SDK 1.3 |
SVC | |
OS/400 |
6.1 |
IBM SDK 1.5 |
PHD |
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者