Java程序中的“内存泄漏”问题

jopen 10年前

Java程序中的“内存泄漏”问题

大多数程序员都知道,使用Java编程语言的一大好处就是,不必再担心内存的分配和释放问题。您只须创建对象,当应用程序不再需要这些对象 时,Java 会通过一种称为“垃圾回收”的机制将这些对象的内存释放掉。他们认为Java不存在内存泄漏问题,或者认为即使有内存泄漏也不是程序的责 任,而是垃圾回收器(GC)或Java虚拟机(JVM)的问题。但事实真的是这样吗?Java真的已经解决了困扰其他编程语言的内存泄露问题了吗?

一、Java的内存管理机制

在进一步讨论之前,我们先了解一下Java的内存管理机制。Java的内存管理就是对象的分配和释放问题。在Java中,程序员需要通过关键字new为每 个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。在Java中,内存的分配是由程序完成的,而内存的释放是则是由垃圾 回收器决定和执行的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的负担,这也是Java程序运行速度较慢的原因之一。因为, 垃圾回收器为了能够正确回收对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等。监视对象状态是为了准确、及时地释放对象,而释 放对象的基本原则就是该对象是否仍被引用。

垃圾收集器的工作是发现应用程序不再需要的对象,并在这些对象不再被访问或引用时将它们删除。垃圾收集器从根节点(在 Java 应用程序的整个生存周期 内始终存在的那些类)开始,遍历所有仍被引用的节点,进行垃圾回收。任何对象只要不再被引用,就符合垃圾回收的条件。垃圾回收器回收这些对象后,它们所占 用的内存资源也就被返回给了Java虚拟机。

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

为了更好理解地垃圾回收器的工作原理,我们可以将对象考虑为有向图的顶点,将引用关系考虑为图的有向边,有向边从引用对象指向被引对象。每个线程可以作为 一个图的起始顶点,例如大多程序从main线程开始执行,那么该图就是以main线程为顶点的一个有向图。在这个有向图中,根顶点可达的对象都是有效对 象,如果某个对象不可达,那么垃圾回收器会认为这个对象不再被引用,可以被回收。对于程序的每一个时刻,我们都有一个有向图表示JVM的内存分配情况。

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

在C++ 程序中,内存泄漏是指应用程序为某些对象被分配了内存空间,然后却因为某些原因不可达,以至于被这些对象使用的内存无法被释放并返还给操作系统,这些内存将永远收不回来。

令人欣慰的是,这种内存泄露问题在Java程序中并不存在。在Java中,对象使用的内存都由垃圾回收器负责回收的,而Java虚拟机并不存在任何被证实 的内存泄漏问题。实践证明,垃圾收集器一般能够精确地判断哪些对象可被收集,回收它们占用的内存空间并返还给Java 虚拟机。

对于Java来说,内存泄漏是指在程序中存在一些实际上并不需要的对象引用。这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可 以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。一个典型的例子是向一个集合中加入一些对象以便以后使用它们,但是却没有在使用完后 从集合中删除对这些对象的引用。因为集合可以无限制地扩大,并且从来不会变小,所以当向集合中加入了太多的对象(或者是有很多的对象被集合中的元素所引 用)时,就会因为堆空间被填满而导致内存耗尽。垃圾收集器并不会把这些您认为已经用完的对象当作垃圾进行回收,因为对于垃圾收集器来说,应用程序仍然可以 通过这个集合在任何时候访问这些对象。

1、静态集合类引起内存泄露:

    Static Vector v = new Vector(10);        for (int i = 1; i<100; i++)        {        Object o = new Object();        v.add(o);        o = null;        }//  
</div> </div>

在这个例子中,循环申请Object 对象,并将所申请的对象放入一个Vector 中,如果仅仅释放引用本身(o=null),那么Vector 仍然引用该对象,所以这个对象对GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从Vector 中删除,最简单的方法就是将Vector对象设置为null。
2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。

    public static void main(String[] args)        {        Set<Person> set = new HashSet<Person>();        Person p1 = new Person("唐僧","pwd1",25);        Person p2 = new Person("孙悟空","pwd2",26);        Person p3 = new Person("猪八戒","pwd3",27);        set.add(p1);        set.add(p2);        set.add(p3);        System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!        p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变        set.remove(p3); //此时remove不掉,造成内存泄漏        set.add(p3); //重新添加,居然添加成功        System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!        for (Person person : set)        {        System.out.println(person);        }        }  
</div> </div>

通过以上分析,可以知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,都由垃圾回收器进行内存的回收管理。

随着越来越多的服务器程序、嵌入式系统及游戏平台采用Java技术,出现了较多内存有限、需要长期运行Java应用。内存泄露问题也就变得十分关键,即使每次少量内存泄漏,长期运行之后,系统也有面临内存溢出的危险。

三、典型的内存泄漏问题及解决方法

我们知道了在Java中确实会存在内存泄漏,那么就让我们看一看几种典型的泄漏,并试图找出他们的解决方法。

3.1 全局集合

在大型应用程序中存在各种各样的全局数据储存库是很普遍的,比如一个Session Table。在这些情况下,必须注意管理储存库的大小。必须使用某种机制从储存库中移除不再需要的数据。

通常有很多不同的解决形式,其中最常用的一种是周期运行的清除作业。这个作业会验证仓库中的数据然后清除一切不需要的数据。

另一种管理储存库的方法是使用反向链接(Referrer)计数。然后集合负责统计集合中每个元素反向链接的数目,当反向链接数目为零时,该元素就可以从集合中移除了。 
3.2 缓存 
    缓存一种用来快速查找已经执行过的操作结果的数据结构。因此,如果一个操作执行需要比较多的资源并会多次被使用,通常做法是把常用的输入数据的操 作结果进行缓存,以便在下次调用该操作时使用缓存中的数据。缓存通常都是以动态方式实现的,如果缓存设置不正确而大量使用缓存的话,则会出现内存溢出的后 果,因此需要将所使用的内存容量与检索数据的速度加以平衡。

常用的解决途径是使用软引用或弱引用类将对象放入缓存。这个方法可以保证当虚拟机用完内存或者需要更多堆的时候,可以释放这些对象的引用。

3.3 类装载器

Java类装载器的使用为内存泄漏提供了许多可乘之机。一般来说类装载器都具有复杂结构,因为类装载器不仅仅是只与“常规”的对象引用有关,同时也和对象 内部的引用有关。比如数据变量,方法和各种类。这意味着只要存在对数据变量,方法,各种类和对象的引用,那么类装载器将驻留在Java虚拟机中。既然类装 载器可以同很多的类关联,同时也可能和静态数据变量关联,那么相当多的内存就可能发生泄漏。

3.4 物理连接

一些物理连接,比如数据库连接和网络连接,除非其显式的关闭了连接,否则是不会自动被GC 回收的。Java数据库连接一般用 DataSource.getConnection()来创建,当不再使用时必须用Close()方法来释放。对于ResultSet 和 Statement 对象可以不进行显式回收,但Connection 一定要显式回收,,因为这些连接是独立于Java虚拟机的,在任何时候都无法自动 回收,而Connection一旦回收,ResultSet 和Statement 对象就会立即变为NULL。但是如果使用连接池,情况就不一样了,除 了要显式地关闭连接,还必须显式地关闭ResultSet和Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的 Statement 对象无法释放,从而引起内存泄漏。

3.5 内部模块和外部模块等的引用

内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。对于程序员而言,自己的程序很清楚,如果发现内存泄漏,自己对这 些对象的引用可以很快定位并解决。但是在大型应用软件的开发中,整个系统并非一个人实现,个人担当的可能只是系统的某一机能或某机能的一个模块。所以程序 员要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B模块的一个方法如:public void registerMsg(Object b); 这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B是否需要提供相应的去除引用的操作。

四、如何找出内存泄漏

查找内存泄漏一般有两种方法:一是安排有经验的编程人员对代码进行走查和分析,找出内存泄漏发生的位置;二是使用专门的内存泄漏测试工具进行测试。

第一种方法,在代码走查工作中,可以安排对系统业务和开发语言较熟悉的开发人员对应用的代码进行了交叉走查,尽量找出代码中存在的数据库连接声明和结果集未关闭、代码冗余等问题代码。

第二种方法就是使用专门的内存泄漏工具进行测试。市场上已有专业检查Java内存泄漏的工具,它们的基本工作原理大同小异,都是通过监测Java程序运行 时,所有对象的申请、释放等动作,将内存管理的所有信息进行统计、分析、可视化。开发人员将根据这些信息判断程序是否有内存泄漏问题。常用的工具有 Optimizeit Profiler,JProbe Profiler,JinSight以及Rational公司的Purify等。

五、Java中的几种引用方式

    Java中有几种不同的引用方式,它们分别是:强引用、软引用、弱引用和虚引用。下面,我们首先详细地了解下这几种引用方式的意义。

    
      强引用

在此之前我们介绍的内容中所使用的引用都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。

软引用(SoftReference

SoftReference 类的一个典型用途就是用于内存敏感的高速缓存。SoftReference 的原理是:在保持对对象的引用时保证在 JVM 报告内存不足情况之前将清除所有的软引用。关键之处在于,垃圾收集器在运行时可能会(也可能不会)释放软可及对象。对象是否被释放取决于垃圾收集器的算法 以及垃圾收集器运行时可用的内存数量。

弱引用(WeakReference

WeakReference 类的一个典型用途就是规范化映射(canonicalized mapping)。另外,对于那些生存期相对较长而且重新创建的开销也不高的对象来说,弱引用也比较有用。关键之处在于,垃圾收集器运行时如果碰到了弱可及对象,将释放 WeakReference 引用的对象。然而,请注意,垃圾收集器可能要运行多次才能找到并释放弱可及对象。

虚引用(PhantomReference

PhantomReference 类只能用于跟踪对被引用对象即将进行的收集。同样,它还能用于执行 pre-mortem 清除操作。PhantomReference 必须与 ReferenceQueue 类一起使用。需要 ReferenceQueue 是因为它能够充当通知机制。当垃圾收集器确定了某个对象是虚可及对象时,PhantomReference 对象就被放在它的 ReferenceQueue 上。将 PhantomReference 对象放在 ReferenceQueue 上也就是一个通知,表明 PhantomReference 对象引用的对象已经结束,可供收集了。这使您能够刚好在对象占用的内存被回收之前采取行动。ReferenceReferenceQueue的配合使用。

GCReferenceReferenceQueue的交互

A、 GC无法删除存在强引用的对象的内存。

B、 GC发现一个只有软引用的对象内存,那么:

① SoftReference对象的referent 域被设置为null,从而使该对象不再引用heap对象。

② SoftReference引用过的heap对象被声明为finalizable

③ 当 heap 对象的 finalize() 方法被运行而且该对象占用的内存被释放,SoftReference 对象就被添加到它的 ReferenceQueue(如果后者存在的话)。

C、 GC发现一个只有弱引用的对象内存,那么:

① WeakReference对象的referent域被设置为null,从而使该对象不再引用heap对象。

② WeakReference引用过的heap对象被声明为finalizable

③ heap对象的finalize()方法被运行而且该对象占用的内存被释放时,WeakReference对象就被添加到它的ReferenceQueue(如果后者存在的话)。

D、 GC发现一个只有虚引用的对象内存,那么:

① PhantomReference引用过的heap对象被声明为finalizable

② PhantomReference在堆对象被释放之前就被添加到它的ReferenceQueue

值得注意的地方有以下几点:

1GC在一般情况下不会发现软引用的内存对象,只有在内存明显不足的时候才会发现并释放软引用对象的内存。

2GC对弱引用的发现和释放也不是立即的,有时需要重复几次GC,才会发现并释放弱引用的内存对象。
3、软引用和弱引用在添加到ReferenceQueue的时候,其指向真实内存的引用已经被置为空了,相关的内存也已经被释放掉了。而虚引用在添加到ReferenceQueue的时候,内存还没有释放,仍然可以对其进行访问。

    代码示例

通过以上的介绍,相信您对Java的引用机制以及几种引用方式的异同已经有了一定了解。光是概念,可能过于抽象,下面我们通过一个例子来演示如何在代码中使用Reference机制。

    String str = new String("hello"); //①             ReferenceQueue<String> rq = new ReferenceQueue<String>(); //②             WeakReference<String> wf = new WeakReference<String>(str, rq); //③             str=null; //④取消"hello"对象的强引用             String str1=wf.get(); //⑤假如"hello"对象没有被回收,str1引用"hello"对象            //假如"hello"对象没有被回收,rq.poll()返回null             Reference<? extends String> ref=rq.poll(); //⑥  
</div> </div>

在以上代码中,注意⑤⑥两处地方。假如“hello”对象没有被回收wf.get()将返回“hello”字符串对象,rq.poll()返回null;而加入“hello”对象已经被回收了,那么wf.get()返回nullrq.poll()返回Reference对象,但是此Reference对象中已经没有str对象的引用了(PhantomReference则与WeakReferenceSoftReference不同)

    引用机制与复杂数据结构的联合应用

    了解了GC机制、引用机制,并配合上ReferenceQueue,我们就可以实现一些防止内存溢出的复杂数据类型。

例如,SoftReference具有构建Cache系统的特质,因此我们可以结合哈希表实现一个简单的缓存系统。这样既能保证能够尽可能多的缓存信息,又可以保证Java虚拟机不会因为内存泄露而抛出OutOfMemoryError。这种缓存机制特别适合于内存对象生命周期长,且生成内存对象的耗时比较长的情况,例如缓存列表封面图片等。对于一些生命周期较长,但是生成内存对象开销不大的情况,使用WeakReference能够达到更好的内存管理的效果。

SoftHashmap的源码一份,相信看过之后,大家会对Reference机制的应用有更深入的理解。

package com.***.widget;            //: SoftHashMap.java       import java.util.*;       import java.lang.ref.*;       import android.util.Log;            public class SoftHashMap extends AbstractMap {        /** The internal HashMap that will hold the SoftReference. */       private final Map hash = new HashMap();        /** The number of "hard" references to hold internally. */        private final int HARD_SIZE;        /** The FIFO list of hard references, order of last access. */        private final LinkedList hardCache = new LinkedList();       /** Reference queue for cleared SoftReference objects. */       private ReferenceQueue queue = new ReferenceQueue();           //Strong Reference number      public SoftHashMap() { this(100); }       public SoftHashMap(int hardSize) { HARD_SIZE = hardSize; }             public Object get(Object key) {         Object result = null;         // We get the SoftReference represented by that key         SoftReference soft_ref = (SoftReference)hash.get(key);         if (soft_ref != null) {            // From the SoftReference we get the value, which can be            // null if it was not in the map, or it was removed in            // the processQueue() method defined below            result = soft_ref.get();           if (result == null) {              // If the value has been garbage collected, remove the            // entry from the HashMap.              hash.remove(key);            } else {             // We now add this object to the beginning of the hard             // reference queue.  One reference can occur more than             // once, because lookups of the FIFO queue are slow, so             // we don't want to search through it each time to remove             // duplicates.               //keep recent use object in memory            hardCache.addFirst(result);             if (hardCache.size() > HARD_SIZE) {              // Remove the last entry if list longer than HARD_SIZE               hardCache.removeLast();            }           }         }         return result;       }        /** We define our own subclass of SoftReference which contains       not only the value but also the key to make it easier to find       the entry in the HashMap after it's been garbage collected. */        private static class SoftValue extends SoftReference {         private final Object key; // always make data member final          /** Did you know that an outer class can access private data          members and methods of an inner class?  I didn't know that!        I thought it was only the inner class who could access the        outer class's private information.  An outer class can also          access private members of an inner class inside its inner        class. */        private SoftValue(Object k, Object key, ReferenceQueue q) {            super(k, q);            this.key = key;          }        }             /** Here we go through the ReferenceQueue and remove garbage       collected SoftValue objects from the HashMap by looking them        up using the SoftValue.key data member. */        public void processQueue() {          SoftValue sv;         while ((sv = (SoftValue)queue.poll()) != null) {              if(sv.get()== null){                Log.e("processQueue", "null");             }else{                Log.e("processQueue", "Not null");           }           hash.remove(sv.key); // we can access private data!           Log.e("SoftHashMap", "release " + sv.key);         }       }       /** Here we put the key, value pair into the HashMap using       a SoftValue object. */       public Object put(Object key, Object value) {         processQueue(); // throw out garbage collected values first         Log.e("SoftHashMap", "put into " + key);        return hash.put(key, new SoftValue(value, key, queue));        }       public Object remove(Object key) {         processQueue(); // throw out garbage collected values first          return hash.remove(key);        }       public void clear() {         hardCache.clear();          processQueue(); // throw out garbage collected values         hash.clear();       }       public int size() {         processQueue(); // throw out garbage collected values first         return hash.size();       }       public Set entrySet() {         // no, no, you may NOT do that!!! GRRR         throw new UnsupportedOperationException();       }     }   
</div> </div>