读《Effective Java》笔记 - 条目7

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

Java垃圾回收机制

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

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

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

举个例子

java 复制代码
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. 长生命周期的对象持有短生命周期的对象引用

当长生命周期的对象(如静态字段、单例对象或全局对象等)持有短生命周期的对象的引用时,即使短生命周期的对象已经不再需要,垃圾回收器也无法回收这些对象,因为它们仍然被长生命周期的对象引用着。

java 复制代码
public class MemoryLeakExample {
    static List<Object> cache = new ArrayList<>();
    public void cacheData(Object data) {
        cache.add(data);  // 数据被长生命周期的静态集合持有
    }
}
// cache 列表会不断增大,即使数据不再需要,也无法被回收
2.未及时清除事件监听器或回调函数

在使用事件监听器、回调或者观察者模式时,如果对象不再需要,未注销的监听器或回调会持有对该对象的引用,导致该对象无法被垃圾回收。

java 复制代码
public class MemoryLeakExample {
    private Button button;
    public void setup() {
        button.addActionListener(e -> {
            // 事件处理器中的匿名类持有外部类的引用
        });
    }
}
// 即使button被销毁,事件监听器仍然持有外部类的引用,导致内存泄漏
3.集合或缓存中的强引用

如果你将对象存储在集合中并且忘记从集合中移除它们,即使对象不再需要,集合中的引用仍然会阻止垃圾回收器回收这些对象,导致内存泄漏。

java 复制代码
public class CacheExample {
    private Map<String, Object> cache = new HashMap<>();
    public void addToCache(String key, Object value) {
        cache.put(key, value);
    }
    // 如果忘记清理缓存,缓存中的对象就不会被回收
}
4.使用静态字段保存对象引用

静态字段的生命周期与类的生命周期相同,即使类的实例已经不再使用,静态字段仍然存在。这可能会导致静态字段引用的对象无法被回收,从而发生内存泄漏。

java 复制代码
public class StaticMemoryLeak {
    static MyClass obj = new MyClass();
    // 即使obj不再需要,静态字段会阻止其被回收
}
5.ThreadLocal导致的内存泄漏

ThreadLocal用于线程内存中的数据存储,每个线程都有自己的变量副本。如果没有显式调用ThreadLocal.remove(),该线程局部变量在该线程生命周期内可能一直存在,导致内存泄漏,尤其在多线程环境下长期运行时。

java 复制代码
public class ThreadLocalLeakExample {
    private static ThreadLocal<MyClass> threadLocal = new ThreadLocal<>();
    
    public void setThreadLocalValue() {
        threadLocal.set(new MyClass());
    }
    // 如果不调用 threadLocal.remove(),则 MyClass 对象不会被回收
}
6.类加载器泄漏

如果类加载器加载了很多类,并且这些类的实例持有引用而没有被清理,可能导致类加载器无法被回收。通常发生在Web应用中,当应用被重新部署时,旧的类加载器及其持有的对象无法被回收,形成内存泄漏。

7.未关闭的资源(如数据库连接、文件句柄、网络连接等)

虽然这些资源本身可能不导致直接的内存泄漏,但如果它们没有被关闭,它们可能会保持对其他对象的引用,导致这些对象无法被垃圾回收,间接地造成内存泄漏。

java 复制代码
public class ResourceLeakExample {
    public void readFile() {
        InputStream is = new FileInputStream("somefile.txt");
        // 如果忘记关闭输入流,可能会导致资源泄漏
    }
}

防止内存泄漏

  1. 及时清理引用:确保不再使用的对象引用被清理,尤其是在集合、缓存和事件监听器中。
  2. 使用弱引用 :对于缓存等情况,使用WeakReferenceWeakHashMap等来避免长生命周期的对象持有短生命周期对象的引用。
  3. 正确关闭资源 :使用try-with-resources语句或显式关闭打开的资源(如文件、数据库连接、流等)。
  4. 避免静态引用:尽量避免使用静态字段持有不再需要的对象引用,尤其是那些具有长生命周期的静态字段。
  5. 分析工具:使用内存分析工具(如VisualVM、JProfiler等)定期检查程序的内存使用情况,及时发现内存泄漏问题。
相关推荐
腻害兔5 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
克里斯蒂亚诺更新6 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117768 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
维天说10 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
破碎的南瓜10 小时前
sqli--sql注入过关笔记(1,2,3,4,5,9)
数据库·笔记·sql
Zane199410 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
你想知道什么?10 小时前
线性回归-学习笔记
笔记·学习·线性回归