代码改变世界

消除过期的对象引用

2017-03-12 08:31  ttylinux  阅读(718)  评论(0编辑  收藏  举报
本文涉及的概念
1.Java的内存泄露
2.出现Java内存泄露的几种常见场景
 
Java的内存泄露
Java语言,创建对象后,程序员不用手动回收对象。Java虚拟机会手动回收不使用的对象(没有引用指向该对象)。那么,为什么还出现泄露。Java的内存泄露是指这样一种情况,创建一个对象,系统中依然存在引用指向该对象,该对象是可达的;但是,我们找不到该引用,或者我们没发现该引用的存在。于是,一直存在该对象,但是,它对我们不可用,Java虚拟机也不能回收该对象。
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
 
“要让垃圾收集(GC)回收程序不再使用的对象,对象的逻辑 生命周期(应用程序使用它的时间)和对该对象拥有的引用的实际 生命周期必须是相同的。在大多数时候,好的软件工程技术保证这是自动实现的,不用我们对对象生命周期问题花费过多心思。但是偶尔我们会创建一个引用,它在内存中包含对象的时间比我们预期的要长得多,这种情况称为无意识的对象保留(unintentional object retention)。”
  
 
出现Java内存泄露的几种常见场景
一个来自《effective java》的例子
1.元素出栈,忘记设置为Null
import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack {

private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;

public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}

public void push(Object e) {

ensureCapacity();
elements[size++] = e;
}

public Object pop() {

if (size == 0)
throw new EmptyStackException();
return elements[--size];
}

/**
* Ensure space for at least one more element, roughly doubling the capacity
* each time the array needs to grow
*/
private void ensureCapacity() {

if (elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}

}
先进行入栈,然后,再出栈。入栈5个元素,然后,再出栈5个元素。那么此时,会有5个元素不会被销毁,因为,elements中,持有指向5个对象的引用(保存了5个已经出栈的引用,只有再一次入栈5个引用)。出现内存泄露。
将上文的pop方法修改为:
public Object pop() {

if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}

 

2.使用缓存,内存缓存
对象引用存放在缓存中,当对象不再被使用时,然后,你忘记将该对象引用从缓存中清理掉。于是,该对象引用会一直保存在缓存中,而你在逻辑上已经没有使用该对象,但是该对象不会被GC回收,因为仍然有引用指向它。
解决办法:关键是,要知道什么时候,缓存中的对象引用不再有用,有意义。在这个时候,就可以清理掉缓存中的对象引用。
 
一种例子是,如果实现的缓存是这样的:只要在缓存之外存在对某个项的键的引用,该项就有意义;如果没有存在对某个项的键的引用,该项就没有意义。那么,可以使用WeakHashMap来代表缓存。
public class SocketManager {
    private Map<Socket,User> m = new WeakHashMap<Socket,User>();

    public void setUser(Socket s, User u) {
        m.put(s, u);
    }
    public User getUser(Socket s) {
        return m.get(s);
    }
}

上述代码,当外部代码没有持有m中的一个键的引用,那么,该WeakHashMap就会键该键移除掉。不用我们手工去移除。(Hash table based implementation of the Map interface, with weak keys. An entry in a WeakHashMap will automatically be removed when its key is no longer in ordinary use. More precisely, the presence of a mapping for a given key will not prevent the key from being discarded by the garbage collector, that is, made finalizable, finalized, and then reclaimed. When a key has been discarded its entry is effectively removed from the map, so this class behaves somewhat differently from other Map implementations.)

当某个项的键被移除后,调用getUseer,会返回NULL。如果为NULL(表示是死的映射),还需要将该键所对应的项移除掉,从m中移除掉。否则,m中会包含很多没有用的映射。
这意味着,我们还要周期性的去调用getUser,然后,删除死的映射。上述方法只是解决了问题的一部分。如果Map中有很多映射,那么,这个通过遍历去寻找死的映射然后删除掉死的映射的工作量就很大

 

引用队列

"WeakHashMap 用弱引用承载映射键,这使得应用程序不再使用键对象时它们可以被垃圾收集,get() 实现可以根据 WeakReference.get() 是否返回 null 来区分死的映射和活的映射。但是这只是防止 Map 的内存消耗在应用程序的生命周期中不断增加所需要做的工作的一半,还需要做一些工作以便在键对象被收集后从 Map 中删除死项。否则,Map 会充满对应于死键的项。虽然这对于应用程序是不可见的,但是它仍然会造成应用程序耗尽内存,因为即使键被收集了,Map.Entry 和值对象也不会被收集。

可以通过周期性地扫描 Map,对每一个弱引用调用 get(),并在 get() 返回 null 时删除那个映射而消除死映射。但是如果 Map 有许多活的项,那么这种方法的效率很低。如果有一种方法可以在弱引用的 referent 被垃圾收集时发出通知就好了,这就是引用队列 的作用。
"
“引用队列是垃圾收集器向应用程序返回关于对象生命周期的信息的主要方法。弱引用有两个构造函数:一个只取 referent 作为参数,另一个还取引用队列作为参数。如果用关联的引用队列创建弱引用,在 referent 成为 GC 候选对象时,这个引用对象(不是 referent)就在引用清除后加入 到引用队列中。之后,应用程序从引用队列提取引用并了解到它的 referent 已被收集,因此可以进行相应的清理活动,如去掉已不在弱集合中的对象的项。(引用队列提供了与 BlockingQueue 同样的出列模式 —— polled、timed blocking 和 untimed blocking。)”
 
"WeakHashMap 有一个名为 expungeStaleEntries() 的私有方法,大多数 Map 操作中会调用它,它去掉引用队列中所有失效的引用,并删除关联的映射。清单 7 展示了 expungeStaleEntries() 的一种可能实现。用于存储键-值映射的 Entry 类型扩展了 WeakReference,因此当expungeStaleEntries() 要求下一个失效的弱引用时,它得到一个 Entry。用引用队列代替定期扫描内容的方法来清理 Map 更有效,因为清理过程不会触及活的项,只有在有实际加入队列的引用时它才工作。"
--------------------在写程序的时候,当要清理掉WeakHashMap中死的的映射,已经被GC回收的引用键所对应的项,就直接调用该方法
清单 7. WeakHashMap.expungeStaleEntries() 的可能实现
    private void expungeStaleEntries() {
         Entry<K,V> e;
        while ( (e = (Entry<K,V>) queue.poll()) != null) {
            int hash = e.hash;
            Entry<K,V> prev = getChain(hash);
            Entry<K,V> cur = prev;
            while (cur != null) {
                Entry<K,V> next = cur.next;
                if (cur == e) {
                    if (prev == e)
                        setChain(hash, next);
                    else
                        prev.next = next;
                    break;
                }
                prev = cur;
                cur = next;
            }
        }
    }

 

3.监听器和回调

public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NetworkManager.getInstance().registerListener(this);
    }

    @Override
    public void onNetworkUp() {

    }

    @Override
    public void onNetworkDown() {

    }
}
AppCompatActivity实现一个接口OnNetworkChangedListener,用来监听网络的变化。然后,把这个监听器注册到NetworkManager实例中。
NetworkManager实例持有监听器(MainActivity是实现者,指向MainActivity对象的引用)。
当NetworkManger实例的生命周期比MainActivity长,当MainActivity销毁时(比如屏幕发生旋转),NetworkManager实例还没有销毁,它持有指向MainActivity对象的引用。系统回调MainActivity的onDestroy方法,这时,GC是要回收MainActivity对象的,但是,MainActivity对象依然可达(NetworkManager实例持有指向MainActivity对象的引用),所以没有回收,MainActivity对象泄露。
系统中,这种类型的情况很多的话,那么,就会堆积很多你没有使用到的对象,但是它们却没有被GC销毁,积累到一定成都就是内存溢出。
 
解决办法:当系统回调MainActivity的onDestroy方法时,反注册监听器(也就是把NetworkManager实例中指向MainActivity对象的引用设置为NULL)。
public class MainActivity extends AppCompatActivity implements OnNetworkChangedListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        NetworkManager.getInstance().registerListener(this);
    }

    @Override
    public void onNetworkUp() {

    }

    @Override
    public void onNetworkDown() {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        NetworkManager.getInstance().unregisterListener(this);
    }

}

 

引用: