问题的提出:

JAVA的一个重要优点是通过垃圾收集器GC自动管理内存的回收,程序员不需要通过调用函数来释放内存。因此,很多程序员认为Java不存在内存泄漏问题,或者认为即使有内存泄漏也不是程序的责任,而是GC或JVM的问题。

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

JAVA是会产生内存泄漏的:

虽然有回收机制存在,但还是会产生内存泄漏的。程序会把项目加入至列表,但在完成时没有移除,如同人把物件丢到一堆物品中或放到抽屉内,但后来忘记取走这件物品一样。内存管理器不能判断项目是否将再被存取,除非程式作出一些指示表明不会再被存取。

解决泄露问题的关键是管理好对象的生命周期,特别是现在ioc等容器帮我们管理对象后更要注意,像spring中对象默认是单例的,单例对象在整个应用的生命周期中一直存在,不能被gc回收。如果这些单例对象没有注意控制其引用的对象,则被单例对象引用的对象也是整个应用生命周期有效,即不能被gc回收的。所以如果单例对象包含集合类对象,像List这样的属性,特别要注意List的add操作。

泄漏和溢出是两个概念:

溢出和泄露是相关的,回收不了是泄露,当然也因此导致溢出,另外vm参数设置过小,也会导致程序启动就溢出。比如,曾经在一个高并发的Java程序中解析>50M的XML,造成内存溢出,即使那台服务器的内存有的是。我理解为JVM无法快速申请大量内存,配置JVM启动参数就可以解决了。另外需要注意的是长时间的操作,系统不会释放这个操作中利用到的内存。比如,通过一系列复杂查询统计后生成文件,这个过程如果比较长,这期间GC发生了也不会释放内存。除此之外,JVM都有很多启动参数,是值得研究的。SUN JVM、BEA JRocket、IBM JVM的参数还有些不一样。

JAVA如何管理内存:

为了判断Java中是否有内存泄漏,我们首先必须了解Java是如何管理内存的。Java的内存管理就是对象的分配和释放问题。在Java中,程序员需要通过关键字new为每个对象申请内存空间(基本类型除外),所有的对象都在堆(Heap)中分配空间。另外,对象的释放是由GC决定和执行的。在Java中,内存的分配是由程序完成的,而内存的释放是有GC完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,也加重了JVM的工作。这也是Java程序运行速度较慢的原因之一。因为,GC为了能够正确释放对象,GC必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。

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

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

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

 

java 怎么避免full gc java如何避免内存泄露_vector

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

什么是Java中的内存泄漏:

下面,我们就可以描述什么是内存泄漏。在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄漏。

通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。

 

java 怎么避免full gc java如何避免内存泄露_vector_02

因此通过以上分析,我们知道在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=0;i<100;i++){
         Object o = new Object();
         v.add(o);
         o=null;
}

此时,所有的Object对象都没有被释放,因为变量v引用这些对象。

用弱引用堵住内存泄漏:

要让GC回收程序不再使用的对象,对象的逻辑生命周期(应用程序使用它的时间)和该对象拥有的引用的实际生命周期必须是相同的。在大多数时候,好的软件工程技术保证这是自动实现的,不用我们对对象生命周期问题花费过多心思。但是偶尔我们会创建一个引用,它在内存中包含对象的时间比我们预期的要长得多,这种情况称为无意识的对象保留。

全局Map造成的内存泄漏:

无意识对象保留最常见的原因是使用Map将元数据与临时对象transient object相关联。假定一个对象具有中等生命周期,比分配它的那个方法调用的生命周期长,但是比应用程序的生命周期短,如客户端的套接字连接。需要将一些元数据与这个套接字关联,如生成连接的用户标识。在创建Socket时是不知道这些信息的,并且不能将数据添加到Socket对象上,因为不能控制Socket类或者它的子类。这时,典型的方法就是在一个全局Map中存储这些信息,如下面清单1中的SocketManager类所示:

清单1.使用一个全局M安排将元数据关联到一个对象

public class SocketManager{
            private Map<Socket,User> m = new HashMap<Socket,User>();
            public void setUser(Socket s, User u){
                       m.put(s,u);
}
public void getUser(Socket s){
            return m.get(s);
}
public void removeUser(Socket s){
            m.remove(s);
}
}

这种方法的问题是元数据的生命周期需要与套接字的生命周期挂钩,但是除非准确地知道什么时候程序不再需要这个套接字,并记住从 Map 中删除相应的映射,否则,Socket 和 User 对象将会永远留在 Map 中,远远超过响应了请求和关闭套接字的时间。这会阻止 Socket 和 User 对象被垃圾收集,即使应用程序不会再使用它们。这些对象留下来不受控制,很容易造成程序在长时间运行后内存爆满。除了最简单的情况,在几乎所有情况下找出什么时候 Socket 不再被程序使用是一件很烦人和容易出错的任务,需要人工对内存进行管理。

弱引用来救援了!

SocketManager的问题是Socket-User映射的生命周期应当与Socket的生命周期相匹配,但是语言没有提供任何容易的方法实施这项规则。这使得程序不得不使用人工内存管理的老技术。垃圾收集器提供了一种声明这种对象生命周期依赖性的方法,这样垃圾收集器就可以帮助我们防止这种内存泄漏----利用弱引用。

弱引用是对一个对象(称为referent)的引用的持有者。使用弱引用后,可以维持对referent的引用,而不会阻止它被垃圾收集。当垃圾收集器跟踪堆的时候,如果对一个对象的引用只有弱引用,那么这个referent就会成为垃圾收集器的候选对象,就像没有任何剩余的引用一样,而且所有剩余的弱引用都被消除。

WeakReference的referent是在构造时设置的,在没有被清楚之前,可以用get()获取它的值。如果弱引用被清楚了,不管是referent已经被垃圾收集了,还是有人调用了WeakReference.clear(),get()会返回null。相应地,在使用其结果之前,应当总是检查get()是否返回一个非null值,因为referent最终总是会被垃圾收集的。

用一个普通的(强)引用拷贝一个对象引用时,限制 referent 的生命周期至少与被拷贝的引用的生命周期一样长。如果不小心,那么它可能就与程序的生命周期一样 —— 如果将一个对象放入一个全局集合中的话。另一方面,在创建对一个对象的弱引用时,完全没有扩展 referent 的生命周期,只是在对象仍然存活的时候,保持另一种到达它的方法。

弱引用对于构造弱集合最有用,如那些在应用程序的其余部分使用对象期间存储关于这些对象的元数据的集合 —— 这就是 SocketManager 类所要做的工作。因为这是弱引用最常见的用法,WeakHashMap 也被添加到 JDK 1.2 的类库中,它对键(而不是对值)使用弱引用。如果在一个普通 HashMap 中用一个对象作为键,那么这个对象在映射从 Map 中删除之前不能被回收,WeakHashMap 使您可以用一个对象作为 Map 键,同时不会阻止这个对象被垃圾收集。清单 5 给出了 WeakHashMap 的 get() 方法的一种可能实现,它展示了弱引用的使用:

清单2. WeakReference.get() 的一种可能实现

public class WeakHashMap<K,V> implements Map<K,V> {
    private static class Entry<K,V> extends WeakReference<K> 
      implements Map.Entry<K,V> {
        private V value;
        private final int hash;
        private Entry<K,V> next;
        ...
    }
    public V get(Object key) {
        int hash = getHash(key);
        Entry<K,V> e = getChain(hash);
        while (e != null) {
            K eKey= e.get();
            if (e.hash == hash && (key == eKey || key.equals(eKey)))
                return e.value;
            e = e.next;
        }
        return null;
    }

调用 WeakReference.get() 时,它返回一个对 referent 的强引用(如果它仍然存活的话),因此不需要担心映射在 while 循环体中消失,因为强引用会防止它被垃圾收集。WeakHashMap 的实现展示了弱引用的一种常见用法----一些内部对象扩展 WeakReference。其原因在下面一节讨论引用队列时会得到解释。

在向 WeakHashMap 中添加映射时,请记住映射可能会在以后“脱离”,因为键被垃圾收集了。在这种情况下,get() 返回 null,这使得测试 get() 的返回值是否为 null 变得比平时更重要了。

用 WeakHashMap 堵住泄漏

在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如清单3所示。(如果 SocketManager 需要线程安全,那么可以用 Collections.synchronizedMap() 包装 WeakHashMap)。当映射的生命周期必须与键的生命周期联系在一起时,可以使用这种方法。不过,应当小心不滥用这种技术,大多数时候还是应当使用普通的 HashMap 作为 Map 的实现。

清单3. WeakHashMap 修复 SocketManager

public class SocketManager {
    private Map<Socket,User> m = new WeakHashMap<Socket,User>();
    
    public void setUser(Socket s, User u) {
        m.put(s, u);
    }
    public User getUser(Socket s) {
        return m.get(s);
    }
}