Java集合中可变键导致的内存泄漏问题解析
Java集合组件(如Map、List、Set)在我们的应用程序中广泛使用。当它们的键没有被正确处理时,会导致内存泄漏。在这篇文章中,我们将讨论错误处理的HashMap键如何导致OutOfMemoryError。我们还将讨论如何有效诊断此类问题并修复它们。
HashMap内存泄漏
以下是一个示例程序,模拟了由于键被修改而导致的HashMap内存泄漏:
|
|
在继续阅读之前,请花点时间仔细查看上面的程序。
在第5行,定义了’User’类,其中’name’作为成员/实例变量。这个类有一个基于’name’变量的合法’hashCode()‘和’equals()‘方法实现。
在第27行,这个程序进入一个无限循环(即’while(true)’)并创建新的’User’对象。
在第29行,‘User’对象的’name’变量被设置为值’JackX’。
在第30行,‘User’对象被添加到’HashMap’中。
在第33行,用户对象的’name’被改为’Jack & JillX’。基本上,‘HashMap’的键被修改(即更改)了。
在第36行,‘JackX’用户记录被删除,在第37行’Jack & JillX’用户记录从’HashMap’中删除。但这两个删除操作都会静默失败,即用户对象不会从’HashMap’中移除。因此,当程序执行时,HashMap将开始无限增长用户记录,最终导致’java.lang.OutOfMemoryError: Java heap space’。
为什么可变键会导致OutOfMemoryError?
为了理解为什么上述程序会导致OutOfMemoryError,我们需要了解HashMap的实现方式。简而言之:
- HashMap内部包含一个桶数组。每个桶内部都有一个记录列表。
- HashMap使用键对象的’hashcode()‘方法来确定记录应该存储在哪个桶中。一旦确定了桶,记录将被放置在该桶的适当列表中。
- 当我们使用’get()‘方法检索记录时,HashMap使用键对象的相同’hashcode()‘方法来确定应该搜索记录的桶。一旦确定了桶,就会在该桶列表中的所有记录键上调用’equals()‘方法以检索适当的记录。
有了这些知识,让我们讨论当第一个’Jack1’记录插入到’HashMap’时会发生什么。基于User对象中的’hashcode()‘实现,假设’Jack1’记录被插入到桶#1的列表中。一旦记录被存储,实际名称在’HashMap’中被更改为’Jack & Jill1’。因此,在插入之后,桶#1中的用户记录包含’Jack & Jill1’作为键,而不是’Jack1’。
现在让我们回答这个问题:为什么’map.remove(new User(“Jack” + count))‘不能删除记录?
基于这个’Jack1’用户对象的’hascode()‘实现,HashMap将确定记录存储在桶#1中。
现在HashMap将在桶#1列表中的所有键上调用’equals()‘操作。’equals()‘操作将返回’false’,因为列表中这个用户对象的实际名称是’Jack & Jill1’而不是’Jack1’。
现在让我们回答这个问题:为什么’map.remove(new User(“Jack & Jill” + count))‘不能删除记录?
‘Jack & Jill1’的’hashcode()‘实现将返回一个不同的值,这将导致HashMap在不同的桶(比如桶3)中查找记录。
由于在桶#3中不存在记录,它不会被从HashMap中移除。
很棘手,不是吗?
如何诊断由可变键创建的内存泄漏?
您需要按照本文中强调的步骤来诊断OutOfMemoryError: Java Heap Space。简而言之,您需要:
1. 捕获堆转储
您需要在JVM抛出OutOfMemoryError之前从应用程序捕获堆转储。本文讨论了捕获堆转储的八种选项。您可以选择适合您需求的选项。我最喜欢的选项是在启动时向应用程序传递’-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<FILE_PATH_LOCATION>’ JVM参数。示例:
|
|
当您传递上述参数时,JVM将在抛出OutOfMemoryError时生成堆转储并将其写入’/opt/tmp/heapdump.hprof’文件。
2. 分析堆转储
一旦捕获了堆转储,您需要分析转储。在下一节中,我们将讨论如何进行堆转储分析。
堆转储分析
堆转储可以通过各种堆转储分析工具(如HeapHero、JHat、JVisualVM等)进行分析。在这里,让我们使用HeapHero工具分析从此程序捕获的堆转储。
HeapHero工具内部利用机器学习算法来检测堆转储中是否存在任何内存泄漏模式。上面的截图来自堆转储分析报告,标记了一个警告,即’main’线程的局部变量占用了99.92%的内存,并且大多数对象都占用在一个’HashMap’实例中。这是一个强烈的迹象,表明应用程序正在遭受内存泄漏,并且它源自’java.util.HashMap’对象。
HeapHero分析报告中的"最大对象"部分显示了所有内存消耗最大的对象(参考上面的截图)。在这里,您可以清楚地注意到’main’线程占用了99.92%的内存。
该工具还提供了深入调查对象内容的能力。当您深入调查"最大对象"部分中报告的’main’ Thread对象时,您可以看到它的所有子对象。从上图可以看出,它包含338万个User记录。基本上,这些是被添加但从未从HashMap中移除的对象。因此,该工具帮助您指出内存泄漏对象及其起源源,这使得故障排除变得更加容易。
如何修复可变键内存泄漏
您可以将记录的键声明为final,以便在初始化后无法更改。示例:
|
|
结论
从这篇文章中,我们可以理解集合中的变异键有可能导致整个应用程序崩溃。因此,通过不改变键并使用像HeapHero这样的工具进行更快的根本原因分析,您可以保护您的应用程序免受难以检测的中断影响。