现在的位置: 首页 > 综合 > 正文

java内存泄漏

2013年02月19日 ⁄ 综合 ⁄ 共 6375字 ⁄ 字号 评论关闭
1)、java内存泄漏

       在 Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象 是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用 内存。
       在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。
       通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。
       因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。
       对 于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范 定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策 略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心 这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃 圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。
       下 面给出了一个简单的内存泄露的例子。在这个例子中,我们循环申请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认为它还有用),这一点是导致内存泄露最重要的原因。
(2)、判断一个对象是否符合垃圾收集器的收集标准
       Object obj = new Object ( ) ; 我们知道,objObject的一个句柄。当出现new关键字时,就给新建的对象分配内存空间,而obj的值就是新分配的内存空间的首地址,即该对象的值(对象的值和对象的内容是不同含义的两个概念:对象的值就是指其内存块的首地址,即对象的句柄;而对象的内容则是其具体的内存块)。此时如果有 obj = nullobj指向的内存块此时就无用了,因为下面再没有调用该变量了。 请看以下三种内存管理:
程序段1
1fobj = new Object ( ) ;
2fobj. Method ( ) ;
3fobj = new Object ( ) ;
4fobj. Method ( ) ;
问:这段代码中,第几行的fobj 符合垃圾收集器的收集标准?
答:第3行。因为第3行的fobj被赋了新值,产生了一个新的对象,即换了一块新的内存空间,也相当于为第1行中的fobj赋了null值。
程序段2
1Object sobj = new Object ( ) ;
2Object sobj = null ;
3Object sobj = new Object ( ) ;
4sobj = new Object ( ) ;
问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准? 答:第1行和第3行。因为第2行为sobj赋值为null,所以在此第1行的sobj符合垃圾收集器的收集标准。而第4行相当于为sobj赋值为null,所以在此第3行的sobj也符合垃圾收集器的收集标准。 如果有一个对象的句柄a,且你把a作为某个构造器的参数,即 new Constructor ( a )的时候,即使你给a赋值为nulla也不符合垃圾收集器的收集标准。直到由上面构造器构造的新对象被赋空值时,a才可以被垃圾收集器收集。 程序段3
1Object aobj = new Object ( ) ;
2Object bobj = new Object ( ) ;
3Object cobj = new Object ( ) ;
4aobj = bobj;
5aobj = cobj;
6cobj = null;
7aobj = null;
问:这段代码中,第几行的内存空间符合垃圾收集器的收集标准? 答:自己做,答案明天放在论坛中。
答:第7行。注意这类题型是认证考试中可能遇到的最难题型了。
1-3分别创建了Object类的三个对象:aobjbobjcobj
4:此时对象aobj的句柄指向bobj,所以该行的执行不能使aobj符合垃圾收集器的收集标准。
5:此时对象aobj的句柄指向cobj,所以该行的执行不能使aobj符合垃圾收集器的收集标准。
6:此时仍没有任何一个对象符合垃圾收集器的收集标准。
7:对象cobj符合了垃圾收集器的收集标准,因为cobj的句柄指向单一的地址空间。在第6行的时候,cobj已经被赋值为null,但由cobj同时还指向了aobj(第5行),所以此时cobj并不符合垃圾收集器的收集标准。而在第7行,aobj所指向的地址空间也被赋予了空值null,这就说明了,由cobj所指向的地址空间已经被完全地赋予了空值。所以此时cobj最终符合了垃圾收集器的收集标准。但对于aobjbobj,仍然无法判断其是否符合收集标准。
总之,在Java语言中,判断一块内存空间是否符合垃圾收集器收集标准的标准只有两个:
1.给对象赋予了空值null,以下再没有调用过。
2.给对象赋予了新值,既重新分配了内存空间。 最后,一块内存空间符合了垃圾收集器的收集标准,并不意味着这块内存空间就一定会被垃圾收集器收集
五java内存管理示例
(1)、多重引用
程序中的对象是否有用和java gc认为对象是否有用是有差别的:
1、 程序员编写代码通常是认为被创建的对象在其生命周期结束后无用
2、 gc认为只有对象的引用记数=0的时候,该对象才是无用的。
两者会产生不一致的地方,如下图所示:
代码(程序员的观点)                           gc(java)
Public void fun1(){
……
//创建局部变量E
Object E = new E();                             E.count ++
A.a = E;                                                E.count ++
B.b = E;                                                E.count ++
C.c = E;                                                 E.count ++
D.d = E;                                                E.count ++
//我们认为
//E没用了,释放E
E = null;                                                 E.count –
……
}
认为已无用                                               E的引用数=4
                                                                   仍旧有用
应该释放                                                   gc不负责释放
结论:
1、 如果要释放对象,就必须使其的引用记数为0,只有那些不再被引用的对象才能被释放,这个原理很简单,但是很重要,是导致内存泄露的基本原因,也是解决内存泄露方法的宗旨。
2、 程序员无须管理对象空间具体的分配和释放过程,但必须要关注被释放对象的引用记数是否为0
3、 一个对象可能被其他对象引用的过程的几种
a、直接赋值,如上例中的A.a = E;
b、通过参数传递,例如public void addObject(Object E)
c、其它一些情况如系统调用等。
(2)、几种容易遗忘并导致不能释放的引用情况

1、 通常的无用引

上面说明了在java应用程序执行期间具有不同生存周期的两个类,类A首先被实例化,并会在很长一段时间或程序的整个生存周期内存在,在某个时候,类B被创建,类A添加对这个新创建的类的一个引用。现在,我们假定类B是某个用户界面小部件,它由用户显示甚至解除。如果没清除类AB的引用,则即便不再需要类B,并且在执行下一个垃圾收集周期以后,类B仍将存在并占用内存空间
2、 内部类的引用
       内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放,从内部类的引用来看我们要释放对象A,需要做到的不仅是将对象A的引用记数清为0,最好是将指向A对象以及A对象内部成员的引用都清为0
3、 监听器引用
       在java编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如AddXXXListener()等方法来增加监听器,建议在释放对象的时候删除这些对象,如果不这样做,那么程序存在内存泄露的机会将增大很多。
4、 外部模块的引用
       对于程序员而言,自己的程序很清楚,如果发现内存泄露,自己对这些对象的引用可以很快定位并解决,但是现在的应用软件并非一个人实现,模块化的思想在现代软件中非常明显,所以程序员要小心外部模块不经意的引用,例如程序员A负责A模块,它调用了B模块的一个方法如:
public void registerMsg(Object b);
这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B是否提供相应的操作去除引用。
5、 系统的引用
       这种引用比较少,但很难定位和解决,类似监听器引用,当创建系统类对象,譬如线程、定时器、面板、颜色选择框、文件选择对话框等,可能会产生一些引用,导致和它们相关的类不能释放,对于其中的原因和解决方案,大家可以去研究。
(3)、顺序和效率
       当发现有很多类对象没有被释放的时候,不要急于解决这些问题,要分析这些类对象的关系,举例如下:
       如果只要释放b圈内的对象,那么最好从A1开始释放,首先关注a1的释放情况,如果要释放c圈内的对象,那么最好从A对象释放开始做起,这和java GC的工作机制一样,就可以提高我们解决内存泄露问题的效率。
代码修改的原则:
如果要释放一个对象,就要将指向该对象的引用清空,最好连带指向该对象内部的引用也清空。例如,要释放A4对象,就要释放A1->A4的应用A2->A4的引用
(4)、经验总结、预防措施和规范建议
1、 容器的removeall()方法
当类从JpanelJdialog或其它容器类继承的时候,删除该对象之前不妨调用它的removeall()方法
2、 线程的interrupe()方法
当类对象是一个Thread的时候,删除该对象之前不妨调用它的interrupe()方法
3、 JfileChooserremoveChoosableFileFilter()
如果创建了一个JfileChooser,并且加入了自己的文件过滤器,删除该对象之前不妨调用它的removeChoosableFileFilter()方法
4、 调用TimerTimerTaskCancel()方法
5、 当不需要一个类时,最好删除它的监听器
6、 内存检测过程中不仅要关注自己编写的类对象,同时也要关注一些基本类型的对象,例如:int[],String,char[]等等。
7、在确认一个对象无用后,将其所有引用显式的置为null
六 如何检测内存泄漏
       最 后一个重要的问题,就是如何检测Java的内存泄漏。目前,我们通常使用一些工具来检查Java程序的内存泄漏问题。市场上已有几种专业检查Java内存 泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java程序运行时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。 开发人员将根据这些信息判断程序是否有内存泄漏问题。这些工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。
       下面,我们将简单介绍Optimizeit的基本功能和工作原理。
       Optimizeit Profiler版 本4.11支持Application,Applet,Servlet和Romote Application四类应用,并且可以支持大多数类型的JVM,包括SUN JDK系列,IBM的JDK系列,和Jbuilder的JVM等。并且,该软件是由Java编写,因此它支持多种操作系统。Optimizeit系列还包 括Thread Debugger和Code Coverage两个工具,分别用于监测运行时的线程状态和代码覆盖面。
        当 设置好所有的参数了,我们就可以在OptimizeIt环境下运行被测程序,在程序运行过程中,Optimizeit可以监视内存的使用曲线(如下图), 包括JVM申请的堆(heap)的大小,和实际使用的内存大小。另外,在运行过程中,我们可以随时暂停程序的运行,甚至强行调用GC,让GC进行内存回 收。通过内存使用曲线,我们可以整体了解程序使用内存的情况。这种监测对于长期运行的应用程序非常有必要,也很容易发现内存泄露。
在运行过程中,我们还可以从不同视角观查内存的使用情况,Optimizeit提供了四种方式:
·     堆视角。 这是一个全面的视角,我们可以了解堆中的所有的对象信息(数量和种类),并进行统计、排序,过滤。了解相关对象的变化情况。
·     方法视角。通过方法视角,我们可以得知每一种类的对象,都分配在哪些方法中,以及它们的数量。
·     对象视角。给定一个对象,通过对象视角,我们可以显示它的所有出引用和入引用对象,我们可以了解这个对象的所有引用关系。
·     引用图。 给定一个根,通过引用图,我们可以显示从该顶点出发的所有出引用。
      在 运行过程中,我们可以随时观察内存的使用情况,通过这种方式,我们可以很快找到那些长期不被释放,并且不再使用的对象。我们通过检查这些对象的生存周期, 确认其是否为内存泄露。在实践当中,寻找内存泄露是一件非常麻烦的事情,它需要程序员对整个程序的代码比较清楚,并且需要丰富的调试经验,但是这个过程对 于很多关键的Java程序都是十分重要的。
       综上所述,Java也存在内存泄露问题,其原因主要是一些对象虽然不再被使用,但它们仍然被引用。为了解决这些问题,我们可以通过软件工具来检查内存泄露,检查的主要原理就是暴露出所有堆中的对象,让程序员寻找那些无用但仍被引用的对象。
【上篇】
【下篇】

抱歉!评论已关闭.