Java 内存泄漏:应用程序中的无声杀手

110 阅读3分钟

在 Java 开发中,开发人员面临的最难以捉摸且具有潜在破坏性的问题之一是可怕的内存泄漏。虽然 Java 拥有高效的垃圾收集器,但它也无法避免与内存相关的陷阱。

在这里,我们将通过实际示例来了解 Java 中内存泄漏的常见原因。

静态集合

无限增长的静态集合可能会导致内存泄漏,因为它们的生命周期与应用程序的生命周期相关。

public class MemoryLeakExample {
    private static final List<Object> leakyList = new ArrayList<>();
    
    public void addToLeakyList(Object obj) {
        leakyList.add(obj);
    }
}

解决方案:限制集合的大小或定期清除。

public class FixedMemoryLeakExample {
    private static final List<Object> safeList = new ArrayList<>();
    
    public void addToSafeList(Object obj) {
        if (safeList.size() > 1000) {
            safeList.clear();
        }
        safeList.add(obj);
    }
}

未封闭资源

不关闭流、连接或文件等资源可能会导致内存泄漏。

public void readFile(String fileName) throws IOException {
    FileInputStream fis = new FileInputStream(fileName);
    // ... read the file
    // 没有fis.close();
}

解决方案:始终在finally 块中关闭资源或使用try-with-resources 语句。

public void readFileSafely(String fileName) throws IOException {
    try (FileInputStream fis = new FileInputStream(fileName)) {
        // ... read the file
    } // 自动关闭
}

内部类引用

非静态内部类持有对其外部类的隐式引用。如果它们的寿命比外部类长,则可能会导致内存泄漏。

public class OuterClass {
    private HeavyObject heavyObject = new HeavyObject();

    public Runnable createRunnable() {
        return new InnerRunnable();
    }

    private class InnerRunnable implements Runnable {
        @Override
        public void run() {

        }
    }
}

解决方案:使用静态内部类或单独的类。静态内部类不保存对外部类实例的隐式引用。这种解耦确保内部类的生命周期不与外部类绑定,从而防止潜在的内存泄漏

public class OuterClass {
    private HeavyObject heavyObject = new HeavyObject();

    public Runnable createRunnable() {
        return new StaticInnerRunnable(heavyObject);
    }

    private static class StaticInnerRunnable implements Runnable {
        private HeavyObject ref;

        public StaticInnerRunnable(HeavyObject ref) {
            this.ref = ref;
        }

        @Override
        public void run() {

        }
    }
}

缓存对象

存储在缓存中的对象在内存中的保留时间通常会超过必要的时间

public class Cache {
    private Map<String, Object> cache = new HashMap<>();

    public void put(String key, Object value) {
        cache.put(key, value);
    }
}

解决方案:使用支持过期的缓存库,比如EhCache或者Guava的Cache。

public class SafeCache {
    private Cache<String, Object> cache = CacheBuilder.newBuilder()
        .expireAfterAccess(5, TimeUnit.MINUTES)
        .build();

    public void put(String key, Object value) {
        cache.put(key, value);
    }
}

ThreadLocal变量

如果线程不终止,ThreadLocal 变量可能会导致内存泄漏,尤其是在线程池场景中。

public class ThreadLocalLeak {
    private static ThreadLocal<HeavyObject> threadLocal = ThreadLocal.withInitial(() -> new HeavyObject());

    public void useThreadLocal() {
        HeavyObject obj = threadLocal.get();

    }
}

解决方案:当不再需要 ThreadLocal 变量时,始终将其删除。

public void safeUseThreadLocal() {
    try {
        HeavyObject obj = threadLocal.get();

    } finally {
        threadLocal.remove();
    }
}

监听器和回调

注册侦听器而不取消注册它们可能会导致内存泄漏。

public class EventProducer {
    private List<EventListener> listeners = new ArrayList<>();

    public void registerListener(EventListener listener) {
        listeners.add(listener);
    }
}

解决方案:始终提供注销侦听器的机制。

public void unregisterListener(EventListener listener) {
    listeners. Remove(listener);
}

持有大对象的单例类

持有大对象的单例类可能会导致内存泄漏。

public class Singleton {
    private static Singleton instance;
    private HeavyObject heavyObject = new HeavyObject();

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

解决方案:确保单例持有对象的时间不会超过必要的时间。

public void releaseResources() {
    heavyObject = null;
}

数据库连接

不关闭数据库连接可能会导致内存泄漏。

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test", "user", "password");
// ... use the connection
// 没有conn.close();

解决方案:使用后始终关闭数据库连接。

try (Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test", "user", "password")) {
    // ... use the connection
} // conn 会自动关闭

加载类

连续加载类,尤其是使用自定义类加载器,可能会导致内存泄漏,特别是如果您使用自定义类加载器频繁加载和卸载类。 请谨慎对待类加载器和加载的类的生命周期。 确保类加载器被垃圾收集,并且没有对加载的类的延迟引用。

通过了解内存泄漏的常见原因并实施所提供的解决方案,您可以确保 Java 应用程序保持高效且响应迅速。永远记住,解决内存泄漏的最佳方法是首先防止它。