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

Поведение GC при присвоении нулевой ссылочной переменной

Я пытался понять поведение GC, и я нашел то, что меня интересует, что я не могу понять.

См. код и вывод:

public class GCTest {
    private static int i=0;

    @Override
    protected void finalize() throws Throwable {
        i++; //counting garbage collected objects
    }

    public static void main(String[] args) {        
        GCTest holdLastObject; //If I assign null here then no of eligible objects are 9 otherwise 10.

        for (int i = 0; i < 10; i++) {            
             holdLastObject=new GCTest();             
        }

        System.gc(); //requesting GC

        //sleeping for a while to run after GC.
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // final output 
        System.out.println("`Total no of object garbage collected=`"+i);          
    }
}

В приведенном выше примере, если я назначаю holdLastObject null, я получаю Total no of object garbage collected=9. Если я этого не сделаю, я получаю 10.

Может кто-нибудь объяснить это? Я не могу найти правильную причину.

4b9b3361

Ответ 1

Я подозреваю, что это связано с определенным назначением.

Если вы назначаете значение holdLastObject перед циклом, оно определенно назначается для всего метода (начиная с пункта объявления) - поэтому, даже если вы не получаете доступ к нему после цикла, GC понимает, что вы мог бы написать код, который обращается к нему, поэтому он не завершает последний экземпляр.

Поскольку вы не присваиваете значение переменной перед циклом, она определенно не назначается, кроме как внутри цикла, поэтому я подозреваю, что GC рассматривает его так, как если бы он был объявлен в цикле, - он знает, что после цикл может читать из переменной (потому что он определенно не назначен), и поэтому он знает, что может завершить и собрать последний экземпляр.

Чтобы уточнить, что я имею в виду, добавьте:

System.out.println(holdLastObject);

непосредственно перед строкой System.gc(), вы обнаружите, что она не будет компилироваться в вашем первом случае (без назначения).

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

EDIT: вопреки ответам TheLostMind, я считаю, что компилятор передает эту информацию в JVM. Используя javap -verbose GCTest, я нашел это без назначения:

  StackMapTable: number_of_entries = 4
    frame_type = 253 /* append */
      offset_delta = 2
      locals = [ top, int ]
    frame_type = 249 /* chop */
      offset_delta = 19
    frame_type = 75 /* same_locals_1_stack_item */
      stack = [ class java/lang/InterruptedException ]
    frame_type = 4 /* same */

и это с указанием:

  StackMapTable: number_of_entries = 4
    frame_type = 253 /* append */
      offset_delta = 4
      locals = [ class GCTest, int ]
    frame_type = 250 /* chop */
      offset_delta = 19
    frame_type = 75 /* same_locals_1_stack_item */
      stack = [ class java/lang/InterruptedException ]
    frame_type = 4 /* same */

Обратите внимание на разницу в части locals первой записи. Странно, что запись class GCTest не появляется нигде без начального присваивания...

Ответ 2

Изучение байт-кода помогает выявить ответ.

Когда вы назначаете null локальной переменной, как упоминал Джон Скит, это определенное назначение, а javac must создает локальную переменную в методе main., как байт-код доказывает:

// access flags 0x9
public static main([Ljava/lang/String;)V
  TRYCATCHBLOCK L0 L1 L2 java/lang/InterruptedException
 L3
  LINENUMBER 12 L3
  ACONST_NULL
  ASTORE 1

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

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

Изучение байт-кода для этого сценария показывает, что весь блок для LINENUMBER 12 отсутствует, следовательно, он правильно доказывает эту теорию.

Примечание:
Насколько мне известно, это поведение не, определенное стандартом Java, и может варьироваться между реализациями javac. Я наблюдал это со следующей версией:

[email protected] ~/src/untracked $ javac -version
javac 1.8.0_31
[email protected] ~/src/untracked $ java -version
openjdk version "1.8.0_31"
OpenJDK Runtime Environment (build 1.8.0_31-b13)
OpenJDK 64-Bit Server VM (build 25.31-b07, mixed mode)

Ответ 3

Я не нашел существенных отличий в байтовом коде для обоих случаев (поэтому не стоит публиковать здесь байтовый код). Поэтому я полагаю, что это связано с оптимизацией JIT/JVM.

Объяснение:

case -1:

public static void main(String[] args) {
  GCTest holdLastObject; //If I assign null here then no of eligible objects are 9 otherwise 10.
     for (int i = 0; i < 10; i++) {
         holdLastObject=new GCTest();
    }
    //System.out.println(holdLastObject); You can't do this here. holdLastObject might not have been initialized.
     System.gc(); //requesting GC
}

Здесь обратите внимание, что вы не инициализировали holdLastObject до null. Таким образом, вне цикла он не может быть доступен (вы получите ошибку времени компиляции). Это означает, что * JVM определяет, что поле не используется в более поздней части. Eclipse дает вам это сообщение. Таким образом, JVM создаст и разрушит все внутри самого цикла. Итак, 10 объектов Gone.

Случай -2:

 public static void main(String[] args) {
      GCTest holdLastObject=null; //If I assign null here then no of eligible objects are 9 otherwise 10.
         for (int i = 0; i < 10; i++) {
             holdLastObject=new GCTest();
        }
        //System.out.println(holdLastObject); You can't do this here. holdLastObject might not have been initialized.
         System.gc(); //requesting GC
    }

В этом случае, поскольку поле инициализируется нулем, оно создается вне цикла и, следовательно, null reference помещается в его слот в таблице локальных переменных. Таким образом, JVM понимает, что поле доступно извне, поэтому оно не уничтожает последний экземпляр, он сохраняет его в живых, поскольку он все еще доступен /readable. Поэтому, если вы явно не установили значение последней ссылки в значение null, оно существует и доступно, Следовательно, 9 экземпляров будут готовы для GC.