Нет принципиальной разницы между локальной переменной и записью в стеке операндов. Оба живут в одном кадре стека. Ни один из них официально не объявлен, и обеим требуется, чтобы 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-компилятор генерирует код, ему все равно нужно использовать информацию о типе и можно подготовить информацию для сборщика мусора, но это будет сделано только для определенных точек, называемых * 1024. * safepoints , где сгенерированный код проверяет, есть ли ожидающая сборка мусора. Это означает, что между этими точками данные не обязательно должны быть в форме, понятной сборщику мусора, и оптимизированный код может предполагать, что сборщик мусора не будет перемещать объекты во время их обработки.
Это также подразумевает, что в скомпилированном оптимизированном коде достижимость может быть совершенно иной, чем при простом интерпретируемом выполнении, то есть неиспользуемые переменные могут отсутствовать, но даже объекты, используемые с точки зрения исходного кода, могут считаться неиспользованными, когда оптимизированный код работает с копиями их полей, например, в регистрах ЦП.