Effective Java(二):如何消除过期的对象引用

前言

你能看出以下代码哪里内存泄漏吗?

// Can you spot the "memory leak"?
public class 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];
}

/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

分析与解答

答案是:

pop() 方法存在内存泄漏。

内存泄漏可以称为 “ 无意识的对象保持(unintentional object retention)”。

在 pop() 方法中从栈中弹出来的对象将不会被当做垃圾回收。栈内部维护着对这些对象的过期引用(obsolete reference)。所谓的过期引用,是指永远也不会再被解除的引用。凡是在 elements 数组的 “活动部分”(active portion)之外的任何引用都是过期的。活动部分是指 elements 中下标小于 size 的那些元素。

解决方法:上述述例子中的 Stack 类而言,只要一个单元被弹出栈,指向它的引用就过期了。一旦数组元素变成了非活动部分的一部分,就手工清空这些数组元素。修改后的 pop() 方法如下:

public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];//result 相当于一个 temp。
elements[size] = null; // 将引用置为 null,引用所指向的对象将被 GC 回收
return result;
}

清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。

延伸阅读

length、size()、length() 的区别:

  • 数组的长度(length):数组能容纳元素个数的值
  • 泛型集合的大小(size()):泛型中元素的个数
  • 字符串的长度(length()):字符的个数

前缀递减和后缀递增:

  • 前缀递减,”–” 操作符位于变量或表达式前,先执行运算,再生成值。如上例中 elements[--size],size 大小先减 1,所以 Object result = elements[--size]; 中 result 元素下标为 size=size-1。
  • 后缀递增,”++” 操作符位于变量或表达式后,先生成值,再执行运算。如上例中 elements[size++] = e;,元素下标为 size,再执行运算 size=size+1。

Arrays.copyOf() 方法:

  • a copy of the original array, truncated or padded with nulls to obtain the specified length(原始数组的副本,缩短或填补 null 来获取指定的长度)。
  • 作用:如果数组元素的个数等于数组的长度,新建副本数组,将长度扩大为两倍加一,将数组副本赋值给 elements。

参考资料

DeppWang wechat
个人公众号