Слушатели Java должны быть удалены? (В общем) - PullRequest
28 голосов
/ 01 октября 2008

Представьте себе этот пример класса Java:

class A {
    void addListener(Listener obj);
    void removeListener(Listener obj);
}

class B {
    private A a;
    B() {
        a = new A();
        a.addListener(new Listener() {
            void listen() {}
        }
 }

Нужно ли добавить метод B для завершения для вызова a.removeListener? Предположим, что экземпляр A будет использоваться совместно с некоторыми другими объектами и переживет экземпляр B.

Я беспокоюсь, что могу создать здесь проблему с сборщиком мусора. Какова лучшая практика?

Ответы [ 12 ]

17 голосов
/ 01 октября 2008

На контрольном графике есть цикл. A ссылки B и B ссылки A. Сборщик мусора обнаружит циклы и увидит, когда нет внешних ссылок на A и B, а затем соберет оба.

Попытка использовать финализатор здесь неверна. Если B уничтожается, ссылка на A также удаляется.


Утверждение: «Предположим, что экземпляр A также будет использоваться совместно с некоторыми другими объектами и переживет экземпляр B». неправильно. Единственный способ, который произойдет, - это если слушатель явно удаляется из другого места, кроме финализатора. Если ссылки на A передаются, это будет означать ссылку на B, и B не будет собирать мусор, поскольку существуют внешние ссылки на цикл A-B.


Дальнейшее обновление:

Если вы хотите разорвать цикл и не требовать, чтобы B явно удалял прослушиватель, вы можете использовать WeakReference. Примерно так:

class A {
    void addListener(Listener obj);
    void removeListener(Listener obj);
}

class B {
    private static class InnerListener implements Listener {
        private WeakReference m_owner;
        private WeakReference m_source;

        InnerListener(B owner, A source) {
            m_owner = new WeakReference(owner);
            m_source = new WeakReference(source);
        }

        void listen() {
            // Handling reentrancy on this function left as an excercise.
            B b = (B)m_owner.get();
            if (b == null) {
                if (m_source != null) {
                    A a = (A) m_source.get();
                    if (a != null) {
                        a.removeListener(this);
                        m_source = null;
                    }
                }

                return;
            }
            ...
        }
    }

    private A a;

    B() {
        a = new A();
        a.addListener(new InnerListener(this, a));
    }
}

Может быть дополнительно обобщено при необходимости для нескольких классов.

5 голосов
/ 01 октября 2008

Мое понимание GC состоит в том, что до тех пор, пока не будет вызван метод removeListener, класс A будет поддерживать ссылку на слушателя и поэтому не будет кандидатом на очистку GC (и, следовательно, не будет вызываться finalize) ,

3 голосов
/ 01 октября 2008

Вы должны исходить из C ++ или другого языка, где люди реализуют деструкторы. В Java вы этого не делаете. Вы не отменяете финализацию, если не знаете, что делаете. За 10 лет мне никогда не приходилось этого делать, и я до сих пор не могу придумать вескую причину, которая потребовала бы от меня этого.

Возвращаясь к вашему вопросу, ваш слушатель является независимым объектом со своим собственным жизненным циклом и будет собираться после того, как будут собраны все другие объекты, ссылающиеся на него, или когда на него не будет указывать никакой другой объект. Это работает очень хорошо. Так что нет, вам не нужно переопределять финализацию.

3 голосов
/ 01 октября 2008

Если вы добавили B в качестве прослушивателя A, а A предназначен для того, чтобы пережить B, вызов finalize для B никогда не будет вызван, потому что в A есть экземпляр B, поэтому он никогда не будет собирать мусор. Вы можете обойти это, сохранив ссылку на B в A как WeakReference (которая не считается ссылкой во время гаражного сбора), но было бы лучше явно отменить регистрацию B в A, когда она вам больше не нужна.

В целом, в Java рекомендуется не использовать метод finalize в Java, потому что вы никогда не можете быть уверены, когда он будет вызван, и вы не можете использовать его для отмены регистрации в другом классе.

2 голосов
/ 01 октября 2008

В вашей ситуации единственная «проблема» сборки мусора заключается в том, что экземпляры B не будут собираться мусором, пока существуют жесткие ссылки на общий экземпляр A. Вот как сборка мусора должна работать в Java / .NET. Теперь, если вам не нравится тот факт, что экземпляры B не собирались ранее, вы должны спросить себя, в какой момент вы хотите, чтобы они перестали слушать события из A? Получив ответ, вы узнаете, как исправить дизайн.

2 голосов
/ 01 октября 2008

A действительно поддержит B в анонимном экземпляре.

Но я бы не стал переопределять финализацию для решения этой проблемы, а использовал бы статический внутренний класс, который не поддерживает B.

1 голос
/ 01 октября 2008

Как А может пережить В?:

Пример использования B и A:

public static main(args) {
    B myB = new B();
    myB = null;
}

Поведение, которое я ожидал:

GC удалит myB и в экземпляре myB должна была ссылаться только на экземпляр A, поэтому он также будет удален. Со всеми назначенными слушателями?

Возможно, вы имели в виду:

class B {
    private A a;
    B(A a) {
        this.a = a;
        a.addListener(new Listener() {
            void listen() {}
        }
}

При использовании:

public static main(args) {
    A myA = new A();
    B myB = new B(myA);
    myB = null;
}

Потому что тогда я действительно удивлюсь, что происходит с этим анонимным классом ....

1 голос
/ 01 октября 2008

A содержит ссылку на B через анонимный экземпляр, неявно используемый созданным анонимным типом. Это означает, что B не будет освобожден до тех пор, пока не будет вызван removeListener, и, следовательно, финализация B не будет вызвана.

Когда уничтожается A, анонимная ссылка на B также уничтожает B, открывая путь к освобождению B.

Но поскольку B содержит ссылку на A, этого никогда не происходит. Это похоже на проблему дизайна - если у A есть вызовы слушателя, зачем вам B, чтобы также содержать ссылку на A? Почему бы не передать A, который сделал вызов, слушателю, если это необходимо?

0 голосов
/ 20 ноября 2008

Я только что обнаружил огромную утечку памяти, поэтому я собираюсь назвать код, который создал утечку, как неправильный , а мое исправление, которое не дает утечки, как правильно .

Вот старый код: (Это обычный шаблон, который я видел повсюду)

class Singleton {
    static Singleton getInstance() {...}
    void addListener(Listener listener) {...}
    void removeListener(Listener listener) {...}
}

class Leaky {
    Leaky() {
        // If the singleton changes the widget we need to know so register a listener
        Singleton singleton = Singleton.getInstance();
        singleton.addListener(new Listener() {
            void handleEvent() {
                doSomething();
            }
        });
    }
    void doSomething() {...}
}

// Elsewhere
while (1) {
    Leaky leaky = new Leaky();
    // ... do stuff
    // leaky falls out of scope
}

Понятно, что это плохо. Многие Leaky создаются и никогда не собирают мусор, потому что слушатели поддерживают их.

Здесь была моя альтернатива, которая исправила утечку памяти. Это работает, потому что я забочусь о слушателе событий, пока объект существует. Слушатель не должен поддерживать объект в живых.

class Singleton {
    static Singleton getInstance() {...}
    void addListener(Listener listener) {...}
    void removeListener(Listener listener) {...}
}

class NotLeaky {
    private NotLeakyListener listener;
    NotLeaky() {
        // If the singleton changes the widget we need to know so register a listener
        Singleton singleton = Singleton.getInstance();
        listener = new NotLeakyListener(this, singleton);
        singleton.addListener(listener);
    }
    void doSomething() {...}
    protected void finalize() {
        try {
            if (listener != null)
                listener.dispose();
        } finally {
            super.finalize();
        }
    }

    private static class NotLeakyListener implements Listener {
        private WeakReference<NotLeaky> ownerRef;
        private Singleton eventer;
        NotLeakyListener(NotLeaky owner, Singleton e) {
            ownerRef = new WeakReference<NotLeaky>(owner);
            eventer = e;
        }

        void dispose() {
            if (eventer != null) {
                eventer.removeListener(this);
                eventer = null;
            }
        }

        void handleEvent() {
            NotLeaky owner = ownerRef.get();
            if (owner == null) {
                dispose();
            } else {
                owner.doSomething();
            }
        }
    }
}

// Elsewhere
while (1) {
    NotLeaky notleaky = new NotLeaky();
    // ... do stuff
    // notleaky falls out of scope
}
0 голосов
/ 01 октября 2008

Опираясь на то, что @Alexander сказал об удалении себя как слушателя:

Если нет веских причин не делать этого, я научился у своих коллег одной вещи: вместо создания анонимного внутреннего прослушивателя и необходимости сохранять его в переменной, заставить B реализовать Listener, а затем B может удалить себя, когда нужно, с помощью a.removeListener(this)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...