对OOM错误和堆分析的深入研究将帮助您确定Java应用程序内存问题的根本原因,并指导您了解GC。
任何使用过基于Java的企业级后端应用程序的软件开发人员都会遇到来自客户或QA工程师的这一臭名昭著或尴尬的错误:java.lang.OutOfMemoryError:Java heap space。
为了理解这一点,我们必须回到计算机科学的基本原理算法的复杂性,特别是“空间”复杂性。如果我们还记得,每个应用程序都有最坏情况下的性能。具体地说,在内存维度中,当这是不可预测的或是尖头的时候,分配给应用程序的内存将超过建议的内存。这会导致分配的堆内存过度使用,从而出现“内存不足”的情况。
这种特定情况最糟糕的部分是应用程序无法恢复并将崩溃。任何重新启动应用程序的尝试-即使有更多的最大内存(-Xmx
选项)-都不是一个长期的解决方案。如果不了解导致堆使用膨胀或峰值的原因,内存使用稳定性(因此应用程序稳定性)就无法保证。那么,对于理解与内存问题相关的编程问题,哪种方法更有条理呢?这可以通过了解应用程序的内存堆和内存不足时的分布来回答。
在这个序曲中,我们将重点关注以下几点:
- 当Java进程内存不足时,从该进程获取堆转储。
- 了解应用程序遇到的内存问题的类型。
- 使用堆分析器分析内存不足问题,特别是在使用这个开源项目:Eclipse MAT 分析内存不足时。
设置应用程序以便进行堆分析
任何不确定的或零星的问题,比如内存不足错误,都是一个挑战,需要对其进行事后分析。因此,处理oom的最佳方法是让JVM在内存用完时转储JVM内存状态的堆文件。
sun hotspotsjvm有一种方法可以指示JVM在内存不足时将其堆状态转储到文件中。此标准格式为.hprof
。因此,要启用此功能,请将XX:+HeapDumpOnOutOfMemoryError
添加到JVM启动选项中。添加此选项对于生产系统至关重要,因为内存不足可能需要很长时间才能发生。此标志为应用程序增加的性能开销很少或没有。
如果必须将heap dump.hprof
文件写入特定的文件系统位置,则将目录路径添加到XX:HeapDumpPath
。只需确保应用程序对此处给定的特定目录路径具有写入权限。
原因分析
了解内存不足错误的性质
当试图评估和理解内存不足错误时,最初步的理解就是内存增长特征。对以下可能性做出结论:
- 使用峰值:根据负载类型,这种类型的OOM可能会非常剧烈。一个应用程序可以在JVM为20个用户分配的内存不足的情况下运行良好。但是如果第100个用户出现峰值,那么它可能已经达到内存峰值,从而导致内存不足错误。解决这个问题有两种可能。
- 泄漏:这是内存使用随着时间的推移而增加的地方,这是由于编程问题造成的问题。
在我们了解了导致使用量激增的内存问题的本质之后,可以使用以下方法来根据堆分析得出的推断来避免碰到OOM错误。
修复内存溢出问题
1. 修复导致OOM的代码:由于应用程序在一段时间内递增地添加了一个对象,而没有清除它的引用(从正在运行的应用程序的对象引用中),因此必须修复编程错误。例如,这可能是一个哈希表,在业务逻辑和事务完成后,不删除业务对象,而是以增量方式插入业务对象。
2. 增加最大内存作为修复:在了解运行时内存特性和堆之后,可能必须增加分配的最大堆内存以避免再次出现OOM错误,因为建议的最大内存不足以保证应用程序的稳定性。因此,应用程序可能需要更新,以使用基于堆分析的评估值具有更高值的Java-Xmx
标志来运行。
堆分析
下面我们将详细探讨如何使用堆分析工具分析堆转储。在我们的例子中,我们将使用Eclipse基金会提供的开源工具MAT。
使用MAT进行堆分析
现在是深度潜水的时候了。我们将介绍一系列步骤,这些步骤将有助于探索MAT的不同特性和视图,从而获得一个OOM堆转储的示例,并对分析进行思考。
1. 打开OOM错误发生时生成的堆(.hprof)。确保将转储文件复制到专用文件夹,因为MAT会创建很多索引文件:file->open
2. 这将打开包含泄漏可疑报告和组件报告选项的转储。选择运行泄漏可疑报告。
3. 当 leak suspect“泄漏可疑”图表打开时,概览窗格中的饼图将按每个对象显示保留内存的分布。它显示内存中最大的对象(具有高保留内存的对象-由它累积的内存以及它引用的对象)。
4. 上面的饼图显示了3个问题疑点,它们聚集了包含最高聚合内存引用(包括shall和retained)的对象。
让我们看看OOM是否是错误的根源:
嫌疑1
454570个java.lang.ref.Finalizer
实例,由“<system class loader>”加载,占用790205576(47.96%)字节。
上面的内容告诉我们,有454570个JVM终结器实例占据了分配的应用程序内存的近50%。
哎呀!基于读者知道Java终结器做什么的基本假设,这让我们理解了什么?
请阅读以下内容:http://stackoverflow.com/questions/2860121/why-do-finalizers-have-a-severe-performance-pension
本质上,开发人员编写了自定义终结器来释放实例所持有的某些资源。终结器收集的这些实例使用单独的队列在jvm gc收集算法的范围之外收集。本质上,这是GC清理的一条较长的路径。所以,现在我们正试图理解这些终结者正在完成什么?
可能是怀疑sun.security.ssl.SSLSocketImpl
,占内存的20%。我们能确认这些实例是否是被终结器清除的实例吗?
嫌疑2
现在,让我们打开Dominator支配者视图,它位于MAT顶部的工具按钮下。我们可以看到按类名列出的所有实例,这些实例按MAT进行解析,并在堆转储中可用。
接下来,在支配者的观点中,我们将试图理解java.lang.Finalizer
以及sun.security.ssl.SSLSocketImpl
。我们右键单击sun.security.ssl.SSLSocketImpl
行并打开到:GC Roots -> exclude soft/weak references。
现在,MAT将开始计算内存图,以显示指向引用此实例的GC根的路径。这将显示另一个页面,显示参考信息如下:
如上面的引用链所示,实例SSLSocketImpl
由来自java.lang.ref.Finalizer
,它本身在该级别上约占保留堆的88k。我们还可以注意到finalizer链是一个带有next指针的链表数据结构。
推断:此时,我们有一个明确的提示,即Java终结器正在尝试收集SSLSocketImpl
对象。为了解释为什么没有收集到这么多,我们开始查看代码。
检验代码
此时需要进行代码检查,以查看套接字/I/O流是否使用finally
子句关闭。在本例中,它显示了与I/O相关的所有流实际上都正确地关闭了。现在,我们怀疑JVM是罪魁祸首。事实上,情况就是这样的:openjdk6.0.XX的GC收集代码中有一个bug。
我希望本文提供一个模型来分析堆转储并推断Java应用程序中的根本原因。
扩展阅读
浅堆ShallowHeap 与 保留堆Retained Heap
浅堆是一个对象消耗的内存。一个对象每个引用需要32或64位(取决于操作系统体系结构),每个整数需要4个字节,每个长度需要8个字节,等等。根据堆转储格式的不同,可以调整大小(例如对齐到8等),以便更好地模拟虚拟机的实际消耗。
保留的X集是垃圾收集X时GC将删除的对象集。
保留堆X是保留X集合中所有对象的浅层大小之和,即X保持活动的内存。
一般来说,一个对象的浅堆就是它在堆中的大小。同一对象的保留大小是对该对象进行垃圾回收时将释放的堆内存量。
前导对象集的保留集,例如特定类的所有对象或由特定类装入器加载的所有类的所有对象,或只是一组任意对象,是在该前导集的所有对象变得不可访问时释放的对象集。保留集包括这些对象以及只能通过这些对象访问的所有其他对象。retained size是保留集中包含的所有对象的总堆大小。
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/1035.html
暂无评论