关于Java中内存泄露问题的研究

时间:2022-06-21 11:53:03

关于Java中内存泄露问题的研究

摘要:越来越多的企业级应用系统采用Java技术开发,这些系统往往长时间运行,哪怕是很小量的内存泄露也有可能导致系统的崩溃,因此内存泄露的问题不容忽视。文章详细分析了Java系统产生内存泄露的原因和表现形式,提出了预防方法和解决方法。

关键词:Java;内存泄露;内存管理;垃圾收集(GC)

中图分类号:TP312文献标识码:A文章编号:1009-3044(2009)14-3794-03

Research on Memory Leak of Java

ZHU Qiang, CHENG Xiao-hui

(Electronics and Computer Science Department & Guilin University of Technology, Guilin Guangxi, 541004)

Abstract: An increasing number of enterprise-class applications using Java technology development, these systems are often long-running, even a small amount of memory leak could lead to system collapse, so the memory leak problem can not be ignored. This paper detailly analyzes the causes and manifestation forms of memory leak which are produced by Java system, andproposes methods of prevention and solutions.

Key words: Java; memory leak; memory management; gargbage collection(GC)

1 引言

随着人们对网络程序的安全性要求越来越高,Java以其高安全性的特点迅速成为现代最流行的高级编程语言之一。尤其是它特有的内存管理机制――垃圾收集器(Gargbage Collector,GC),减轻了程序员的负担,减少了许多内存泄露的可能性,提高了程序的安全性。然而,这并不是说在Java中不存在内存泄露的问题,只是Java的内存泄露比较隐蔽,为了提高程序的安全性和稳定性,Java中的内存泄露是值得我们深刻分析一下的。

2 Java内存泄露的概念诠释

内存泄露,通常是指分配出去后却无法回收的内存空间。[1]

2.1 传统语言中的内存泄露

在传统语言(如C/C++等)中内存泄露的范围和发生的可能性是十分大的,程序员需要自行管理内存,如果程序中为变量或对象申请了内存空间,则在不需要时必须调用相应的函数进行显式释放它们占用的内存空间,即使超出变量或对象的作用域,否则这块内存将永远得不到回收直至系统重启。因此可见,传统语言中一旦发生内存泄露,其危害性是不言而喻的。

2.2 Java中的内存泄露

针对传统语言的不足,Java中一个很大的改进就是引入了垃圾回收器(GC)的机制,它使程序员从传统语言复杂的内存管理中解放出来,将更多的精力关注于业务逻辑的开发,程序员只需要用关键字new或者用Java的反射机机制为对象开辟一块内存空间,在对象不再使用时,而不需要进行显式的释放,这块空间会被GC自动回收,这种收支两条线的内存管理机制有效地解决了传统语言中的内存泄露问题,极大地提高了编程的效率。尽管如此,GC的引入并不能完全避免Java中的内存泄露。Java中的内存泄露和传统语言中的内存泄露是十分不同的,它是指对象不再被需要时,但却仍被程序无意识地、错误地保持或引用而导致GC无法回收对象所占用的内存空间。因为在GC看来,它们还是“有用”的,即Java中的内存泄露是主观的内存泄露,是由于程序员的水平或一时大意而造成的。

可以用图论来描述Java中的内存泄露。把对象看成是有向图的顶点,引用关系看成是有向图的有向边,有向边从引用对象指向被引用对象,线程对象作为有图的起始顶点,如图1。

static List list=new ArrayList();

public static main(String args[]){

Object o1=new Object();

Object o2=new Object();

list.add(o2);

o2=null;}

通过上图可知,Java中的内存泄露的对象具有以下两个特点[2]:首先,这些对象是可达的,即在有向图中存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。

3 内存泄露的表现形式

由于有了GC的帮助,那些“不可达”的对象将不会再被泄露,Java中内存泄露的机率降到了很低。因此,Java中内存泄露往往并不像传统语言中那样表现得很明显,使程序很快出现致命地错误,但往往会在系统运行一段时间后就会暴露出来。

3.1 瞬间泄露

瞬间泄露是指由于程序在短时间内保持了大量的无用对象的引用而导致堆内存不存或耗尽。它对应用程序来说是致命的,在软件开发过程中一般都能被检测出来,因为不解决它,程序是无法正常运行的。其最最明显的表现形式就是操作过程中程序瞬间抛出java.lang.OutOfMemoryError,即内存溢出。我们可以通过扩大堆内存空间的方式解决瞬间泄露的出现,但内存的增长毕竟是有限的,而且这种解决方式很有可能把瞬间泄露转成下面我们将说的另一种更为隐蔽的泄露形式――缓慢泄露。

3.2 缓慢泄露

缓慢泄露是指程序每次只泄露少量对象,短时间内不足以影响程序的正常运行,但运行时间一长,程序必定会因为内存不足出现java.lang.OutOfMemoryError错误。它具有隐蔽性、泄露周期长的特点,所以在开发过程中最容易被忽视,这部分内存泄露也是Java内存泄露中的最主要形式。下面是一段缓慢泄露的程序。

public class EmulateStack{

private Object[] statck;

private int pointer;

public EmulateStack(int initial){

stack=new Object[initial];

pointer=0;}

public Object outStack(){

pointer--;

return stack[pointer];}

public void intoStack(Object o){

stack[pointer]=o;

pointer++;}

public static void main(String args[]){

EmulateStack es=new EmulateStatck(5);

Object o=new Object();

es.intoStack(o);

es.outStack();})

在上面的例子中,EmulateStack类模拟数据结构中的栈,使用intoStack和outStack方法进行进栈和出栈,pointer指向栈顶位置,在main方法中,初始化了一个大小为5的栈,然后把一个Object对象入栈,接着又把它出栈,这时Object对象占用的空间就被回收,事实上并非如此,outStack方法只是减少了栈顶指针pointer的值,栈中仍然保持着对Object对象的引用,该程序每执行一次,都会泄露一个Object对象。实际上,我们只要修改outStack方法,即可解决内存泄露的问题。修改如下:

public Object outStack(){

pointer--;

Object o=stack[pointer];

stack[pointer]=null;

return o;}

4 内存泄露的原因

4.1 客观原因

主要是由于GC的机制所决定的,GC和程序员对垃圾的认知角度是不一样的。在GC看来,凡是不可达的对象都是垃圾,凡是有句柄指向的对象都是正在使用的对象,不应该被回收;而在程序员看来,程序不再需要使用的对象都是垃圾,而实际上,这些“所谓的垃圾”还是被某些正在使用的对象引用着的,程序员认为它应该被回收,而GC却不会回收它们。另外,GC参数的设置不当,也会增大内存泄露的可能性。

4.2 主观原因

主要是由于程序员的编程水平或疏忽大意而错误地、无意识地保持着某些无用对象的引用而造成的,这在Java内存泄露中十分常见。

List list=new ArrayList();

for(int i=0;i

Object o=new Object();

list.add(o);

o=null;}

在上面的例子中,程序循环申请Object对象,然后将对象加入一个List容器中,然后试图通过o=null将对象所占用的空间释放掉,其实这是不可行的。因为List容器还持有对Object对象的引用,所以GC不会回收这些Object对象,只有用list=null或list.remove(o)才能释放这些对象。

5 内存泄露的解决方法

5.1 提早预防内存泄露

5.1.1 GC调优

不同的JVM采用了不同的垃圾回收机制和启动参数,有的GC是定时启动,有的是当CPU资源空闲时开始收集垃圾,有的是当堆内存不足时才开始收集。因此,优化GC配置对预防内存泄露十分重要。GC的算法和参数对应用程序的影响是十分大的,不适当的垃圾回收机制和参数可能为程序的内存泄露埋下了隐患。

下面将以最流行的JVM――SUN公司的HotSpot虚拟机为例来说明一下GC如何调优。

HotSpot是用“分代”方式来管理堆空间的,它将整个堆空间分成了三块:永久代(Permanent Generation)、年老代(Old Generation)、年轻代(Young Generation)。年老代保存反射创建的对象,年轻代保存刚刚实例化的对象,当年轻代被填满时,GC会将一部分仍存活的年代代对象移入年老代。针对Hotspot的GC,以下几条优化的原则[3]。

1) 最好将-Xms和-Xmx设为相同值,让-Xmn的值等于-Xmx的1/3;

2) 一个GUI程序最好是每10到20秒间运行一次GC,每次在半秒内完成;

3) 增加Heap空间的大小虽然会降低GC的频率,但也增加了每次GC的时间,并且GC运行时所有的用户线程被暂停,也就是GC期间,Java应用程序不做任何工作;

4) 尽可能增大Heap空间,除非应用程序遇到了较长的响应时间;

5.1.2 良好的编程习惯

高效优质的代码可以在很大程度上减少内存泄露的可能性。为了避免内存泄露,最主要的原则就是尽早释放对“无用”对象的引用,即在对象不再需要时,用“对象=null”的方式显式释放对象,以便GC能尽早回收它所占用的内存空间。许多程序员在使用临时变量时,总是让它在退出作用域后自动释放所引用的对象,这对于一些逻辑结构简单的程序可能影响并不大,但对引用关系较为复杂的大型应用,就有可能对临时变量还持用一些错误引用而导致临时对象不能被释放。下面给出几条提高编码效率的建议。[4]

①尽量少用临时对象,临时对象的存活周期非常短,很快就会变成垃圾,它会使GC频繁启动,从而降低应用程序的性能。

②尽量不要显式调用System.gc(),因为此方法只是建议JVM进行垃圾回收,至于什么时候回收还是不确定的,JVM可能会在不该进行回收时而启动GC,导致应用程序临时中断。

③尽量少用finalize方法,它会使GC的收集时间增长。

④对象在使用时再实例化,无用时尽早释放对象的引用,即对象句柄=null。

⑤尽量避免在类的构造函数中创建大量对象,防止在调用其自类的构造方法时造成不必要的内存资源占用。

⑥尽量不要显式申请数组空间,这样会造成堆空间浪费。

⑦能用基本类型的就不要用封装类型,如能用int型的,就不要用Integer类型。

⑧避免过深的类层次结构和过深的方法调用,因为这两者都是十分耗内存的。

⑨对于字符串的操作,尽量用StringBuffer类的appand方法,不要使用String及+,因为对String的每次操作都会产生新的对象。

⑩尽量少用static变量,因为它属于全局变量,直到应用程序退出才会被GC回收。

5.2 内存泄露的检测

1) 代码走查:它是安排有经验的开发人员或对整个程序代码很了解的人员对系统进行仔细排查,找到内存泄露的地方。它对于引用关系不是太复杂的小型系统往往十分有效。

2) 利用专业工具:市场上检测Java内存泄露的工具十分多,如JDK6.0的命令行工具JPS,Borland公司的OptimizeIt,Ej-technologies公司的Jprofiler等,它们的工作原理大同小异,都是通过监测Java程序运行时所有对象的创建、释放等动作,将内存管理的所有信息进行统计、分析、可视化,开发人员将根据这些信息判断程序是否有内存泄露的问题。下面简单介绍一下Jprofiler查找内存泄露的基本思路。[5]

Jprofiler 5.1.3是一个全功能的Java剖析工具,专用于分析J2SE和J2EE应用程序,它直觉式的GUI让你可以找到效率瓶颈,抓出内存泄露,并解决执行绪的问题。Jprofiler的内存视图就是用来观察系统运行时堆内存的大小,实际使用的大小和各个类的实例分配个数。如图2,各列自左到右分别为类名称、当前实例个数、自上次标记点增长或减少的实例个数、占用内存的大小,最下一行是当前JVM的汇总数据。

在现实生产中,可以分别在系统运行2小时为间隔点,点击“快照”按钮,记录消退时的内存状态,抓取当时的内存快照,找出对象个数增长比较靠前的类,记录这些类的当前对象个数,记录数据后,点击上面的“标记”按钮,将该点的状态作为下一次记录数据的比较点,一个正常的系统其运行时的内存占用量一般是比较稳定的,不会随着时间的增长而增长,同样,一个类的对象也是有一个上限值的,不会无限制的增长,我们可以通过得到的内存快照,对这些快照进行综合全面的分析,如果有某类对象的内存占用空间一直都在增长,那么就可以初略认定该类对象可能存在内存泄露,接下来,我们再只对这些可疑对象进行仔细监控分析,必定会找到内存泄露的对象和地方。

6 结论

综上所述,Java的内存泄露主要是由于一些无用对象被错误地保持着,导致它们的空间不能被GC回收造成的。因此,它经常并不容易被发现,本文旨在帮助大家更容易地找出内存泄露,解决性能瓶颈,提高程序的稳定性。

参考文献:

[1] 陈小玉.Java内存泄漏泄露问题的改进与研究[J].微型电脑应用,2005,21(7).

[2] 关锋,卢铁,关威.关于 Java的内存泄漏[J].信息技术,2003,27(6).

[3] Jonathan Knudsen, Patrick Niemeyer. Learning Java, 3rd Edition[M].O' Reilly, 2005.

[4] 于海雯,刘萍等.Java的内存管理与垃圾收集机制分析[J].电脑知识与技术,2006,20.

[5] 朱颖芳.关于Java语言内存泄漏问题的探讨[J].电脑知识与技术,2006(32).

上一篇:CAD技术在建筑工程教学中的应用 下一篇:AT89S52在多功能测量仪表中的应用