本文是 《Effective Java 3》第二章的学习笔记:排除过时的对象引用。
介绍
“Eliminate obsolete object references” 是一条Java编程最佳实践的原则,指的是在代码中及时清理不再使用的对象引用,以避免内存泄漏和性能问题。当一个对象不再需要时,应该尽快将其引用设置为null,这样JVM可以及时回收它所占用的内存。
考虑以下简单的堆栈实现:
import java.util.Arrays;
import java.util.EmptyStackException;
// 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);
}
}
这个程序没有明显的错误。你可以对它进行详尽的测试,它会以优异的成绩通过所有的测试,但是有一个潜在的问题。简单地说,该程序有一个「内存泄漏」问题,由于垃圾收集器活动的增加或内存占用的增加,它可以悄无声息地表现为性能的降低。在极端情况下,这种内存泄漏可能导致磁盘分页,甚至出现 OutOfMemoryError 程序故障,但这种故障相对少见。
那么内存泄漏在哪里呢?如果堆栈增长,然后收缩,那么从堆栈中弹出的对象将不会被垃圾收集,即使使用堆栈的程序不再引用它们。这是因为栈保留了这些对象的旧引用。一个过时的引用,是指永远不会被取消的引用。在本例中,元素数组的「活动部分」之外的任何引用都已过时。活动部分由索引小于大小的元素组成。
垃圾收集语言中的内存泄漏(更确切地说是无意的对象保留)是暗藏的风险。如果无意中保留了对象引用,那么对象不仅被排除在垃圾收集之外,该对象引用的任何对象也被排除在外,依此类推。即使只是无意中保留了一些对象引用,许多许多的对象也可能被阻止被垃圾收集,从而对性能产生潜在的巨大影响。
解决这类问题的方法很简单:一旦引用过时,就将置空。在我们的 Stack 类中,对某个项的引用一旦从堆栈中弹出就会过时。pop 方法的正确版本如下:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
用 null 处理过时引用的另一个好处是,如果它们随后被错误地关联引用,程序将立即失败,出现 NullPointerException,而不是悄悄地做错误的事情。尽可能快地检测编程错误总是有益的。
那么,什么时候应该取消引用呢?Stack 类的哪些方面容易导致内存泄漏?简单地说,它管理自己的内存。存储池包含元素数组的元素(对象引用单元,而不是对象本身)数组的活动部分(如前面所定义的)中的元素被分配,而数组其余部分中的元素是空闲的。垃圾收集器没有办法知道这一点;对于垃圾收集器,元素数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。只要数组元素成为非活动部分的一部分,程序员就可以通过手动清空数组元素,有效地将这个事实传递给垃圾收集器。
一般来说,一个类管理它自己的内存时,程序员应该警惕内存泄漏。当释放一个元素时,该元素中包含的任何对象引用都应该被置为 null。
另一个常见的内存泄漏源是缓存。 一旦将对象引用放入缓存中,就很容易忘记它就在那里,并且在它变得无关紧要之后很久仍将它留在缓存中。有几个解决这个问题的办法。如果你非常幸运地实现了一个缓存,只要缓存外有对其键的引用,那么就将缓存表示为 WeakHashMap;当条目过时后,条目将被自动删除。记住,WeakHashMap 只有在缓存条目的预期生存期由键的外部引用(而不是值)决定时才有用。
更常见的情况是,缓存条目的有效生存期定义不太好,随着时间的推移,条目的价值会越来越低。在这种情况下,缓存偶尔应该清理那些已经停用的条目。这可以通过后台线程(可能是 ScheduledThreadPoolExecutor
)或向缓存添加新条目时顺便完成。LinkedHashMap 类通过其 removeEldestEntry
方法简化了后一种方法。对于更复杂的缓存,你可能需要直接使用 java.lang.ref
。
内存泄漏的第三个常见来源是侦听器和其他回调。 如果你实现了一个 API,其中客户端注册回调,但不显式取消它们,除非你采取一些行动,否则它们将累积。确保回调被及时地垃圾收集的一种方法是仅存储对它们的弱引用,例如,将它们作为键存储在 WeakHashMap 中。
如何排除过时对象引用
以下是一些示例,展示了如何使用Java语言中的一些技术来排除过时的对象引用。
- 在循环中使用局部变量
List<String> list = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
String str = list.get(i);
// do something with str
}
在这个示例中,我们使用局部变量str
来引用列表中的元素,而不是重复地使用list.get(i)
。这样,当循环结束时,str
的引用将被自动清除,避免了过时的对象引用。
- 在使用完对象后及时清除引用
SomeObject obj = new SomeObject();
// do something with obj
obj = null; // clear the reference to obj
在这个示例中,我们在使用完对象后立即将其引用设置为null,以便JVM可以及时回收它所占用的内存。如果不清除引用,对象将一直存在于内存中,直到JVM进行垃圾回收。
- 使用弱引用
WeakReference<SomeObject> ref = new WeakReference<>(new SomeObject());
SomeObject obj = ref.get();
// do something with obj
obj = null; // clear the reference to obj
在这个示例中,我们使用了一个弱引用来引用对象,以便在对象不再被强引用时可以被及时回收。当我们需要使用对象时,可以通过弱引用获取对象的引用,使用完后及时将其引用设置为null。
- 使用try-with-resources语句
try (InputStream in = new FileInputStream("file.txt")) {
// do something with in
}
在这个示例中,我们使用了try-with-resources语句来打开一个文件流,并在使用完后自动关闭它。这样可以确保在不再需要文件流时,它的引用将被清除,避免了过时的对象引用。
- 使用软引用
SoftReference<SomeObject> ref = new SoftReference<>(new SomeObject());
SomeObject obj = ref.get();
// do something with obj
obj = null; // clear the reference to obj
在这个示例中,我们使用了一个软引用来引用对象,以便在JVM需要回收内存时可以回收对象。软引用在内存不足时通常会被回收,但在内存充足时可以保留对象,避免了过时的对象引用。
- 使用虚引用
ReferenceQueue<SomeObject> queue = new ReferenceQueue<>();
PhantomReference<SomeObject> ref = new PhantomReference<>(new SomeObject(), queue);
// do something
ref.clear(); // clear the reference to obj
在这个示例中,我们使用了一个虚引用来引用对象,以便在JVM回收对象之前可以进行一些必要的清理工作。虚引用在JVM回收对象时会被添加到一个引用队列中,因此我们可以在对象被回收之前执行必要的清理工作。
- 使用对象池
class ObjectPool<T> {
private final Set<T> objects = new HashSet<>();
public synchronized T getObject() {
T obj;
if (objects.isEmpty()) {
obj = createObject();
} else {
obj = objects.iterator().next();
objects.remove(obj);
}
return obj;
}
public synchronized void returnObject(T obj) {
objects.add(obj);
}
private T createObject() {
// create a new object
}
}
在这个示例中,我们使用了一个对象池来管理对象的生命周期。当需要一个对象时,我们从对象池中获取一个对象,而不是每次都创建一个新的对象。当不再需要对象时,我们将其返回到对象池中,以便其他对象可以重复使用。对象池可以避免过时的对象引用,并提高代码的性能和可伸缩性。
- 使用弱散列映射
Map<SomeObject, Object> map = new WeakHashMap<>();
SomeObject key = new SomeObject();
Object value = new Object();
map.put(key, value);
// do something
key = null; // clear the reference to key
// do something
// the entry in the map may be removed if key is not strongly referenced elsewhere
在这个示例中,我们使用了一个弱散列映射来存储对象引用和相应的值。当对象不再被强引用时,它的引用可能被从映射中删除,从而避免了过时的对象引用。弱散列映射通常用于缓存和事件处理等场景。
- 使用缓存
class SomeObjectCache {
private static final int MAX_SIZE = 100;
private static final Map<String, SomeObject> cache = new LinkedHashMap<String, SomeObject>(MAX_SIZE, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry<String, SomeObject> eldest) {
return size() > MAX_SIZE;
}
};
public synchronized static SomeObject get(String key) {
return cache.get(key);
}
public synchronized static void put(String key, SomeObject value) {
cache.put(key, value);
}
}
在这个示例中,我们使用了一个缓存来存储对象引用和相应的值。当缓存达到最大大小时,最旧的条目将被自动删除,从而避免了过时的对象引用。缓存通常用于频繁访问的数据和计算结果,可以提高代码的性能和可伸缩性。
- 使用对象池框架
对象池框架是一种用于管理对象生命周期的通用框架,可以避免过时的对象引用和提高代码的性能和可伸缩性。一些流行的对象池框架包括Apache Commons Pool和Google Guava Cache。
扩展
弱引用、软引用和虚引用区别
弱引用、软引用和虚引用是Java中三种不同类型的引用,它们之间的区别如下:
弱引用(WeakReference)
弱引用是一种较弱的引用类型,当一个对象只被弱引用所引用时,它在下一次垃圾回收时会被回收。
弱引用通常用于需要缓存大量对象的应用场景,例如缓存和高速缓存等。在这些场景中,使用弱引用可以避免占用过多的内存,同时又可以快速访问缓存中的对象。
应该使用弱引用的情况包括:
需要缓存大量对象:使用弱引用可以避免占用过多的内存空间,从而可以缓存更多的对象。
不需要快速访问缓存中的对象:由于弱引用只有在下一次垃圾回收时才会被回收,因此可能会导致较长的访问延迟。如果应用程序可以容忍这种情况,可以考虑使用弱引用。
需要频繁的垃圾回收:由于弱引用只有在下一次垃圾回收时才会被回收,因此可能会导致频繁的垃圾回收。如果应用程序可以容忍这种情况,可以考虑使用弱引用。
例如,我们可以使用弱引用来实现一个缓存,当内存不足时,JVM会自动回收弱引用所引用的对象,从而避免内存泄漏和OOM错误。
Map<String, WeakReference<SomeObject>> cache = new HashMap<>();
public SomeObject getObject(String key) {
SomeObject obj = null;
WeakReference<SomeObject> reference = cache.get(key);
if (reference != null) {
obj = reference.get();
}
if (obj == null) {
obj = createObject();
if (obj != null) {
cache.put(key, new WeakReference<>(obj));
}
}
return obj;
}
在这个示例中,我们使用弱引用来缓存对象,当内存不足时,JVM会自动回收弱引用所引用的对象。这样可以避免占用过多的内存,同时又可以快速访问缓存中的对象。
软引用(SoftReference)
软引用是一种较强的引用类型,当一个对象只被软引用所引用时,只有在内存不足时才会被回收。
软引用通常用于需要缓存大量对象的应用场景,例如图片缓存、数据缓存等。在这些场景中,使用软引用可以避免占用过多的内存,同时又可以快速访问缓存中的对象。
应该使用软引用的情况包括:
需要缓存大量对象:使用软引用可以避免占用过多的内存空间,从而可以缓存更多的对象。
需要快速访问缓存中的对象:使用软引用可以快速访问缓存中的对象,避免频繁地加载和计算。
可以容忍偶尔的垃圾回收:由于软引用只有在内存不足时才会被回收,因此可能会导致偶尔的垃圾回收。如果应用程序可以容忍这种情况,可以考虑使用软引用。
例如,我们可以使用软引用来实现一个图片缓存,当内存不足时,JVM会自动回收软引用所引用的对象,从而避免内存泄漏和OOM错误。
Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();
public Bitmap loadImage(String url) {
Bitmap bitmap = null;
SoftReference<Bitmap> reference = imageCache.get(url);
if (reference != null) {
bitmap = reference.get();
}
if (bitmap == null) {
bitmap = downloadImage(url);
if (bitmap != null) {
imageCache.put(url, new SoftReference<>(bitmap));
}
}
return bitmap;
}
在这个示例中,我们使用软引用来缓存图片,当内存不足时,JVM会自动回收软引用所引用的对象。这样可以避免占用过多的内存,同时又可以快速访问缓存中的图片。
虚引用(PhantomReference)
虚引用是Java中四种引用类型中最弱的一种,它主要用于跟踪对象被垃圾回收的状态。虚引用本身并不会对对象的生命周期产生影响,但可以在对象被垃圾回收时收到一个通知,从而进行一些清理或其他操作。
虚引用的使用场景比较少,一般用于以下几个方面:
- 对象的finalize()方法: 虚引用可以用于实现对象的finalize()方法,当对象被垃圾回收时,虚引用会收到一个通知,从而触发对象的finalize()方法。
- NIO DirectByteBuffer对象的释放: 在使用NIO编程时,可能会创建大量的DirectByteBuffer对象,这些对象可能会占用大量的内存空间。当这些对象不再使用时,需要手动调用System.gc()方法触发一次垃圾回收,才能释放这些对象的内存。使用虚引用可以避免手动调用System.gc()方法,当这些对象被垃圾回收时,虚引用会收到一个通知,从而释放这些对象的内存。
- 对象池的管理: 在一些需要频繁创建和销毁对象的应用场景中,可以使用对象池来提高性能。当对象不再使用时,可以将对象放入虚引用中,当对象被垃圾回收时,虚引用会收到一个通知,从而将对象从对象池中移除。
需要注意的是,虚引用不适用于缓存或其他需要快速访问对象的应用场景,因为虚引用本身并不保证对象的可用性和可访问性。
因此,软引用和弱引用的主要区别在于它们的强度和垃圾回收的时机。软引用比弱引用更强,只有在内存不足时才会被回收,而弱引用则更弱,只有在下一次垃圾回收时才会被回收。同时,使用软引用可能会导致更少的垃圾回收,但可能会占用更多的内存空间,而使用弱引用可能会导致更频繁的垃圾回收,但可以更快地释放内存空间。
弱引用是否会影响程序的性能?
弱引用可能会影响程序的性能,因为它们可能会导致频繁的垃圾回收。
由于弱引用只有在下一次垃圾回收时才会被回收
,因此当使用大量的弱引用时,可能会导致更频繁的垃圾回收,从而降低程序的性能。
当一个对象只被弱引用所引用时,在下一次垃圾回收时它会被回收。如果应用程序中存在大量的弱引用对象,每次垃圾回收都需要扫描这些对象,从而增加了垃圾回收的时间和开销。
因此,在使用弱引用时需要注意以下几点:
- 不要过度使用弱引用:如果应用程序中存在大量的弱引用对象,可能会导致频繁的垃圾回收,从而影响程序的性能。因此,应该避免过度使用弱引用,尽可能减少弱引用对象的数量。
- 注意垃圾回收的时机:弱引用只在下一次垃圾回收时才会被回收,因此可能会导致较长的访问延迟。在使用弱引用时需要注意垃圾回收的时机,如果应用程序需要快速访问缓存中的对象,可能需要使用其他类型的引用。
- 检查弱引用是否被回收:当一个对象只被弱引用所引用时,它在下一次垃圾回收时会被回收。在使用弱引用时需要注意检查弱引用对象是否被回收,避免引用无效的对象。
如何检查弱引用对象是否被回收?
在Java中,可以通过获取弱引用对象的get()方法返回的对象来检查引用对象是否被回收。当一个弱引用所引用的对象被回收后,get()方法返回的对象将为null。
例如,以下示例代码演示了如何使用弱引用检查对象是否被回收:
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
// 检查对象是否被回收
if (weakRef.get() != null) {
// 对象未被回收
System.out.println("Object is alive");
} else {
// 对象已被回收
System.out.println("Object has been collected");
}
在这个示例中,我们创建了一个对象,并使用弱引用来引用它。然后,我们通过检查弱引用对象的get()方法返回的对象来判断对象是否被回收。
当对象未被回收时,get()方法返回的对象不为null,表示对象仍然存活。当对象被回收时,get()方法返回的对象为null,表示对象已经被回收。
需要注意的是,由于弱引用只在下一次垃圾回收时才会被回收,因此在使用弱引用检查对象是否被回收时,需要注意垃圾回收的时机。如果应用程序需要立即检查对象是否被回收,可以手动触发一次垃圾回收,例如通过System.gc()
方法来触发。