摘要
尽管Java虚拟机(JVM)及其垃圾收集器(GC)负责管理大多数内存任务,但是Java软件程序中仍然可能存在内存泄漏。实际上,这是大型项目中的常见问题。避免内存泄漏的第一步是弄清楚它是如何发生的。本文介绍了一些用于编写Java代码的常见内存泄漏陷阱,以及一些用于编写非泄漏代码的最佳实践。一旦发生内存泄漏,很难指出导致泄漏的代码。因此,本文还介绍了一种用于诊断泄漏并查明根本原因的新工具。该工具的开销非常小,因此可用于查找生产中系统中的内存泄漏。
垃圾收集器的作用
尽管垃圾收集器处理了大多数内存管理问题,从而使程序员的生活更轻松,但程序员仍可能会犯错并导致内存问题。简而言之,GC循环跟踪“根”对象(堆栈对象,静态对象,JNI句柄指向的对象等)中的所有引用,并将其可以到达的所有对象标记为活动对象。该程序只能操作这些对象;其他所有对象均被删除。因为GC使程序无法到达已删除的对象,所以这样做是安全的。
尽管可以说内存管理是自动化的,但是它并不能使程序员不必考虑内存管理问题。例如,分配(和释放)内存总会产生开销,尽管这种开销对于程序员而言是不可见的。创建太多对象的程序将比完成相同功能但创建更少对象(在相同条件下)的程序慢。
此内。确切的范围可能难以计算,因为高速缓存中的对象不断变化,并且它们的引用无所不包。为缓存设置正确的大小是一项非常复杂的任务,需要在已用内存量和数据检索速度之间取得平衡。
解决此问题的另一种方法是使用java.lang.ref.SoftReference类跟踪高速缓存中的对象。此方法可确保在虚拟机内存不足且需要更多堆时可以删除这些引用。
ClassLoader
Java ClassLoader结构的使用为内存泄漏提供了许多机会。正是结构本身的复杂性使得ClassLoader在内存泄漏方面遇到了很多问题。关于ClassLoader的特殊之处在于,它不仅涉及“常规”对象引用,还涉及元对象引用,例如字段,方法和类。这意味着只要存在对字段,方法,类或ClassLoader对象的引用,ClassLoader将驻留在JVM中。由于ClassLoader本身可以关联许多类及其静态字段,因此会泄漏大量内存。
确定泄漏的位置
通常发生内存泄漏的第一个迹象是应用程序中的OutOfMemoryError。这通常发生在您几乎不希望发生的生产环境中,而调试几乎是不可能的。可能是因为测试环境以与生产系统不完全相同的方式运行应用程序,这导致泄漏仅出现在生产中。在这种情况下,您需要使用一些较便宜的工具来监视和查找内存泄漏。您还需要能够将这些工具连接到正在运行的系统,而无需重新启动系统或修改代码。也许最重要的是,在执行分析时,您需要能够断开工具的连接,同时保持系统不受干扰。
尽管OutOfMemoryError通常是内存泄漏的信号,但应用程序实际上可能正在使用太多内存;对于后者,必须增加JVM可用的堆数量,或者必须对应用程序进行一些更改以使其使用更少的内存。但是,在许多情况下,OutOfMemoryError是内存泄漏的信号。一种发现的方法是连续监视GC的活动,以确定内存使用量是否随着时间增加。如果是这样,则可能发生了内存泄漏。
详细输出
有很多方法可以监视垃圾收集器的活动。也许使用最广泛的是使用-Xverbose:gc选项启动JVM并观察输出。
[内存] 1 0. 109-1 0. 235:GC 65536K-> 16788K(65536K),12 6. 000 ms
箭头后面的值(在此示例中为16788K)是用于垃圾回收的堆的容量。

控制面板
查看GC连续详细统计信息的输出将非常繁琐。幸运的是,有用于此的工具。 JRockit管理控制台可以显示堆使用情况的图表。借助此图,可以轻松查看堆使用率是否随着时间增加。
图1. JRockit管理控制台
甚至可以配置管理控制台,以便在发生过多堆使用(或基于其他事件)时,该控制台可以向您发送电子邮件。显然,这使检查内存泄漏变得更加容易。
内存泄漏检测工具
还有其他专用于内存泄漏检测的工具。 JRockit内存泄漏检测器可用于查看内存泄漏,并且可以更深入地找出泄漏源。该功能强大的工具紧密集成到JRockit JVM中,其开销非常小,并且对虚拟机堆的访问也很容易。
工具的优势
一旦知道确实发生了内存泄漏,就需要更的工具来找出发生泄漏的原因。 JVM本身不会告诉您。这些工具基本上有两种从JVM获取内存系统信息的方法:JVMTI和字节码检测。 Java虚拟机工具接口(JVMTI)及其前身Java虚拟机概要分析接口(JVMPI)是用于外部工具与JVM通信并从JVM收集信息的标准化接口。字节码技术是指使用检测器处理字节码以获得工具所需信息的技术。
对于内存泄漏检测,这两种技术有两个缺点,这使得它们不太适合生产环境。首先,它们在内存占用和性能降低方面的开销不容忽视。必须以某种方式从JVM导出有关堆使用情况的信息,并将其收集到该工具中进行处理。这意味着为工具分配内存。信息的导出也会影响JVM的性能。例如,在收集信息时,垃圾收集器的运行速度会变慢。另一个缺点是需要始终将工具连接到JVM。这是不可能的:将工具连接到已经启动的JVM,进行分析,断开工具的连接,并保持JVM的运行。
因为JRockit内存泄漏检测器已集成到JVM中,所以没有这两个缺点。首先,在JVM内部执行大量处理和分析工作,因此无需转换或重新创建任何数据。处理也可以背负在垃圾收集器本身上,这意味着提高了速度。其次,只要使用-Xmanagement选项启动JVM(允许通过远程JMX接口监视和管理JVM),就可以将内存泄漏检测器与正在运行的JVM连接或断开连接。断开该工具的连接后,JVM中什么也没有剩下,并且JVM会像连接该工具之前一样全速运行代码。
趋势分析
让我们仔细看看该工具及其如何用于跟踪内存泄漏。知道发生了内存泄漏之后,第一步就是找出正在泄漏什么数据—导致泄漏的是哪类对象? JRockit内存泄漏检测器通过对每个垃圾收集期间每个类的现有对象进行计数来实现此步骤。如果特定类的对象数随时间(“增长率”)增长,则可能发生内存泄漏。
图2.内存泄漏检测器的趋势分析视图
由于泄漏可能很小,因此趋势分析必须运行很长时间。在短时间内,可能会发生某些类型的局部增长,然后它们将下降。但是趋势分析的开销非常小(最大的开销是每次垃圾回收时将数据包从JRockit发送到Memory Leak Detector)。对于任何系统,开销都不应该成为问题,即使是全速运行的生产系统。
起初,数字将继续上升,但经过一段时间后,它们将稳定下来并显示正在增长的班级。
找到根本原因
有时候,知道哪些对象正在泄漏就足够了。这些类只能在代码的非常有限的部分中使用,并且快速检查代码可以发现问题。不幸的是,这种信息很可能还不够。例如,通常会在类java.lang.String的对象上泄漏,但是由于在整个程序中都使用了字符串,因此并没有太大帮助。
我们想知道的是,还有哪些其他对象与泄漏的对象相关联?在这种情况下,它是字符串。为什么泄漏的对象仍然存在?哪些对象保留对这些对象的引用?但是所有列出的保留了对String的引用的对象将是如此之多,以至于它们没有实际用途。为了限制数据量,可以将数据分为几类,以便您可以查看哪些其他对象的类与泄漏的对象(字符串)相关联。例如,字符串在哈希表中非常常见,因此我们可能会看到与字符串关联的哈希表数据项对象。与Hashtable数据项相反,我们终于可以找到与这些数据项相关的Hashtable对象和字符串(如图3所示)。

图片3.在工具中看到的类型图示例视图
反向推动
因为我们仍然将对象视为类的对象,而不是单个对象,所以我们不知道哪个Hashtable在泄漏。如果我们能弄清系统中所有哈希表的大小,我们可以假设最大的哈希表就是正在泄漏的哈希表(因为它会累积泄漏并随着时间的推移而变得很大)。因此,列出所有Hashtable对象及其引用的数据量将有助于我们指出导致泄漏的确切Hashtabl。
图4.界面:哈希表对象及其引用的数据数量列表
对象参考数据的数量的计算成本非常大(有必要以对象为根遍历参考图)。如果必须对许多对象执行此操作,则将花费大量时间。如果您了解一点Hashtable的内部实现原理,则可以找到一条捷径。 Hashtable中有一个Hashtable数据项数组。数组随着哈希表中对象数的增长而增长。因此,为了找到最大的哈希表,我们只需要找到引用哈希表数据项的最大数组。这要快得多。
图5.界面:最大的Hashtable数据项数组及其大小的列表
走远
当我们找到泄漏的Hashtable实例时,我们可以看到哪些其他实例正在引用该Hashtable,然后回退以查看哪个Hashtable在泄漏。
图片6.这是工具中的示例图片
例如,哈希表可以在名为activeSessions的字段中由MyServer类型的对象引用。这些信息通常足以查找源代码以查找问题。
图7.检查对象及其对其他对象的引用
找出分配位置
跟踪内存泄漏时,查看分配对象的位置很有用。仅了解它们与其他对象的关系(即哪些对象引用了它们)还不够,有关它们在何处创建的信息也很有用。当然,您不想创建应用程序的辅助组件来打印每个分配的堆栈跟踪。您也不想在运行应用程序时将连接到生产环境,而只是为了跟踪内存泄漏。
借助JRockit内存泄漏检测器,可以在分配过程中动态添加应用程序中的代码以创建堆栈跟踪。这些堆栈跟踪可以在工具中累积和分析。只要不启用它,就不会因该功能而产生任何费用,这意味着可以随时执行分配跟踪。当请求分配跟踪时,JRockit编译器动态插入代码以监视分配,但仅针对请求的特定类。更好的是,在数据分析期间,所有添加的代码都将被删除,并且代码中没有留下任何会降低应用程序性能的更改。
图8.示例程序执行期间字符串分配的堆栈跟踪
结论
很难发现内存泄漏。本文重点介绍了几种避免内存泄漏的最佳实践,包括始终记住数据结构中放置的内容以及密切监视内存使用情况以检测突然的增加。
我们都已经看到了JRockit内存泄漏检测器如何在生产系统中用于跟踪内存泄漏。该工具使用三步法查找泄漏。首先,执行趋势分析以找出泄漏的对象类别。接下来,查看其他哪些类与泄漏的类的对象相关联。最后,仔细研究各个对象,并了解它们之间的关系。还可以动态堆叠系统中所有对象分配的跟踪。这些功能以及该工具与JVM的紧密集成,使您可以跟踪内存泄漏并以安全有效的方式对其进行修复。
原始来源:
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/shoujiruanjian/article-359684-1.html
却患了重病
他也是命好
额