Java内存泄露

原出处和做者未找到,原做者看到请与我联系
===============================

1、问题的提出

Java的一个重要优势就是经过垃圾收集器(Garbage CollectionGC)自动管理内存的回收,程序员不须要经过调用函数来释放内存。所以,不少程序员认为Java不存在内存泄漏问题,或者认为即便有内存泄漏也不是程序的责任,而是GCJVM的问题。其实,这种想法是不正确的,由于Java也存在内存泄露,但它的表现与C++不一样。java

随着愈来愈多的服务器程序采用Java技术,例如JSPServlet EJB等,服务器程序每每长期运行。另外,在不少嵌入式系统中,内存的总量很是有限。内存泄露问题也就变得十分关键,即便每次运行少许泄漏,长期运行以后,系统也是面临崩溃的危险。程序员

2、Java是如何管理内存

为了判断Java中是否有内存泄露,咱们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。分配内存的方式多种多样,取决于该种语言的语法结构。但不管是哪种语言的内存分配方式,最后都要返回所分配的内存块的起始地址,即返回一个指针到内存块的首地址。在Java中,程序员须要经过关键字new为每一个对象申请内存空间 (基本类型除外),全部的对象都在堆 (Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线的方法确实简化了程序员的工做。但同时,它也加剧了JVM的工做。这也是Java程序运行速度较慢的缘由之一。算法

由于,GC为了可以正确释放对象,GC必须监控每个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都须要进行监控。编程

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象再也不被引用。安全

为了更好理解GC的工做原理,咱们能够将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用者指向被引对象。另外,每一个线程对象能够做为一个图的起始顶点,例如大多程序从main进程开始执行,那么该图就是以main进程顶点开始的一棵根树。在这个有向图中,根顶点可达的对象都是有效对象,GC将不回收这些对象。若是某个对象 (连通子图)与这个根顶点不可达(注意,该图为有向图),那么咱们认为这个(这些)对象再也不被引用,能够被GC回收。服务器

如下,咱们举一个例子说明如何用有向图表示内存管理。对于程序的每个时刻,咱们都有一个有向图表示JVM的内存分配状况。如下右图,就是左边程序运行到第6行的示意图。网络

 

Java使用有向图的方式进行内存管理,能够消除引用循环的问题,例若有三个对象,相互引用,只要它们和根进程不可达的,那么GC也是能够回收它们的。这种方式的优势是管理内存的精度很高,可是效率较低。另一种经常使用的内存管理技术是使用计数器,例如COM模型采用计数器方式管理构件,它与有向图相比,精度行低(很难处理循环引用的问题),但执行效率很高。jvm

Java 中,全部对象都驻留在堆内存,所以局部对象就不存在。当你建立一个对象时,Java 虚拟机(JVM)为该对象分配内存、调用构造器并开始跟踪你使用的对象。当你中止使用一个对象(就是说,当没有对该对象有效的引用时),JVM 经过垃圾回收器将该对象标记为释放状态。当垃圾回收器要释放一个对象的内存时,它首先调用该对象的finalize() 方法(若是该对象定义了此方法的话)。垃圾回收器以独立的低优先级的方式运行,因此只有当其余线程都挂起等待内存释放的状况出现时,它才开始释放对象的内存。模块化

3、java垃圾收集器

垃圾收集器线程是一种低优先级的线程,在一个Java程序的生命周期中,它只有在内存空闲的时候才有机会运行。函数

垃圾收集器的特色和它的执行机制:

垃圾收集器系统有本身的一套方案来判断哪一个内存块是应该被回收的,哪一个是不符合要求暂不回收的。垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,即便程序员能明确地判断出有一块内存已经无用了,是应该回收的,程序员也不能强制垃圾收集器回收该内存块。程序员惟一能作的就是经过调用System. gc 方法来"建议"执行垃圾收集器,但其是否能够执行,何时执行却都是不可知的。这也是垃圾收集器的最主要的缺点。固然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。

垃圾收集器的工做是发现应用程序再也不须要的对象,并在这些对象再也不被访问或引用时将它们删除。从根节点(在java应用程序的整个生存周期内始终存在的那些类)开始,遍历被应用的全部节点进行清除。在它遍历这些节点的同时,它跟踪哪些对象当前正被引用着。任何类只要再也不被引用,它就符合垃圾收集的条件。当删除这些对象后,就将它们所占用的内存资源返回给jvm

1)、垃圾收集器的主要特色:

1.垃圾收集器的工做目标是回收已经无用的对象的内存空间,从而避免内存渗漏体的产生,节省内存资源,避免程序代码的崩溃。

2.垃圾收集器判断一个对象的内存空间是否无用的标准是:若是该对象不能再被程序中任何一个"活动的部分"所引用,此时咱们就说,该对象的内存空间已经无用。所谓"活动的部分",是指程序中某部分参与程序的调用,正在执行过程当中,还没有执行完毕。

3.垃圾收集器线程虽然是做为低优先级的线程运行,但在系统可用内存量太低的时候,它可能会突发地执行来挽救内存资源。固然其执行与否也是不可预知的。

4.垃圾收集器不能够被强制执行,但程序员能够经过调用System. gc方法来建议执行垃圾收集器。

5.不能保证一个无用的对象必定会被垃圾收集器收集,也不能保证垃圾收集器在一段Java语言代码中必定会执行。所以在程序执行过程当中被分配出去的内存空间可能会一直保留到该程序执行完毕,除非该空间被从新分配或被其余方法回收。因而可知,彻底完全地根绝内存渗漏体的产生也是不可能的。可是请不要忘记,Java的垃圾收集器毕竟使程序员从手工回收内存空间的繁重工做中解脱了出来。设想一个程序员要用CC++来编写一段10万行语句的代码,那么他必定会充分体会到Java的垃圾收集器的优势!

6.一样没有办法预知在一组均符合垃圾收集器收集标准的对象中,哪个会被首先收集。

7.循环引用对象不会影响其被垃圾收集器收集。

8.能够经过将对象的引用变量(reference variables,即句柄handles)初始化为null值,来暗示垃圾收集器来收集该对象。但此时,若是该对象链接有事件监听器(典型的 AWT组件),那它仍是不能够被收集。因此在设一个引用变量为null值以前,应注意该引用变量指向的对象是否被监听,如有,要首先除去监听器,而后才能够赋空值。

9.每个对象都有一个finalize( )方法,这个方法是从Object类继承来的。

10finalize( )方法用来回收内存之外的系统资源,就像是文件处理器和网络链接器。该方法的调用顺序和用来调用该方法的对象的建立顺序是无关的。换句话说,书写程序时该方法的顺序和方法的实际调用顺序是不相干的。请注意这只是finalize( )方法的特色。

11.每一个对象只能调用finalize( )方法一次。若是在finalize( )方法执行时产生异常(exception),则该对象仍能够被垃圾收集器收集。

12.垃圾收集器跟踪每个对象,收集那些不可到达的对象(即该对象没有被程序的任何"活的部分"所调用),回收其占有的内存空间。但在进行垃圾收集的时候,垃圾收集器会调用finalize( )方法,经过让其余对象知道它的存在,而使不可到达的对象再次"复苏"为可到达的对象。既然每一个对象只能调用一次finalize( )方法,因此每一个对象也只可能"复苏"一次。

13finalize( )方法能够明确地被调用,但它却不能进行垃圾收集。

14finalize( )方法能够被重载(overload),但只有具有初始的finalize( )方法特色的方法才能够被垃圾收集器调用。

15.子类的finalize( )方法能够明确地调用父类的finalize( )方法,做为该子类对象的最后一次适当的操做。但Java编译器却不认为这是一次覆盖操做(overriding),因此也不会对其调用进行检查。

16.当finalize( )方法还没有被调用时,System. runFinalization( )方法能够用来调用finalize( )方法,并实现相同的效果,对无用对象进行垃圾收集。

此时容易引发一个误解——你们可能想finalize() 方法是安全的,这时一些重要的事情须要注意:

首先,只有当垃圾回收器释放该对象的内存时,才会执行finalize()。若是在 Applet 或应用程序退出以前垃圾回收器没有释放内存,垃圾回收器将不会调用finalize()

其次,除非垃圾回收器认为Applet或应用程序须要额外的内存,不然它不会试图释放再也不使用的对象的内存。换句话说,有可能出现这样的状况:一个 Applet 给少许的对象分配了内存,但没有形成严重的内存需求,因而垃圾回收器没有释放这些对象的内存程序就退出了。

显然,若是为某个对象定义了finalize() 方法,JVM可能不会调用它,由于垃圾回收器未曾释放过那些对象的内存。即便调用System.gc() 也可能不会起做用,由于它仅仅是给 JVM 的一个建议而不是命令,因此finalize()方法的做用也就不是那么明显。Java 1.1中有一个System.runFinalizersOnExit()方法部分地解决了这个问题。(不要将这个方法与 Java1.0中的System.runFinalizations()方法相混淆。)不象System.gc() 方法那样,System.runFinalizersOnExit()方法并不当即试图启动垃圾回收器。而是当应用程序或 Applet 退出时,它调用每一个对象的finalize()方法。

结论:不该当依靠垃圾回收器或finalize() 来执行你的 Applet 和应用程序的资源清除工做。取而代之,应当使用肯定的方法来清除那些资源或建立一个try...finally 块(或相似的机制)来实现。

17.当一个方法执行完毕,其中的局部变量就会超出使用范围,此时能够被看成垃圾收集,但之后每当该方法再次被调用时,其中的局部变量便会被从新建立。

18Java语言使用了一种"标记交换区的垃圾收集算法"。该算法会遍历程序中每个对象的句柄,为被引用的对象作标记,而后回收还没有作标记的对象。所谓遍历能够简单地理解为"检查每个"

19Java语言容许程序员为任何方法添加finalize( )方法,该方法会在垃圾收集器交换回收对象以前被调用。但不要过度依赖该方法对系统资源进行回收和再利用,由于该方法调用后的执行结果是不可预知的。

经过以上对垃圾收集器特色的了解,你应该能够明确垃圾收集器的做用,和垃圾收集器判断一块内存空间是否无用的标准。简单地说,当你为一个对象赋值为null而且从新定向了该对象的引用者,此时该对象就符合垃圾收集器的收集标准。

典型地,GC不会自动执行,直到程序须要的内存比当前可用内存多时才调用,此时,jvm将首先尝试激活GC以获得更多的可用内存,若是仍得不到充足的可用内存,jvm将转向从操做系统申请更多的内存,直到最终超过度配的最大内存而致使java.lang.OutOfMemoryError

4、什么是Java中的内存泄露

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 = null obj指向的内存块此时就无用了,由于下面再没有调用该变量了。

请看如下三种内存管理:

 

程序段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.给对象赋予了新值,既从新分配了内存空间。

最后,一块内存空间符合了垃圾收集器的收集标准,并不意味着这块内存空间就必定会被垃圾收集器收集

5、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()方法

3JfileChooserremoveChoosableFileFilter()

若是建立了一个JfileChooser,而且加入了本身的文件过滤器,删除该对象以前不妨调用它的removeChoosableFileFilter()方法

4、调用TimerTimerTaskCancel()方法

5、当不须要一个类时,最好删除它的监听器

6、内存检测过程当中不只要关注本身编写的类对象,同时也要关注一些基本类型的对象,例如:int[],String,char[]等等。

7、在确认一个对象无用后,将其全部引用显式的置为null

6、如何检测内存泄漏

最后一个重要的问题,就是如何检测Java的内存泄漏。目前,咱们一般使用一些工具来检查Java程序的内存泄漏问题。市场上已有几种专业检查Java内存泄漏的工具,它们的基本工做原理大同小异,都是经过监测Java程序运行时,全部对象的申请、释放等动做,将内存管理的全部信息进行统计、分析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。这些工具包括Optimizeit ProfilerJProbe ProfilerJinSight , Rational 公司的Purify等。

下面,咱们将简单介绍Optimizeit的基本功能和工做原理。

Optimizeit Profiler版本4.11支持ApplicationAppletServletRomote Application四类应用,而且能够支持大多数类型的JVM,包括SUN JDK系列,IBMJDK系列,和JbuilderJVM等。而且,该软件是由Java编写,所以它支持多种操做系统。Optimizeit系列还包括Thread DebuggerCode Coverage两个工具,分别用于监测运行时的线程状态和代码覆盖面。

当设置好全部的参数了,咱们就能够在OptimizeIt环境下运行被测程序,在程序运行过程当中,Optimizeit能够监视内存的使用曲线(以下图),包括JVM申请的堆(heap)的大小,和实际使用的内存大小。另外,在运行过程当中,咱们能够随时暂停程序的运行,甚至强行调用GC,让GC进行内存回收。经过内存使用曲线,咱们能够总体了解程序使用内存的状况。这种监测对于长期运行的应用程序很是有必要,也很容易发现内存泄露。

在运行过程当中,咱们还能够从不一样视角观查内存的使用状况,Optimizeit提供了四种方式:

    堆视角。 这是一个全面的视角,咱们能够了解堆中的全部的对象信息(数量和种类),并进行统计、排序,过滤。了解相关对象的变化状况。

    方法视角。经过方法视角,咱们能够得知每一种类的对象,都分配在哪些方法中,以及它们的数量。

    对象视角。给定一个对象,经过对象视角,咱们能够显示它的全部出引用和入引用对象,咱们能够了解这个对象的全部引用关系。

    引用图。 给定一个根,经过引用图,咱们能够显示从该顶点出发的全部出引用。

在运行过程当中,咱们能够随时观察内存的使用状况,经过这种方式,咱们能够很快找到那些长期不被释放,而且再也不使用的对象。咱们经过检查这些对象的生存周期,确认其是否为内存泄露。在实践当中,寻找内存泄露是一件很是麻烦的事情,它须要程序员对整个程序的代码比较清楚,而且须要丰富的调试经验,可是这个过程对于不少关键的Java程序都是十分重要的。

综上所述,Java也存在内存泄露问题,其缘由主要是一些对象虽然再也不被使用,但它们仍然被引用。为了解决这些问题,咱们能够经过软件工具来检查内存泄露,检查的主要原理就是暴露出全部堆中的对象,让程序员寻找那些无用但仍被引用的对象。