Может ли java завершить объект, когда он все еще находится в области видимости?

Я искал ошибку в моем коде, которая, кажется, вызвана некоторым "уродливым" кодом финализатора. Код выглядит примерно так:

public class A {
   public B b = new B();
   @Override public void finalize() {
     b.close();
   }
}

public class B {
   public void close() { /* do clean up our resources. */ }
   public void doSomething() { /* do something that requires us not to be closed */ } 
}

void main() {
   A a = new A();
   B b = a.b;
   for(/*lots of time*/) {
     b.doSomething();
   }
}

Я думаю, что происходит, что a обнаруживается как не имеющий ссылок после вторая строка main() и получение GC'd и финализация потока финализатора - пока цикл for все еще происходит, используя b, а a по-прежнему "в области видимости".

Является ли это правдоподобным? Является ли java доступным для GC объекта перед тем, как он выходит из области видимости?

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

Ответ 1

Может ли Java завершить объект, когда он все еще находится в области видимости?

Да.

Однако я здесь педант. Сфера - это понятие языка, которое определяет правильность имен. Независимо от того, может ли объект быть собранным мусором (и, следовательно, финализирован), зависит от его доступности.

Ответ от ajb почти имел его (+1), сославшись на значительный отрывок из JLS. Однако я не думаю, что это прямо применимо к ситуации. JLS §12.6.1 также говорит:

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

Теперь рассмотрим это применительно к следующему коду:

class A {
    @Override protected void finalize() {
        System.out.println(this + " was finalized!");
    }

    public static void main(String[] args) {
        A a = new A();
        System.out.println("Created " + a);
        for (int i = 0; i < 1_000_000_000; i++) {
            if (i % 1_000_000 == 0)
                System.gc();
        }
        // System.out.println(a + " was still alive.");
    }
}

В JDK 8 GA это будет завершаться a каждый раз. Если вы раскомментируете println в конце, a никогда не будет завершена.

С комментарием println можно увидеть, как применяется правило достижимости. Когда код достигает цикла, нет никакого способа, чтобы поток мог иметь доступ к a. Таким образом, он недоступен и, следовательно, подлежит окончательной доработке и сбору мусора.

Обратите внимание, что имя a по-прежнему находится в области видимости, потому что можно использовать a в любом месте внутри блока - в этом случае тело метода main - от его объявления до конца блока. Точные правила охвата описаны в JLS §6.3. Но на самом деле, как вы видите, область охвата не имеет ничего общего с достижимостью или сборкой мусора.

Чтобы предотвратить сбор мусора, вы можете сохранить ссылку на него в статическом поле или если вы этого не хотите, вы можете сохранить его доступным, используя его позже в том же методе после длительный цикл. Достаточно назвать безобидный метод, например toString.

Ответ 2

JLS §12.6.1:

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

Итак, я думаю, что для компилятора можно добавить скрытый код, чтобы установить a в null, что позволяет ему собирать мусор. Если это то, что происходит, вы не сможете сказать из байт-кода (см. Комментарий @user2357112).

Возможное (уродливое) обходное решение: добавьте public static boolean alwaysFalse = false; в основной класс или какой-либо другой класс, а затем в конец main() добавьте if (alwaysFalse) System.out.println(a); или что-то еще, что ссылается на a. Я не думаю, что оптимизатор может с уверенностью определить, что alwaysFalse никогда не устанавливается (поскольку какой-то класс всегда может использовать отражение для его установки); поэтому он не сможет сказать, что a больше не требуется. По крайней мере, такой "обходной путь" можно было бы использовать, чтобы определить, действительно ли это проблема.