摘要
虽然Java虚拟机(JVM)及其垃圾收集器(garbage collector,GC)负责管理大多数的内存任务,Java软件程序中还是有可能出现内存泄漏。实际上,这在大型项目中是一个常见的问题。避免内存泄漏的第一步是要弄清楚它是如何发生的。本文介绍了编写Java代码的一些常见的内存泄漏陷阱,以及编写不泄漏代码的一些最佳实践。一旦发生了内存泄漏,要指出造成泄漏的代码是非常困难的。因此本文还介绍了一种新工具,用来诊断泄漏并指出根本原因。该工具的开销非常小,因此可以使用它来寻找处于生产中的系统的内存泄漏。
垃圾收集器的作用
虽然垃圾收集器处理了大多数内存管理问题,从而使编程人员的生活变得更轻松了,但是编程人员还是可能犯错而导致出现内存问题。简单地说,GC循环地跟踪所有来自“根”对象(堆栈对象、静态对象、JNI句柄指向的对象,诸如此类)的引用,并将所有它所能到达的对象标记为活动的。程序只可以操纵这些对象;其他的对象都被删除了。因为GC使程序不可能到达已被删除的对象,这么做就是安全的。
虽然内存管理可以说是自动化的,但是这并不能使编程人员免受思考内存管理问题之苦。例如,分配(以及释放)内存总会有开销,虽然这种开销对编程人员来说是不可见的。创建了太多对象的程序将会比完成同样的功能而创建的对象却比较少的程序更慢一些(在其他条件相同的情况下)。
而且,与本文更为密切相关的是,如果忘记“释放”先前分配的内存,就可能造成内存泄漏。如果程序保留对永远不再使用的对象的引用,这些对象将会占用并耗尽内存,这是因为自动化的垃圾收集器无法证明这些对象将不再使用。正如我们先前所说的,如果存在一个对对象的引用,对象就被定义为活动的,因此不能删除。为了确保能回收对象占用的内存,编程人员必须确保该对象不能到达。这通常是通过将对象字段设置为null或者从集合(collection)中移除对象而完成的。但是,注意,当局部变量不再使用时,没有必要将其显式地设置为null。对这些变量的引用将随着方法的退出而自动清除。
概括地说,这就是内存托管语言中的内存泄漏产生的主要原因:保留下来却永远不再使用的对象引用。
典型泄漏
既然我们知道了在Java中确实有可能发生内存泄漏,就让我们来看一些典型的内存泄漏及其原因。
全局集合
在大的应用程序中有某种全局的数据储存库是很常见的,例如一个JNDI树或一个会话表。在这些情况下,必须注意管理储存库的大小。必须有某种机制从储存库中移除不再需要的数据。
这可能有多种方法,但是最常见的一种是周期性运行的某种清除任务。该任务将验证储存库中的数据,并移除任何不再需要的数据。
另一种管理储存库的方法是使用反向链接(referrer)计数。然后集合负责统计集合中每个入口的反向链接的数目。这要求反向链接告诉集合何时会退出入口。当反向链接数目为零时,该元素就可以从集合中移除了。
缓存
缓存是一种数据结构,用于快速查找已经执行的操作的结果。因此,如果一个操作执行起来很慢,对于常用的输入数据,就可以将操作的结果缓存,并在下次调用该操作时使用缓存的数据。
缓存通常都是以动态方式实现的,其中新的结果是在执行时添加到缓存中的。典型的算法是:
检查结果是否在缓存中,如果在,就返回结果。
如果结果不在缓存中,就进行计算。
将计算出来的结果添加到缓存中,以便以后对该操作的调用可以使用。
该算法的问题(或者说是潜在的内存泄漏)出在最后一步。如果调用该操作时有相当多的不同输入,就将有相当多的结果存储在缓存中。很明显这不是正确的方法。
为了预防这种具有潜在破坏性的设计,程序必须确保对于缓存所使用的内存容量有一个上限。因此,更好的算法是:
检查结果是否在缓存中,如果在,就返回结果。
如果结果不在缓存中,就进行计算。
如果缓存所占的空间过大,就移除缓存最久的结果。
将计算出来的结果添加到缓存中,以便以后对该操作的调用可以使用。
通过始终移除缓存最久的结果,我们实际上进行了这样的假设:在将来,比起缓存最久的数据,最近输入的数据更有可能用到。这通常是一个不错的假设。
新算法将确保缓存的容量处于预定义的内存范围之内。确切的范围可能很难计算,因为缓存中的对象在不断变化,而且它们的引用包罗万象。为缓存设置正确的大小是一项非常复杂的任务,需要将所使用的内存容量与检索数据的速度加以平衡。
解决这个问题的另一种方法是使用java.lang.ref.SoftReference类跟踪缓存中的对象。这种方法保证这些引用能够被移除,如果虚拟机的内存用尽而需要更多堆的话。
ClassLoader
Java ClassLoader结构的使用为内存泄漏提供了许多可乘之机。正是该结构本身的复杂性使ClassLoader在内存泄漏方面存在如此多的问题。ClassLoader的特别之处在于它不仅涉及“常规”的对象引用,还涉及元对象引用,比如:字段、方法和类。这意味着只要有对字段、方法、类或ClassLoader的对象的引用,ClassLoader就会驻留在JVM中。因为ClassLoader本身可以关联许多类及其静态字段,所以就有许多内存被泄漏了。
确定泄漏的位置
通常发生内存泄漏的第一个迹象是:在应用程序中出现了OutOfMemoryError。这通常发生在您最不愿意它发生的生产环境中,此时几乎不能进行调试。有可能是因为测试环境运行应用程序的方式与生产系统不完全相同,因而导致泄漏只出现在生产中。在这种情况下,需要使用一些开销较低的工具来监控和查找内存泄漏。还需要能够无需重启系统或修改代码就可以将这些工具连接到正在运行的系统上。可能最重要的是,当进行分析时,需要能够断开工具而保持系统不受干扰。
虽然OutOfMemoryError通常都是内存泄漏的信号,但是也有可能应用程序确实正在使用这么多的内存;对于后者,或者必须增加JVM可用的堆的数量,或者对应用程序进行某种更改,使它使用较少的内存。但是,在许多情况下,OutOfMemoryError都是内存泄漏的信号。一种查明方法是不间断地监控GC的活动,确定内存使用量是否随着时间增加。如果确实如此,就可能发生了内存泄漏。
详细输出
有许多监控垃圾收集器活动的方法。而其中使用最广泛的可能是使用-Xverbose:gc选项启动JVM,并观察输出。
[memory ] 10.109-10.235: GC 65536K->16788K (65536K), 126.000 ms
箭头后面的值(本例中是16788K)是垃圾收集所使用的堆的容量。
控制台
查看连续不断的GC的详细统计信息的输出将是非常乏味的。幸好有这方面的工具。JRockit Management Console可以显示堆使用量的图示。借助于该图,可以很容易地看出堆使用量是否随时间增加。
Figure 1. The JRockit Management Console
甚至可以配置该管理控制台,以便如果发生堆使用量过大的情况(或基于其他的事件),控制台能够向您发送电子邮件。这明显使内存泄漏的查看变得更容易了。
内存泄漏检测工具
还有其他的专门进行内存泄漏检测的工具。JRockit Memory Leak Detector可以用来查看内存泄漏,并可以更深入地查出泄漏的根源。这个强大的工具是紧密集成到JRockit JVM中的,其开销非常小,对虚拟机的堆的访问也很容易。
专业工具的优点
一旦知道确实发生了内存泄漏,就需要更专业的工具来查明为什么会发生泄漏。JVM自己是不会告诉您的。这些专业工具从JVM获得内存系统信息的方法基本上有两种:JVMTI和字节码技术(byte code instrumentation)。Java虚拟机工具接口(Java Virtual Machine Tools Interface,JVMTI)及其前身Java虚拟机监视程序接口(Java Virtual Machine Profiling Interface,JVMPI)是外部工具与JVM通信并从JVM收集信息的标准化接口。字节码技术是指使用探测器处理字节码以获得工具所需的信息的技术。
对于内存泄漏检测来说,这两种技术有两个缺点,这使它们不太适合用于生产环境。首先,它们在内存占用和性能降低方面的开销不可忽略。有关堆使用量的信息必须以某种方式从JVM导出,并收集到工具中进行处理。这意味着要为工具分配内存。信息的导出也影响了JVM的性能。例如,当收集信息时,垃圾收集器将运行得比较慢。另外一个缺点是需要始终将工具连在JVM上。这是不可能的:将工具连在一个已经启动的JVM上,进行分析,断开工具,并保持JVM运行。
因为JRockit Memory Leak Detector是集成到JVM中的,就没有这两个缺点了。首先,许多处理和分析工作是在JVM内部进行的,所以没有必要转换或重新创建任何数据。处理还可以背负(piggyback)在垃圾收集器本身上而进行,这意味着提高了速度。其次,只要JVM是使用-Xmanagement选项(允许通过远程JMX接口监控和管理JVM)启动的,Memory Leak Detector就可以与运行中的JVM进行连接或断开。当该工具断开时,没有任何东西遗留在JVM中,JVM又将以全速运行代码,正如工具连接之前一样。
趋势分析
让我们深入地研究一下该工具以及它是如何用来跟踪内存泄漏的。在知道发生内存泄漏之后,第一步是要弄清楚泄漏了什么数据--哪个类的对象引起了泄漏?JRockit Memory Leak Detector是通过在每次垃圾收集时计算每个类的现有对象的数目来实现这一步的。如果特定类的对象数目随时间而增长(“增长率”),就可能发生了内存泄漏。
图2. Memory Leak Detector的趋势分析视图
因为泄漏可能像细流一样非常小,所以趋势分析必须运行很长一段时间。在短时间内,可能会发生一些类的局部增长,而之后它们又会跌落。但是趋势分析的开销很小(最大开销也不过是在每次垃圾收集时将数据包由JRockit发送到Memory Leak Detector)。开销不应该成为任何系统的问题——即使是一个全速运行的生产中的系统。
起初数目会跳跃不停,但是一段时间之后它们就会稳定下来,并显示出哪些类的数目在增长。
找出根本原因
有时候知道是哪些类的对象在泄漏就足以说明问题了。这些类可能只用于代码中的非常有限的部分,对代码进行一次快速检查就可以显示出问题所在。遗憾地是,很有可能只有这类信息还并不够。例如,常见到泄漏出在类java.lang.String的对象上,但是因为字符串在整个程序中都使用,所以这并没有多大帮助。
我们想知道的是,另外还有哪些对象与泄漏对象关联?在本例中是String。为什么泄漏的对象还存在?哪些对象保留了对这些对象的引用?但是能列出的所有保留对String的引用的对象将会非常多,以至于没有什么实际用处。为了限制数据的数量,可以将数据按类分组,以便可以看出其他哪些对象的类与泄漏对象(String)关联。例如,String在Hashtable中是很常见的,因此我们可能会看到与String关联的Hashtable数据项对象。由Hashtable数据项倒推,我们最终可以找到与这些数据项有关的Hashtable对象以及String(如图3所示)。
图3. 在工具中看到的类型图的示例视图
倒推
因为我们仍然是以类的对象而不是单独的对象来看待对象,所以我们不知道是哪个Hashtable在泄漏。如果我们可以弄清楚系统中所有的Hashtable都有多大,我们就可以假定最大的Hashtable就是正在泄漏的那一个(因为随着时间的流逝它会累积泄漏而增长得相当大)。因此,一份有关所有Hashtable对象以及它们引用了多少数据的列表,将会帮助我们指出造成泄漏的确切Hashtabl。
图4. 界面:Hashtable对象以及它们所引用数据的数量的列表
对对象引用数据数目的计算开销非常大(需要以该对象作为根遍历引用图),如果必须对许多对象都这么做,将会花很多时间。如果了解一点Hashtable的内部实现原理就可以找到一条捷径。Hashtable的内部有一个Hashtable数据项的数组。该数组随着Hashtable中对象数目的增长而增长。因此,为找出最大的Hashtable,我们只需找出引用Hashtable数据项的最大数组。这样要快很多。
图5. 界面:最大的Hashtable数据项数组及其大小的清单
更进一步
当找到发生泄漏的Hashtable实例时,我们可以看到其他哪些实例在引用该Hashtable,并倒推回去看看是哪个Hashtable在泄漏。
图 6. 这就是工具中的实例图
例如,该Hashtable可能是由MyServer类型的对象在名为activeSessions的字段中引用的。这种信息通常就足以查找源代码以定位问题所在了。
图7. 检查对象以及它对其他对象的引用
找出分配位置
当跟踪内存泄漏问题时,查看对象分配到哪里是很有用的。只知道它们如何与其他对象相关联(即哪些对象引用了它们)是不够的,关于它们在何处创建的信息也很有用。当然了,您并不想创建应用程序的辅助构件,以打印每次分配的堆栈跟踪(stack trace)。您也不想仅仅为了跟踪内存泄漏而在运行应用程序时将一个分析程序连接到生产环境中。
借助于JRockit Memory Leak Detector,应用程序中的代码可以在分配时进行动态添加,以创建堆栈跟踪。这些堆栈跟踪可以在工具中进行累积和分析。只要不启用就不会因该功能而产生成本,这意味着随时可以进行分配跟踪。当请求分配跟踪时,JRockit 编译器动态插入代码以监控分配,但是只针对所请求的特定类。更好的是,在进行数据分析时,添加的代码全部被移除,代码中没有留下任何会引起应用程序性能降低的更改。
图8. 示例程序执行期间String的分配的堆栈跟踪
结束语
内存泄漏是难以发现的。本文重点介绍了几种避免内存泄漏的最佳实践,包括要始终记住在数据结构中所放置的内容,以及密切监控内存使用量以发现突然的增长。
我们都已经看到了JRockit Memory Leak Detector是如何用于生产中的系统以跟踪内存泄漏的。该工具使用一种三步式的方法来找出泄漏。首先,进行趋势分析,找出是哪个类的对象在泄漏。接下来,看看有哪些其他的类与泄漏的类的对象相关联。最后,进一步研究单个对象,看看它们是如何互相关联的。也有可能对系统中所有对象分配进行动态的堆栈跟踪。这些功能以及该工具紧密集成到JVM中的特性使您可以以一种安全而强大的方式跟踪内存泄漏并进行修复。
相关推荐
本文将深入探讨如何检测和分析Java内存泄露与溢出,并介绍一种常用的工具——Memory Analyzer(MAT)。 首先,理解内存泄露的概念至关重要。在Java中,内存泄露通常发生在对象不再被程序使用但仍然保持在内存中,...
java检测内存泄露工具--jprofiler5 里面含破解注册码,按照使用说明即可使用!
为了解决这个问题,开发人员需要借助特定的工具来检测和定位内存泄露,其中“VisualLeakDetector”就是这样一款高效实用的工具。 Visual Leak Detector(VLD)是一款专门为Microsoft Visual C++编译器设计的内存...
总结来说,Java内存泄漏的分析工具如JVisualVM、MAT、JProfiler和Arthas等,提供了丰富的功能来帮助开发者定位和解决内存泄漏问题。理解这些工具的使用方法和特性,结合实际的项目经验,可以有效地防止和修复内存...
总结来说,JNI层内存泄漏检测工具LeakTracer是Android开发中的一个强大工具,它可以帮助开发者有效地定位和解决JNI层的内存泄漏问题,提升应用的性能和可靠性。通过深入研究和实践,开发者可以更好地掌握JNI内存管理...
通过深入了解Java的内存管理机制,并借助于专业的工具如OptimizeIt,可以有效地检测和解决内存泄漏问题。此外,开发者还需要遵循良好的编程实践,比如合理使用静态变量、正确管理资源的生命周期等,从而避免内存泄漏...
### Windows 下 C++ 内存泄露检测工具使用详解 在 Windows 平台下进行 C++ 开发时,内存管理是一项非常重要的任务。由于 C++ 语言本身的特性,开发者需要手动管理内存分配与释放,这就很容易导致内存泄露的问题。...
14. IBM Rational PurifyPlus:是一个功能强大的内存泄漏检测工具,用于检测 C/C++、托管.NET、Java 和 VB6 代码中的性能和可靠性错误。 IBM Rational PurifyPlus 工具可以检测内存泄漏问题,并提供了详细的错误...
"JAVA内存泄漏分析工具"正是一款用于解决此类问题的专业工具,它能帮助开发者定位并修复内存相关的问题,如内存泄漏和内存溢出。 内存泄漏是程序在申请内存后,无法释放已申请的内存空间,一次小的内存泄漏可能看似...
"arm环境内存泄漏检测工具valgrind"就是这样一个针对ARM平台的专业工具,它能帮助我们识别并解决内存泄漏问题。 Valgrind是一个开源的动态分析工具集,主要用于调试、性能评估和内存错误检测。在ARM环境下,...
为了解决这个问题,开发者通常会使用专门的内存泄漏检测工具。本文将详细介绍两个用于Windows平台的内存泄漏排查工具:LeakDiag和LDGrapher。 **LeakDiag** LeakDiag是由微软开发的一款轻量级内存泄漏检测工具,...
Java内存泄露检测是Java开发中一个关键的议题,因为它直接影响到程序的稳定性和资源效率。内存泄露是指程序中已分配的内存无法被正确地释放,从而导致系统资源的浪费和可能导致程序性能下降甚至崩溃。 首先,理解...
本文将详细介绍Lua内存泄露检测工具LuaMonitor,以及如何利用它来提升项目性能。 LuaMonitor是一款专门针对Lua环境设计的内存泄露检测工具。它提供了一个直观的视图分析界面,帮助开发者了解程序在运行过程中内存的...
Java内存泄漏问题追踪 在Java编程中,内存泄漏是一个严重的问题,它会导致程序性能...通过了解其原因、使用合适的工具进行检测,并结合最佳实践来优化代码,可以有效避免和解决内存泄漏问题,保证程序的稳定性和性能。
C#内存检测工具是开发过程中不可或缺的辅助软件,主要用于诊断和优化C#应用程序的内存使用情况。在.NET框架下,C#程序的内存管理主要由垃圾收集器(Garbage Collector, GC)负责,但理解并控制内存使用仍然是开发者...
为了检测和解决内存泄露,我们可以使用一些工具进行内存分析,如VisualVM、JProfiler、MAT(Memory Analyzer Tool)等。这些工具能提供详细的内存分配、对象生命周期和引用链路信息,帮助我们定位问题。 分析内存...
tMemMonitor(TMM)是一款运行时C/C++内存泄漏分析工具,其检测结果专业、准确,操作却极其简单,可以帮助C/C++程序员迅速解决内存泄漏。TMM中引入GC机制,内存泄露检测准确率可达100%,无需内存快照,不影响目标...
JS内存泄漏检测工具:IEJSLeaksDetector
Valgrind是一款开源的动态分析工具,主要用于检测C和C++程序中的内存错误,包括内存泄漏、未初始化的内存读取、无效指针访问等。它通过构建一个虚拟机,使得程序在该虚拟机上运行,从而能够对内存操作进行精细的监控...
jstack命令是一个强大的工具,用于分析Java程序的崩溃原因和堆栈信息。它可以将core文件转换为人类可读的格式,方便开发者快速定位问题所在。如果Java程序崩溃生成core文件,jstack工具可以用来获得core文件的Java栈...