当你从手工管理内存的语言(比如或C++)转换到遇有垃圾回收功能的语言的时候,程序员的工作会变的更加容易,因为当你用完了对象之后,他们会自动回收。当你第一次经历对像回收功能的时候
,会觉的这有点不可思议。这很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了。其实不然:
考虑下面这个简单的栈实现的例子:
public calss Stack{
private Object[] elements;
private int size=0;
private static final int DEFAULT_INITIAL_CAPACITY=16;
public Stack(){
elements=new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++]=e;
}
public Object pop(){
if(size==0)
throw new EmptyStackException();
return elements[--size];
}
private ovid ensureCapacity(){
if(elements.length==size)
elements=Arrays.copyOf(elements,2*size+1);
}
}
这段程序中并没有明显的错误,无论如何测试,它都会成功地通过每一项测试,但是这个程序中隐含着一个问题。不严格的讲,这段程序有一个“内存泄露”的问题,随着垃圾收集器的
的活动增加,或者由于内存占用的不断增加,程序性能的降低会逐渐显现出来。在极端的情况下,这种内存泄露会导致磁盘交换,甚至导致程序失败(OOm),但是这种失败情形相对
较少。
那么,程序中那里出现了内存泄露的问题呢?如果一个栈先是增长,然后是再收缩,那么,从栈中弹出来的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,
它们也不会被回收。这是因为,栈内部维护着这些过期对象的引用。所谓的过期引用,是指永远也不会再被解除的引用。在本例中,凡是在elements数组的"活动部分"之外的任何引用
都是过期的。活动部分是指elements中下标小于size的那些元素。
在支持垃圾回收的语言中,内存泄露是隐蔽的(称这类内存泄露为"无意识的对象保持"更为恰当)。如果一个对象引用被无意识的保留起来了,那么垃圾回收机制不仅不会回收
这个对象,而且也不会处理这个对象所引用的所有其他对象。即使只有几个少量的对象引用被无意识的保留下来,也会有许许多多的对象被排除在垃圾回收机制之外,从而对象性能
造成潜在的影响。
这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可。对于上述例子中的Stack 类而言,只要一个单元被弹出栈,指向它的引用就过期。pop方法的修订
版如下所示:
public Object pop(){
if(size==0)
throw new EmptyStackException();
Object result=elements[--size];
elements[size]=null;
return result;
}
当程序员第一次被类似的问题困扰的时候,他们往往会过分小心:对于没一个对象引用,一旦程序不再用他,就把它清空。其实这样做既没必要,也不是我们所期望的,因为这样做
会把程序代码弄的很乱。清空对象引用应该只是一个例外,而不是一种规范行为。消除过期对象引用最好的方法是让包含该应用的变量结束器生命周期。如果你是在最紧凑的作用
域范围内定义每一个变量,这种情形就会自然而然的发生。
那么何时应该清空引用呢? Stack 类的哪方面特性使它容易遭受内存泄露的影响呢?简而言之,问题在于Stack类自己管理内存,存储池包含了elements数组(对象引用单元,而不是对象
本身)的元素,elements数组中的所有对象引用都同等有效。只有程序员知道数组的非活动部分是不重要的。程序员可以把这情况告诉垃圾回收器,做法很简单;一旦数组元素变成
了非活动部分的一部分,程序员就会手工清空这些数组元素。
一般而言,只要类是自己管理内存,程序员就应该警惕内存泄露问题。一旦元素被释放掉,则该元素中包含的对象引用都应该被清空。
内存泄露的另一个常见来源是缓存。一旦你把对象应用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间保留在缓存中。对于这个问题,有几种可能的解决
方案。如果你正好要实现这样的缓存;只要缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,他们就会
被自动删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
更为常见的情则是,“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变的越来越没有价值。在这种情况下,缓存应该时不时清除没用的
项。这项清除工作可以由一个后台线程(可能是Timer或者ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap类利用他的
removeEldestEntry 方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref.
内存泄露的第三个常见来源是监听器和其他回调。如果你实现了一个Api,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则他们就会积聚。
确保回调立即被当作垃圾回收的最佳方法是只保存他们的弱引用,例如,只将他们保存成WeakHashMap中的键。
由于内存泄露通常不会表现成明显的失败,所以他们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现内存泄露的
问题。因此,如果能够在内存泄露发生之前就知道如果预测此类问题,并阻止发生,那是最好不过的了。
关键字:内存泄露,解决方案 | 来源:互联网 | 责任编辑:系统管理员 | 最后编辑:2011年08月21日 21时52分12秒