读《Effective Java》笔记 - 条目7
条目7:清除过期的对象引用
Java垃圾回收机制
Java中的垃圾回收器(GC,Garbage Collector)主要通过对象的引用关系来判断对象是否可以被回收。
那么什么情况下,对象会被回收呢?
- 没有任何活动引用:如果一个对象不再有任何活动的引用指向它,它就变成了垃圾回收的候选对象。无论是局部变量、实例变量、还是静态字段,只要不再指向该对象,GC就可以认为该对象没有被使用,可以回收。
- 引用链断裂:当对象的所有引用链都断裂,GC会认为该对象不再被使用,且没有任何活动引用能够到达它。因此,它被认为是垃圾对象。
- 垃圾回收的标准:垃圾回收器并不关心对象的内部内容,而是依据是否有活跃的引用来决定对象是否可以被回收。如果对象不再可达(没有任何引用指向它),那么它就可以被垃圾回收器回收。
举个例子
public class GCExample {
public static void main(String[] args) {
MyClass obj1 = new MyClass(); // obj1指向MyClass对象
MyClass obj2 = obj1; // obj2也指向同一个MyClass对象
obj1 = null; // 现在obj1不再指向MyClass对象
// 此时,MyClass对象仍然被obj2引用,所以不会被GC回收
obj2 = null; // 现在没有任何引用指向MyClass对象
// 现在MyClass对象没有任何活动引用,它是可回收的
}
}
什么情况下会导致内存泄漏?
1. 长生命周期的对象持有短生命周期的对象引用
当长生命周期的对象(如静态字段、单例对象或全局对象等)持有短生命周期的对象的引用时,即使短生命周期的对象已经不再需要,垃圾回收器也无法回收这些对象,因为它们仍然被长生命周期的对象引用着。
public class MemoryLeakExample {
static List<Object> cache = new ArrayList<>();
public void cacheData(Object data) {
cache.add(data); // 数据被长生命周期的静态集合持有
}
}
// cache 列表会不断增大,即使数据不再需要,也无法被回收
2.未及时清除事件监听器或回调函数
在使用事件监听器、回调或者观察者模式时,如果对象不再需要,未注销的监听器或回调会持有对该对象的引用,导致该对象无法被垃圾回收。
public class MemoryLeakExample {
private Button button;
public void setup() {
button.addActionListener(e -> {
// 事件处理器中的匿名类持有外部类的引用
});
}
}
// 即使button被销毁,事件监听器仍然持有外部类的引用,导致内存泄漏
3.集合或缓存中的强引用
如果你将对象存储在集合中并且忘记从集合中移除它们,即使对象不再需要,集合中的引用仍然会阻止垃圾回收器回收这些对象,导致内存泄漏。
public class CacheExample {
private Map<String, Object> cache = new HashMap<>();
public void addToCache(String key, Object value) {
cache.put(key, value);
}
// 如果忘记清理缓存,缓存中的对象就不会被回收
}
4.使用静态字段保存对象引用
静态字段的生命周期与类的生命周期相同,即使类的实例已经不再使用,静态字段仍然存在。这可能会导致静态字段引用的对象无法被回收,从而发生内存泄漏。
public class StaticMemoryLeak {
static MyClass obj = new MyClass();
// 即使obj不再需要,静态字段会阻止其被回收
}
5.ThreadLocal导致的内存泄漏
ThreadLocal
用于线程内存中的数据存储,每个线程都有自己的变量副本。如果没有显式调用ThreadLocal.remove()
,该线程局部变量在该线程生命周期内可能一直存在,导致内存泄漏,尤其在多线程环境下长期运行时。
public class ThreadLocalLeakExample {
private static ThreadLocal<MyClass> threadLocal = new ThreadLocal<>();
public void setThreadLocalValue() {
threadLocal.set(new MyClass());
}
// 如果不调用 threadLocal.remove(),则 MyClass 对象不会被回收
}
6.类加载器泄漏
如果类加载器加载了很多类,并且这些类的实例持有引用而没有被清理,可能导致类加载器无法被回收。通常发生在Web应用中,当应用被重新部署时,旧的类加载器及其持有的对象无法被回收,形成内存泄漏。
7.未关闭的资源(如数据库连接、文件句柄、网络连接等)
虽然这些资源本身可能不导致直接的内存泄漏,但如果它们没有被关闭,它们可能会保持对其他对象的引用,导致这些对象无法被垃圾回收,间接地造成内存泄漏。
public class ResourceLeakExample {
public void readFile() {
InputStream is = new FileInputStream("somefile.txt");
// 如果忘记关闭输入流,可能会导致资源泄漏
}
}
防止内存泄漏
- 及时清理引用:确保不再使用的对象引用被清理,尤其是在集合、缓存和事件监听器中。
- 使用弱引用:对于缓存等情况,使用
WeakReference
或WeakHashMap
等来避免长生命周期的对象持有短生命周期对象的引用。 - 正确关闭资源:使用
try-with-resources
语句或显式关闭打开的资源(如文件、数据库连接、流等)。 - 避免静态引用:尽量避免使用静态字段持有不再需要的对象引用,尤其是那些具有长生命周期的静态字段。
- 分析工具:使用内存分析工具(如VisualVM、JProfiler等)定期检查程序的内存使用情况,及时发现内存泄漏问题。