《Effective Java》解读第7条:消除过期的对象引用精华总结
文章目录
- 第7条:消除过期的对象引用精华总结
- 三种经典的内存泄漏场景与解决方案
- 总结
第7条:消除过期的对象引用精华总结
核心思想:
不要过分依赖垃圾回收器(GC)。 虽然Java拥有自动内存管理,但如果程序无意中保留了对象的引用(即“过期的对象引用”),这些对象依然无法被回收,从而导致内存泄漏。
内存泄漏在Java中表现为:对象在逻辑上已不再使用,但由于被意外的引用所持有,导致GC无法回收它们。
三种经典的内存泄漏场景与解决方案
- 类自己管理内存(最常见、最隐蔽)
场景:利用数组实现栈:
// 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);}
}
对于栈而言,elements数组中index >= size的部分是“有效元素”,而index < size的部分是“过期区域”。栈内部知道这些数据已无效,但GC不知道,只要数组还在,这部分引用指向的对象就永远不会被回收。
解决方案: 一旦对象引用不再需要,立即手动将其设置为null。
这种做法不应成为常态。最佳实践是让对象在其作用域结束时自然消亡。 只有在类自己管理内存(明确知道哪些引用已过期)时,才应该使用这种手动清空的方式。
- 缓存
场景:将对象放入缓存后,很容易忘记它们,从而使其长期留在内存中。
解决方法:
使用弱引用(WeakHashMap): 如果你要实现一个缓存,只要缓存项在外部没有其他强引用,就允许GC回收它们,那么WeakHashMap是理想选择。当GC运行时,这些条目会被自动清理。
后台清理: 对于更复杂的缓存,可以使用ScheduledThreadPoolExecutor或类似Caffeine、Guava Cache这样的缓存库,通过定时任务或基于访问频率等策略来定期清理过期条目。
- 监听器和其他回调
场景: 在API中注册了监听器或回调,但没有显式地取消注册。
如果客户端注册了回调却没有注销,那么回调对象(通常持有其外部类的引用)会一直被API持有,导致该客户端及其相关对象都无法被回收。
解决方案:
显式注销: 提供并鼓励使用unregister或removeListener方法。
使用弱引用: 仅保存监听器的弱引用(如WeakReference),这样当监听器对象没有其他强引用时,GC可以回收它,避免内存泄漏。
总结
- 警惕“内存管理器”角色: 只要一个类自己管理内存(如自定义的集合、对象池),你就应该警惕内存泄漏的可能性。
- null引用是“最后手段”: 不要在所有地方都滥用null。它应该作为一种明确的信号,表明“这个引用指向的对象我已明确不再需要,请GC可以回收它”。在绝大多数情况下,让变量自然超出作用域是更清晰、更推荐的做法。
- 优先使用现有工具: 对于缓存,优先考虑使用成熟的库(如Caffeine、Guava Cache),它们已经内置了强大的内存管理策略。
- 代码审查与静态分析: 内存泄漏通常难以通过单元测试发现,但在代码审查中应重点关注上述几种场景。同时,可以使用如FindBugs、SpotBugs、SonarQube等静态分析工具来帮助检测潜在的内存泄漏问题。
- 核心思维模式: 编程时要有“引用管理”的意识。不仅要考虑一个对象何时被使用,更要考虑它何时应该被“释放”。不要因为有了GC就高枕无忧。
