问题的提出:
Java的一个重要优点就是通过垃圾回收器(Garbage Collection,GC)自动管理内存的回收,程序员不需要通过调用函数来释放内存。因此,很多程序员认为Java不存在内存泄露问题,或者认为即使存在内存泄露也不是程序的责任,而是GC或JVM的问题。其实这种想法是不正确的,因为Java也存在内存泄露,但是它的表现与C++不同。
随着越来越多的服务器程序采用Java语言,例如JSP,Servlet,EJB等,服务器程序往往长期运行。另外,在很多嵌入式系统中,内存的总量非常有限。内存泄露问题也就变的十分关键,即使每次运行只有少量泄露,长期运行之后,系统也会面临崩溃的危险。
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模型采用计数器方式管理构建,它与有向图相比,精度低(很难处理循环引用的问题),但是执行效率高。
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引用这些对象。
如何检测内存泄露
通过专业的工具,例如Optimizeit Profiler, Jprobe Profiler, JinSight, Rational公司的Purify等。