06-消除过期对象引用
01 - 当你从手工管理内存的语言转换到具有垃圾回收功能的语言的时候,程序员的工作会变得更加容易,因为当你用完了对象之后,它们会被自动回收。当你第一次经历对象回收功能的时候,会觉得这简直有点不可思议。这很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了。其实不然,
考虑下面的简单的栈实现的例子:
/**
* @author: Hashub小晖
* @WeChat: NGSHMVP
* @Date: 2018/10/4 21:51
* @function:
*/
public class Stack
{
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INTTIAL_CAPACITY = 16;
public Stack()
{
elements = new Object[DEFAULT_INTTIAL_CAPACITY];
}
public void push(Object e)
{
ensureCapacity();
elements[size++] = e;
}
public Object pop()
{
if (size == 0)
{
throw new EmptyStackException();
}
return elements[--size];
}
private void ensureCapacity()
{
if (elements.length == size)
{
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
02 - 这段程序中并没有很明显的错误。无论如何测试,它都会成功地通过每一项测试,但是这个程序中隐藏着一个问题。不严格地讲,这段程序有一个“内存泄漏”,随着垃圾回收器活动的增加,或者由于内存占用的不断增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄漏会导致磁盘交换(Disk Paging),甚至导致程序失败(OutOfMemoryError错误),但是这种失败情形相对比较少见。
03 - 如果一个栈先是增长,然后再收缩,那么从栈中弹出来的对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会被回收。这是因为,栈内部维护者对这些对象的过期引用(obsolete reference)。所谓的过期引用,是指永远也不会再被解除的引用。在本例中,凡是elements数组的“活动部分(activity portion)”之外的任何引用都是过期的。活动部分是指elements中下标小于size的那些元素。
04 - 在支持垃圾回收的语言中,内存泄漏是隐蔽的(称这类内存泄漏为“无意识对象保持(UNintentionl object retention)”更为恰当)。如果一个对象引用被无意识地保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。即使只有少量的几个对象引用被无意识地保留下来,也会有许许多多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。
05 - 这类问题的修复方法很简单:一旦对象引用已经过期,只需要清空这些引用即可。对于上述例子中的Stack类而言,只要一个单元被弹出栈,指向它的引用就过期了。pop方法的修订版本如下所示:
/**
* pop方法修订
*
* @return
*/
public Object pop2()
{
if (size == 0)
{
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;//Eliminate obsolete reference
return elements[--size];
}
06 - 情况过期引用的另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出NullPointerException异常 ,而不是悄悄地错误运行下去。尽快地检测出程序中的错误总是有益的。
07 - 当程序员第一次被类似这样的问题困扰的时候,他们往往会过分小心;对于每一个对象引用,一旦程序不再用到它,就把它清空。其实这样做既没必要,也不是我们期望的,因为这样做会把代码弄得很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量,这种情形就会自然而然地发生。
08 - 简而言之,问题在于,Stack类自己管理内存(manage its own memory)。存储池(storage pool)包含了elements数组(对象引用单元,而不是对象本身)的元素。数组获得区域(同前面的定义)中的元素是已分配的(allocated),而数组其余部分的元素则是自由的(free)。但是垃圾回收器并不知道这一点:对于垃圾回收器而言,elements数组中的所有对象引用都等同有效,只有程序员知道数组的非活动部分是不重要的。程序员可以把这个情况告诉垃圾回收器,做法很简单:一旦数组元素变成了非活动部分的一部分,程序员就手工清空这些数组元素。
、
09 - 一般而言,只有类是自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
010 - 内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它不再有用之后很长一段时间内仍然留在缓存中。对于这个问题,有几种可能的解决方案。如果你正要实现这杨的缓存:只要在缓存之外存在某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存:当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的对象周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
011 - 更为常见的情形则是,“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清除掉没有用的项。这项清除工作可以由一个后台线程(可能是Timer或者ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap类利用他的removeEldestEntry方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref。
012 - 内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却却没有现实地取消注册,那么除非你采取某些动作,否则它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如:只将它们保存成WeakHashMap中的键。
013 - 由于内存泄漏通常不会表现成明显的失败,所以它们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或者借助于Heap剖析工具(Heap Profiler)才能发现内存泄漏问题。因此,入股能够在内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。
网友评论