稳定性是衡量软件系统质量的重要指标,内存泄漏是破坏系统稳定性的重要因素。由于采用垃圾回收机制,Java语言的内存泄漏的模式与C++等语言相比有很 大的不同。全文通过与C++中的内存泄漏问题进行对比,讲述了Java内存泄漏的基本原理,以及如何借助Optimizeit profiler工具来测试内存泄漏和分析内存泄漏的原因,在实践中证明这是一套行之有效的方法。
关键词 Java; 内存泄漏; GC(垃圾收集器) 引用; Optimizeit
问题的提出
笔者曾经参与开发的网管系统,系统规模庞大,涉及上百万行代码。系统主要采用Java语言开发,大体上分为客户端、服务器和数据库三个层次。在版本进入 测试和试用的过程中,现场人员和测试部人员纷纷反映:系统的稳定性比较差,经常会出现服务器端运行一昼夜就死机的现象,客户端跑死的现象也比较频繁地发 生。对于网管系统来讲,经常性的服务器死机是个比较严重的问题,因为频繁的死机不仅可能导致前后台数据不一致,发生错误,更会引起用户的不满,降低客户的 信任度。因此,服务器端的稳定性问题必须尽快解决。
解决思路
通过察看服务器 端日志,发现死机前服务器端频繁抛出OutOfMemoryException内存溢出错误,因此初步把死机的原因定位为内存泄漏引起内存不足,进而引起 内存溢出错误。如何查找引起内存泄漏的原因呢?有两种思路:第一种,安排有经验的编程人员对代码进行走查和分析,找出内存泄漏发生的位置;第二种,使用专 门的内存泄漏测试工具Optimizeit进行测试。这两种方法都是解决系统稳定性问题的有效手段,使用内存测试工具对于已经暴露出来的内存泄漏问题的定 位和解决非常有效;但是软件测试的理论也告诉我们,系统中永远存在一些没有暴露出来的问题,而且,系统的稳定性问题也不仅仅只是内存泄漏的问题,代码走查 是提高系统的整体代码质量乃至解决潜在问题的有效手段。基于这样的考虑,我们的内存稳定性工作决定采用代码走查结合测试工具的使用,双管齐下,争取比较彻 底地解决系统的稳定性问题。
在代码走查的工作中,安排了对系统业务和开发语言工具比较熟悉的开发人员对应用的代码进行了交叉走查,找出代码中存在的数据库连接声明和结果集未关闭、代码冗余和低效等故障若干,取得了良好的效果,文中主要讲述结合工具的使用对已经出现的内存泄漏问题的定位方法。
内存泄漏的基本原理
在C++语言程序中,使用new操作符创建的对象,在使用完毕后应该通过delete操作符显示地释放,否则,这些对象将占用堆空间,永远没有办法得到回收,从而引起内存空间的泄漏。如下的简单代码就可以引起内存的泄漏:
void function(){
Int[] vec = new int[5];
}
在function()方法执行完毕后,vec数组已经是不可达对象,在C++语言中,这样的对象永远也得不到释放,称这种现象为内存泄漏。
而Java是通过垃圾收集器(Garbage Collection,GC)自动管理内存的回收,程序员不需要通过调用函数来释放内存,但它只能回收无用并且不再被其它对象引用的那些对象所占用的空 间。在下面的代码中,循环申请Object对象,并将所申请的对象放入一个Vector中,如果仅仅释放对象本身,但是因为Vector仍然引用该对象, 所以这个对象对GC来说是不可回收的。因此,如果对象加入到Vector后,还必须从Vector中删除,最简单的方法就是将Vector对象设置为 null。
Vector v = new Vector(10);
for (int i = 1; i < 100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}//此时,所有的Object对象都没有被释放,因为变量v引用这些对象。
实际上无用,而还被引用的对象,GC就无能为力了(事实上GC认为它还有用),这一点是导致内存泄漏最重要的原因。
Java的内存回收机制可以形象地理解为在堆空间中引入了重力场,已经加载的类的静态变量和处于活动线程的堆栈空间的变量是这个空间的牵引对象。这里牵 引对象是指按照Java语言规范,即便没有其它对象保持对它的引用也不能够被回收的对象,即Java内存空间中的本原对象。当然类可能被去加载,活动线程 的堆栈也是不断变化的,牵引对象的集合也是不断变化的。对于堆空间中的任何一个对象,如果存在一条或者多条从某个或者某几个牵引对象到该对象的引用链,则 就是可达对象,可以形象地理解为从牵引对象伸出的引用链将其拉住,避免掉到回收池中;而其它的不可达对象由于不存在牵引对象的拉力,在重力的作用下将掉入 回收池。在图1中,A、B、C、D、E、F六个对象都被牵引对象所直接或者间接地“牵引”,使得它们避免在重力的作用下掉入回收池。如果TR1-A链和 TR2-D链断开,则A、B、C三个对象由于失去牵引,在重力的作用下掉入回收池(被回收),D对象也是同样的原因掉入回收池,而F对象仍然存在一个牵引 链(TR3-E-F),所以不会被回收,如图2、3所示。
图1 初始状态
图2 TR1-A链和TR2-D链断开,A、B、C、D掉入回收池
图3 A、B、C、D四个对象被回收
通过前面的介绍可以看到,由于采用了垃圾回收机制,任何不可达对象都可以由垃圾收集线程回收。因此通常说的Java内存泄漏其实是指无意识的、非故意的 对象引用,或者无意识的对象保持。无意识的对象引用是指代码的开发人员本来已经对对象使用完毕,却因为编码的错误而意外地保存了对该对象的引用(这个引用 的存在并不是编码人员的主观意愿),从而使得该对象一直无法被垃圾回收器回收掉,这种本来以为可以释放掉的却最终未能被释放的空间可以认为是被“泄漏 了”。
这里通过一个例子来演示Java的内存泄漏。假设有一个日志类Logger,其提供一个静态的log(String msg)方法,任何其它类都可以调用Logger.Log(message)来将message的内容记录到系统的日志文件中。Logger类有一个类型 为HashMap的静态变量temp,每次在执行log(message)方法的时候,都首先将message的值丢入temp中(以当前线程+当前时间 为键),在方法退出之前再从temp中将以当前线程和当前时间为键的条目删除。注意,这里当前时间是不断变化的,所以log方法在退出之前执行删除条目的 操作并不能删除方法执行之初丢入的条目。这样,任何一个作为参数传给log方法的字符串最终由于被Logger的静态变量temp引用,而无法得到回收, 这种违背实现者主观意图的无意识的对象保持就是我们所说的Java内存泄漏。
鉴别泄漏对象的方法
一般说来,一个正常的系统在其运行稳定后其内存的占用量是基本稳定的,不应该 是无限制的增长的,同样,对任何一个类的对象的使用个数也有一个相对稳定的上限,不应该是持续增长的。根据这样的基本假设,我们可以持续地观察系统运行时 使用的内存的大小和各实例的个数,如果内存的大小持续地增长,则说明系统存在内存泄漏,如果某个类的实例的个数持续地增长,则说明这个类的实例可能存在泄 漏情况。
Optimizeit是Borland公司的产品,主要用于协助对软件系统进行代码优化和故障诊断,其功能众多,使用方便, 其中的OptimizeIt Profiler主要用于内存泄漏的分析。Profiler的堆视图(如图4)就是用来观察系统运行使用的内存大小和各个类的实例分配的个数的,其界面如 图四所示,各列自左至右分别为类名称、当前实例个数、自上个标记点开始增长的实例个数、占用的内存空间的大小、自上次标记点开始增长的内存的大小、被释放 的实例的个数信息、自上次标记点开始增长的内存的大小被释放的实例的个数信息,表的最后一行是汇总数据,分别表示目前JVM中的对象实例总数、实例增长总 数、内存使用总数、内存使用增长总数等。
在实践中,可以分别在系统运行四个小时、八个小时、十二个小时和二十四个小时时间点记录当时 的内存状态(即抓取当时的内存快照,是工具提供的功能,这个快照也是供下一步分析使用),找出实例个数增长的前十位的类,记录下这十个类的名称和当前实例 的个数。在记录完数据后,点击Profiler中右上角的Mark按钮,将该点的状态作为下一次记录数据时的比较点。
图4 Profiler 堆视图
系统运行二十四小时以后可以得到四个内存快照。对这四个内存快照进行综合分析,如果每一次快照的内存使用都比上一次有增长,可以认定系统存在内存泄漏,找出在四个快照中实例个数都保持增长的类,这些类可以初步被认定为存在泄漏。
分析与定位
通过上面的数据收集和初步分析,可以得出初步结论:系统是否存在内存泄漏和哪些对象存在泄漏(被泄漏),如果结论是存在泄漏,就可以进入分析和定位阶段了。
前面已经谈到Java中的内存泄漏就是无意识的对象保持,简单地讲就是因为编码的错误导致了一条本来不应该存在的引用链的存在(从而导致了被引用的对象 无法释放),因此内存泄漏分析的任务就是找出这条多余的引用链,并找到其形成的原因。前面还讲到过牵引对象,包括已经加载的类的静态变量和处于活动线程的 堆栈空间的变量。由于活动线程的堆栈空间是迅速变化的,处于堆栈空间内的牵引对象集合是迅速变化的,而作为类的静态变量的牵引对象的集合在系统运行期间是 相对稳定的。
对每个被泄漏的实例对象,必然存在一条从某个牵引对象出发到达该对象的引用链。处于堆栈空间的牵引对象在被从栈中弹出后就失去其牵引的能力,变为非牵引对象,因此,在长时间的运行后,被泄露的对象基本上都是被作为类的静态变量的牵引对象牵引。
Profiler的内存视图除了堆视图以外,还包括实例分配视图(图5)和实例引用图(图6)。
Profiler的实例引用图为找出从牵引对象到泄漏对象的引用链提供了非常直接的方法,其界面的第二个栏目中显示的就是从泄漏对象出发的逆向引用链。 需要注意的是,当一个类的实例存在泄漏时,并非其所有的实例都是被泄漏的,往往只有一部分是被泄漏对象,其它则是正常使用的对象,要判断哪些是正常的引用 链,哪些是不正常的引用链(引起泄漏的引用链)。通过抽取多个实例进行引用图的分析统计以后,可以找出一条或者多条从牵引对象出发的引用链,下面的任务就 是找出这条引用链形成的原因。
实例分配图提供的功能是对每个类的实例的分配位置进行统计,查看实例分配的统计结果对于分析引用链的形成具有一定的作用,因为找到分配链与引用链的交点往往就可以找到了引用链形成的原因,下面将具体介绍。
图5 实例分配图
图6 实例引用图
设想一个实例对象a在方法f中被分配,最终被实例对象b所引用,下面来分析从b到a的引用链可能的形成原因。方法f在创建对象a后,对它的使用分为四种 情况:1、将a作为返回值返回;2、将a作为参数调用其它方法;3、在方法内部将a的引用传递给其它对象;4、其它情况。其中情况4不会造成由b到a的引 用链的生成,不用考虑。下面考虑其它三种情况:对于1、2两种情况,其造成的结果都是在另一个方法内部获得了对象a的引用,它的分析与方法f的分析完全一 样(递归分析);考虑第3种情况:1、假设方法f直接将对象a的引用加入到对象b,则对象b到a的引用链就找到了,分析结束;2、假设方法f将对象a的引 用加入到对象c,则接下来就需要跟踪对象c的使用,对象c的分析比对象a的分析步骤更多一些,但大体原理都是一样的,就是跟踪对象从创建后被使用的历程, 最终找到其被牵引对象引用的原因。
现在将泄漏对象的引用链以及引用链形成的原因找到了,内存泄漏测试与分析的工作就到此结束,接下来的工作就是修改相应的设计或者实现中的错误了。
总结
使用上述的测试和分析方法,在实践中先后进行了三次测试,找出了好几处内存泄漏错误。系统的稳定性得到很大程度的提高,最初运行1~2天就抛出内存溢出 异常,修改完成后,系统从未出现过内存溢出异常。此方法适用于任何使用Java语言开发的、对稳定性有比较高要求的软件系统。
分享到:
相关推荐
Java系统中的内存泄漏是一个复杂且严重的问题,尤其是在大型软件系统中,它会直接影响系统的稳定性和性能。内存泄漏不同于C++等语言,Java由于其垃圾回收(Garbage Collection, GC)机制,内存泄漏的表现形式有所...
在Android系统中,内存泄漏是一个严重的问题,它会导致应用程序占用过多的内存,进而影响设备性能,甚至可能导致应用崩溃。理解并有效地分析内存泄漏是每个Android开发者必须掌握的关键技能。 内存泄漏通常发生在...
因此,了解Java内存泄漏的成因、检测方法以及解决方案对于保证应用的高效稳定运行至关重要。 #### 2. Java内存回收机制 Java的内存管理主要集中在堆(Heap)区域,其中对象的创建通常是通过`new`关键字或反射方式...
Java内存泄漏是一个严重的问题,它会导致程序性能下降,甚至可能导致应用程序崩溃。为了有效地诊断和解决这类问题,开发者需要借助特定的分析工具。本篇将详细探讨Java内存泄漏及其相关的分析工具。 内存泄漏是指...
在内存泄露测试中,测试用例应尽可能覆盖所有可能引起内存泄露的情况,包括但不限于: - 大量创建和销毁对象。 - 异常处理不当导致的资源未释放。 - 循环引用导致的对象无法被垃圾回收机制回收。 ##### 5.2 测试...
在Java编程环境中,调用外部动态链接库(DLL)是一个常见的需求,特别是在需要与...在实际项目中,对本地代码进行充分的测试和调试,以及使用内存分析工具监控Java进程的内存使用情况,都是确保代码健壮性的重要环节。
Java内存泄露检测是Java开发中一个关键的议题,因为它直接影响到程序的稳定性和资源效率。内存泄露是指程序中已分配的内存无法被正确地释放,从而导致系统资源的浪费和可能导致程序性能下降甚至崩溃。 首先,理解...
通过设置环境变量、调用mtrace函数以及编写适当的测试代码,可以轻松地识别出程序中的内存泄露。需要注意的是,mtrace只能检测通过malloc/free机制分配和释放的内存,对于其他类型的内存错误(比如越界访问),还...
### 性能测试之内存泄露篇 #### 一、概念 在进行性能测试时,内存泄露是一个非常重要的问题。本文将详细介绍内存泄露及其与内存溢出的区别,并介绍如何监测和解决这些问题。 **内存泄露**指的是应用程序在运行...
Java内存泄露问题是软件开发过程中常见的难题之一。通过对内存管理机制的理解及上述几种常见内存泄露场景的认识,开发者可以更好地预防和解决这类问题。为了进一步提高代码的质量,建议在开发过程中结合单元测试和...
检测Java内存泄漏的方法包括使用JVM提供的工具,如VisualVM、JProfiler等,它们可以监测堆内存使用情况,定位对象生命周期,找出长时间存活但不再使用的对象。此外,还可以通过代码审查、单元测试、压力测试等手段...
在Java开发中,通过单元测试可以有效地检测内存泄露等问题,避免在生产环境中出现严重问题。 **单元测试与内存泄露的关系:** 1. **早期发现问题**:通过编写针对特定功能模块的单元测试,可以在开发早期阶段发现...
5. **LeakCanary**:对于Android应用开发者,LeakCanary是一款自动化内存泄漏检测工具,可以在应用运行时自动检测内存泄露,并提供详细的泄漏堆栈追踪。 在检测到内存泄露后,解决方法通常涉及以下几个步骤: 1. *...
- **ProGuard**:一个代码混淆和优化工具,也可以用来检查内存泄漏,尤其是在代码混淆后。 #### 二、MAT的安装与使用 1. **安装MAT**: - 打开Eclipse,选择`Help -> Install New Software...` - 在`Work with:`...
中Java应用内存泄漏的检测,通过监控集合类对象的内存消耗和集合内元素的 使用情况,得出对象内存泄漏的可能性大小,量化对象内存泄漏的风险。检测 系统首先收集垃圾回收事件后的应用内存数据,确定进行...
解决Java内存溢出和内存泄露的方法主要包括以下几点: 1. 适当调整JVM参数:通过设置-Xms和-Xmx指定堆内存的初始大小和最大大小,避免因动态扩展导致的溢出。同时,可以通过-Xss设置线程栈的大小,防止栈溢出。 2....