JVM может легко обновлять ссылки на локальные переменные, статические ссылки, экземпляры классов или экземпляры массива объектов при перемещении объекта в куче. Но как он может обновить ссылки, помещенные в стек операндов?
Как сборщик мусора обновляет ссылки, помещаемые в стек операндов?
Ответы (1)
Принципиальной разницы между локальной переменной и записью в стеке операндов нет. Оба живут в одном кадре стека. Ни один из них не объявлен официально, и оба нуждаются в JVM для выполнения вывода, чтобы распознать их фактическое использование.
Следующий код
public static void example() {
{
int foo = 42;
}
{
Object bar = "text";
}
{
long x = 100L;
}
{
Object foo, bar = new Object();
}
}
будет (обычно) компилироваться в
public static void example();
Code:
0: bipush 42
2: istore_0
3: ldc #1 // String text
5: astore_0
6: ldc2_w #2 // long 100l
9: lstore_0
10: new #4 // class java/lang/Object
13: dup
14: invokespecial #5 // Method java/lang/Object."<init>":()V
17: astore_1
18: return
Обратите внимание, как локальная переменная с индексом 0
в кадре стека переназначается со значениями разных типов. В качестве бонуса последнее сохранение в переменную с индексом 1
делает недействительной переменную с индексом 0
, поскольку в противном случае она содержала бы оборванную половину значения long
.
Нет дополнительных подсказок о типе локальных переменных, отладочная информация необязательна, а таблицы карты стека присутствуют только тогда, когда код содержит ветки.
Единственный способ определить, содержит ли локальная переменная ссылку, — это проследить ход выполнения программы и отследить действие инструкций. Это уже подразумевает вывод значений в стеке операндов, так как без этого мы бы даже не знали, что инструкция store
поместила в переменную.
Это делает верификатор, он даже обязателен, и сборщик мусора или любой другой поддерживающий код JVM тоже может это делать. Реализация может даже иметь один анализирующий код, сохраняющий информацию о типе первого анализа, который будет проверкой.
Но даже если эта информация восстанавливается каждый раз, когда она нужна сборщику мусора, накладные расходы не будут астрономическими. Сборщик мусора запускается только периодически, и ему нужна эта информация только для текущих выполняемых методов. И это все только об интерпретируемом исполнении.
Когда JIT-компилятор генерирует код, ему в любом случае необходимо использовать информацию о типе, и он может подготовить информацию для сборщика мусора, но он будет делать это только для определенных точек, называемых safepoints, где сгенерированный код проверяет, существует ли выдающийся сбор мусора. Это означает, что между этими точками данные не обязательно должны быть в форме, понятной сборщику мусора, и оптимизированный код может предполагать, что сборщик мусора не будет перемещать объекты во время их обработки.
Это также подразумевает, что в скомпилированном оптимизированном коде достижимость может быть совершенно иной, чем в простом интерпретируемом выполнении, то есть неиспользуемые переменные могут отсутствовать, но даже объекты, используемые с точки зрения исходного кода, могут считаться неиспользуемыми, когда оптимизированный код работает с копии их полей, например. в регистрах процессора.