文章目录
前言
本文只是自己当前的一些总结记录,并不保证绝对全面。
java中我们能遇到的内存泄漏
在Java中,内存泄漏指的是不再使用的对象由于某种原因未能被垃圾收集器回收,从而导致内存浪费的现象。
场景
长生命周期对象对短生命周期的对象引用
无意之间保留了过时引用
比如,我们用数组实现了一个栈(stack),但是我们的出栈方法是这么写的:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
这就存在一个隐藏的问题,那就是栈保留了这些对象的旧引用。不仅对象被排除在垃圾收集之外,该对象引用的任何对象也被排除在外,依此类推。许许多多的对象也可能被阻止被垃圾收集,从而对性能产生潜在的巨大影响。
解决机制:引用过时,则置空。
pop 方法的正确写法如下:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
将对象放入缓存,然后忘记了
缓存对象如果不加以管理,很容易导致内存泄漏。特别是如果缓存的对象生命周期过长或缓存的大小没有限制。
public class CacheManager {
private Map<String, Object> cache = new HashMap<>();
public void putInCache(String key, Object value) {
cache.put(key, value);
}
public Object getFromCache(String key) {
return cache.get(key);
}
}
解决机智:使用弱引用,如,将缓存表示为 WeakHashMap;当key失效后,该key-value条目将被自动删除。
监听器和其他回调
如果一个对象注册了监听器但没有相应地取消注册,那么这个监听器会一直持有对象的引用,导致内存泄漏。
public class EventSource {
private List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
}
public void fireEvent(Event event) {
listeners.forEach(l -> l.onEvent(event));
}
}
解决机制:
- 提供机制来移除监听器。
- 使用弱引用来存储监听器。
资源未关闭
常见的两个资源未关闭场景:
- 输入输出流:如,读取文件或写入文件时,如果没有关闭流,文件句柄将被占用。
- 数据库连接:如果连接没有被关闭,数据库连接池中的连接数会逐渐增加,最终可能导致连接池耗尽。
解决机制:
- 使用 try-with-resources 语句(推荐这个)或try-finally语句
没有或错误实现(重写)equals和hashCode方法
hashset和hashmap类中,在保证唯一性的时候会调用对象本身的hashCode()方法,假如我们实现这么一个类型:
public Cat{
private String name;
public Cat(String name){
this.name = name;
}
}
我们未实现equals()和hashCode()方法,那么在传入hashset中时,就会导致一个问题:
public class HashSetDemo {
public static void main(String[] args) {
HashSet<Cat> cats = new HashSet<>();
Cat cat1 = new Cat("Tom");
Cat cat2 = new Cat("Tom");
cats.add(cat1);
cats.add(cat2); // 这个对象可能被错误地添加到集合中
System.out.println(cats.size()); // 因为父类的默认实现是比较引用,此处会输出 2,而不是预期的 1
}
}
解决机制:
- 在创建类时应实现且正确实现equals() 和 hashCode() 方法。(注:非必要,不要覆盖 equals 方法。)
重写finalize()方法
重写 finalize() 方法来执行清理工作是一个危险的做法,因为它的执行时间不确定,并且在Java 9及以后的版本中已经被弃用。
解决机制:
- 尽量避免重写 finalize() 方法,使用其他机制如 try-with-resources 或者显式的资源管理。
使用了ThreadLocal,但同时用了线程池
ThreadLocal 是一个线程本地变量,每个线程都可以拥有一个独立的副本,互不影响。然而,如果线程池中的线程长时间存活,而 ThreadLocal 中的对象没有被显式清除,那么这些对象将一直被线程引用,无法被垃圾回收。
解决机制
- 在使用完 ThreadLocal 中的对象后,调用 remove() 方法来清除引用。
本文参考:Effective Java