Bootstrap

读《Effective Java》笔记 - 条目7

条目7:清除过期的对象引用

Java垃圾回收机制

Java中的垃圾回收器(GC,Garbage Collector)主要通过对象的引用关系来判断对象是否可以被回收。

那么什么情况下,对象会被回收呢?

  1. 没有任何活动引用:如果一个对象不再有任何活动的引用指向它,它就变成了垃圾回收的候选对象。无论是局部变量、实例变量、还是静态字段,只要不再指向该对象,GC就可以认为该对象没有被使用,可以回收。
  2. 引用链断裂:当对象的所有引用链都断裂,GC会认为该对象不再被使用,且没有任何活动引用能够到达它。因此,它被认为是垃圾对象。
  3. 垃圾回收的标准:垃圾回收器并不关心对象的内部内容,而是依据是否有活跃的引用来决定对象是否可以被回收。如果对象不再可达(没有任何引用指向它),那么它就可以被垃圾回收器回收。

举个例子

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");
        // 如果忘记关闭输入流,可能会导致资源泄漏
    }
}

防止内存泄漏

  1. 及时清理引用:确保不再使用的对象引用被清理,尤其是在集合、缓存和事件监听器中。
  2. 使用弱引用:对于缓存等情况,使用WeakReferenceWeakHashMap等来避免长生命周期的对象持有短生命周期对象的引用。
  3. 正确关闭资源:使用try-with-resources语句或显式关闭打开的资源(如文件、数据库连接、流等)。
  4. 避免静态引用:尽量避免使用静态字段持有不再需要的对象引用,尤其是那些具有长生命周期的静态字段。
  5. 分析工具:使用内存分析工具(如VisualVM、JProfiler等)定期检查程序的内存使用情况,及时发现内存泄漏问题。
;