Подтвердить что ты не робот

Нужно ли удалять слушателей Java? (В целом)

Представьте себе этот образец класса 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() {}
        }
 }

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

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

4b9b3361

Ответ 1

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

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

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
}

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

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

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
}

Ответ 2

На ссылочном графике есть цикл. Ссылки 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));
    }
}

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

Ответ 3

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

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

Как можно пережить B?:

Пример использования 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;
}

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

Ответ 10

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

Я не знаю каких-либо данных о том, будет ли то, что вы предлагаете, сделать сборщик мусора более эффективным, и, когда это стоит того, но мне было бы интересно увидеть его.

Ответ 11

A действительно будет держать B от сбора мусора в вас, используя стандартные ссылки для хранения ваших слушателей. Альтернативно, когда вы поддерживаете списки слушателей вместо определения   новый ArrayList <ListenerType> (); вы могли бы сделать что-то вроде   новый ArrayList < WeakReference <ListenerType> ();

Обернув свой объект в weakReference, вы можете удержать его от продления срока службы объекта.

Это только работает, конечно, если вы пишете класс, в котором содержатся слушатели

Ответ 12

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

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