有这样一段代码:

public class LapsedListenerTest {

    public static class A{
        private List listeners = new CopyOnWriteArrayList();
        private String name;
        public A(String name) {
            this.name = name;
        }
        public void addListener(PropertyChangeListener listener) {
            listeners.add(listener);
        }
        private void fire(String property,Object oldValue,Object newValue) {
            for(PropertyChangeListener listener:listeners) {
                    listener.propertyChange(new PropertyChangeEvent(this, property, oldValue, newValue));
            }
        }
        public void setName(String name) {
            String oldName = this.name;
            this.name = name;
            fire("name", oldName, name);
        }
    }

    public static class B implements PropertyChangeListener{
        private int id;
        private A a;
        //占用内存
        private byte[] bt= new byte[1024*1024];
        public B(A a,int id) {
            this.id = id;
            this.a = a;
            this.a.addListener(this);
        }
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            System.out.println("on B-" B.this.id "(" evt.getPropertyName() "," evt.getOldValue() "," evt.getNewValue() ")");
        }
    }

    /**
     * @param args
     * @throws InterruptedException
     */
     public static void main(String[] args) throws InterruptedException {
        A a = new A("A");
        int j = 0;
        while(true) {
            B b = new B(a,j);
            //do some thing with b
            a.setName("A" j);
            b.dispose();
            j  ;
            System.gc();
            Thread.sleep(20);
        }
    }

} 

 

代码片段1

A类的对象生命周期都比较长,而B类的对象生命周期都比较短,它需要监听A类的属性变化而进行一些操作。这个程序运行一下,很快就内存溢出了。

这里明显存在一个反模式:流失监听者泄露(Lapsed Listener Leak)。 由于B类的对象的生命周期比较短,如果在系统运行过程中,大量生成B类的对象,而B的构造函数中有将自己作为监听器加入A的监听器列表中,导致B类的对象无法被垃圾回收器回收。在这里,B自己作为监听器,错误看起来比较明显,但如果用内部匿名类的实现方式:

 

public B(A a,int id) {
            this.id = id;
            this.a = a;
            this.a.addListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    System.out.println("on B-" B.this.id "(" evt.getPropertyName() "," evt.getOldValue() "," evt.getNewValue() ")");
                }
            });
        }

 

代码片段2

看起来虽然没那么明显,但效果是一样的。因为匿名类的对象会引用自己的父对象。

 

这个反模式在Java用户界面框架(AWT,Swing)使用的过程中出现的比较多,但没出现问题的原因主要有两个:1.桌面软件的运行时间一般不会太长,很少有需要7X24运行的。2.桌面类软件中的组件数目有限,一般也不会导致内存溢出。但如果是服务器端的程序就没这么幸运了。

《Bitter Java》中给出了三种解决方案:

1.显式的删除监听者

在A中增加removeListener接口,当B的生命周期结束后,将其从A的监听器列表里删除。

2.缩短锚的生命周期

确保A类对象的生命周期不要太长,A类的对象如果能尽快被垃圾回收器回收,内存泄露也就不会发生了。

3.弱化引用

用弱引用(WeakReference)关联监听器。弱引用的好处是,如果垃圾回收器检测到一个对象当前只有弱引用,就认为该对象可以被回收了。下面是用弱引用重构后的实例代码:

 

public class LapsedListenerTest2 {

    public static class A{
        private List listeners = new CopyOnWriteArrayList();
        private String name;
        public A(String name) {
            this.name = name;
        }
        public void addListener(PropertyChangeListener listener) {
            listeners.add(new WeakReference(listener));
        }
        private void fire(String property,Object oldValue,Object newValue) {
            for(WeakReference listenerRef:listeners) {
                PropertyChangeListener listener = listenerRef.get();
                if(listener != null) {
                    listenerRef.get().propertyChange(new PropertyChangeEvent(this, property, oldValue, newValue));
                }else {
                    listeners.remove(listenerRef);
                }
            }
        }
        public void setName(String name) {
            String oldName = this.name;
            this.name = name;
            fire("name", oldName, name);
        }
    }

    public static class B implements PropertyChangeListener{
        private int id;
        private A a;
        private byte[] bt= new byte[1024*1024];

        public B(A a,int id) {
            this.id = id;
            this.a = a;
            this.a.addListener(this);
        }
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            System.out.println("on B-" B.this.id "(" evt.getPropertyName() "," evt.getOldValue() "," evt.getNewValue() ")");
        }
        @Override
        protected void finalize() throws Throwable {
            System.out.println("finalize:" id);
        }
    }

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        A a = new A("A");
        int j = 0;
        B first = null;
        while(true) {
            B b = new B(a,j);
            if(first == null) {
                first = b;
            }
            //do some thing with b
            a.setName("A" j);
            j  ;
            System.gc();
            Thread.sleep(20);
        }
    }
}

 

代码片段3

 运行后可以看出:

由于first引用了第一个B对象,致使该对象一直存活,而其他的B对象都被很快的回收了。是不是一个完美的解决方案?别着急,这儿还有个问题。如果B类采用匿名类监听器的情况呢?(代码片段2的情况)

这样的情况下,所有的监听器都只有弱引用,导致监听器会很快被垃圾回收器回收,即便是B类的对象存活(first)的情况下!所以这种方式要求监听器在A类外部必须还有个引用,并且该引用的生命周期与B一致(B自己或者B的属性)。

综合看来,还是方案1比较可靠,虽然要考手动操作。按照方案1的重构:

 

public class LapsedListenerTest {

    public static class A{
        private List listeners = new CopyOnWriteArrayList();
        private String name;
        public A(String name) {
            this.name = name;
        }
        public void addListener(PropertyChangeListener listener) {
            listeners.add(listener);
        }
        public void removeListener(PropertyChangeListener listener) {
            listeners.remove(listener);
        }
        private void fire(String property,Object oldValue,Object newValue) {
            for(PropertyChangeListener listener:listeners) {
                    listener.propertyChange(new PropertyChangeEvent(this, property, oldValue, newValue));
            }
        }
        public void setName(String name) {
            String oldName = this.name;
            this.name = name;
            fire("name", oldName, name);
        }
    }

    public static class B implements PropertyChangeListener{
        private int id;
        private A a;
        //占用内存
        private byte[] bt= new byte[1024*1024];
        public B(A a,int id) {
            this.id = id;
            this.a = a;
            this.a.addListener(this);
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            System.out.println("on B-" B.this.id "(" evt.getPropertyName() "," evt.getOldValue() "," evt.getNewValue() ")");
        }

        public void dispose() {
            this.a.removeListener(this);
        }

    }

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        A a = new A("A");
        int j = 0;
        while(true) {
            B b = new B(a,j);
            //do some thing with b
            a.setName("A" j);
            b.dispose();
            j  ;
            System.gc();
            Thread.sleep(20);
        }
    }

}

 

 

代码片段4

 

本方案的缺点就是必须显式调用下b.dispose()进行销毁。

不知道还有没有其他的更好的解决方案?