Поведение 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
.
Может кто-нибудь объяснить это? Я не могу найти правильную причину.
Ответы
Ответ 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.