Я новичок в Java и программировании в целом. В настоящее время я изучаю, как Java обрабатывает запоминание и область видимости переменных. Я понял следующее:
локальные переменные (т.е. переменные, объявленные внутри методов) запоминаются в стеке соответствующего потока, внутри кадра стека метода, в котором они объявлены. Как только метод завершает свое выполнение, соответствующий кадр стека удаляется из стека, и, следовательно, локальные переменные перестают существовать.
область действия локальных переменных простирается от места их объявления до конца блока, в котором они содержатся, включая любой вложенный блок. Поэтому можно объявить две одноименные переменные внутри одного метода, если их область действия не перекрывается. Поэтому законно написать что-то вроде этого:
public void myMethod(){
String[] myArray = new String[2];
for(int i=0; i< myArray.length; i++){
String message = "hello";
System.out.println(message);
}
if (true){
int number =3;
System.out.println(number);
}
String message = "Ciao";
int number =-50;
System.out.println(message);
System.out.println(number);
}
Я хотел бы понять, как Java обрабатывает память в отношении одноименных переменных, существующих внутри одного и того же метода, но без перекрывающихся областей. Как я уже говорил ранее, я узнал, что локальные переменные хранятся внутри кадра стека метода до тех пор, пока метод не завершится, но если это действительно так, то как две переменные с одинаковым именем могут храниться внутри одного кадра стека? Действительно ли они оба живут в кадре стека метода до тех пор, пока метод не завершится, или же, как только область действия локальной переменной определенно заканчивается, эта переменная удаляется из кадра стека, даже если метод еще не завершен, что позволяет создать другой локальной переменной с тем же именем?
используйте javap, чтобы увидеть байт-код, сгенерированный компилятором




Во время выполнения на переменные ссылаются не по их именам, а по идентификатору слота (это int). Поэтому проблем с перекрытием не возникает.
Но если вы углубитесь в территорию JIT, идентификатор слота превратится в смещение от начала кадра стека.
Локальные переменные не существуют. Совсем.
Java — это многоэтапный процесс. Сначала компилятор (javac.exe) превращает .java файлы в .class файлы. Затем среда выполнения (java.exe) запускает файл класса.
Во время этого первого шага (компиляции), который очень шаблонен (в спецификации точно указано, как работает javac). В отличие от компиляторов C, которым разрешено, например, выполнять глубокий анализ вашего кода, определять, что цикл не имеет сторон. никаких эффектов и просто полностью исключите их из исполняемого файла, который создает gcc), локальные переменные теряются.
В частности, в формате файла класса система использует нечто совсем другое — стек и слоты.
Любой метод объявляет, сколько «слотов» ему нужно. Спецификация Java работает следующим образом: спецификация определяет, что должно произойти и какие гарантии должны быть предоставлены. Никогда не разъясняется, как все делается. Но иногда объяснить одно конкретное «как» проще, чем пытаться углубиться в спецификацию. Помните, что здесь описано, как это делает большинство JVM, но реализация JVM не обязана:
Байт-код не относится к «локальным переменным», поскольку они не существуют в байт-коде. Вместо этого байт-код может:
POP, который удаляет и отбрасывает верхнюю часть стека, или FADD, который извлекает 2 значения из стека, JVM взрывается, если они не являются числами с плавающей точкой (поэтому мы можем предположить, что это так), добавляет два числа с плавающей запятой и помещает их обратно в стек. (Примечание: проверяющий проверяет, что ситуация «взрыва» не может возникнуть. Это звучит более драматично, чем есть на самом деле).ALOAD_1 — это простая инструкция байт-кода, которая извлекает ссылку на объект из «слота №1» (это будет второй слот — первый слот — это слот №0) и помещает ее в стек.Таким образом, этот Java-код:
int a = readInt();
int b = readInt();
println(a + b);
может быть скомпилировано как:
[START METHOD]
[META: SLOT SIZE: 2]
// int a = readInt(); - 'a' translates to slot 0.
INVOKESTATIC com.foo.KeyboardInput readInt()I;
ISTORE_0
// int b = readInt(); - 'b' translates to slot 1.
INVOKESTATIC com.foo.KeyboardInput readInt()I
ISTORE_1
ILOAD_0 // load int value in slot 0 and push onto stack.
ILOAD_1 // load int value in slot 1 and push onto stack.
IADD // pop 2 int values, add them, push it back
INVOKESTATIC com.foo.BasicOutput println(I)V
Где println потребляет 1 целое число из стека.
Давайте теперь «придумаем» наш метод и представим еще одну локальную переменную:
int a = readInt();
int b = readInt();
int c = a + b;
println(c);
Этот код легко определить как полностью идентичный по действию первому. И если вы скомпилируете это, это фактически создаст точно такой же байт-код. Никакого «третьего слота» не появится. Потому что в этом нет необходимости — компилятор не переводит локальную переменную «один к одному» в слот. Пока вы не используете c где-либо еще, javac вполне способен осознать, что нет необходимости в его существовании как слоте.
Фактически, если бы вы скомпилировали приведенный выше код, вы бы получили… вообще никаких слотов! В конце концов, если это весь метод и a/b/c больше нигде не используются, компилятор выдаст:
[START METHOD]
[META: SLOT SIZE: 2]
// readInt(); - just leave the read value on the stack.
INVOKESTATIC com.foo.KeyboardInput readInt()I;
// read a second value from the stack
INVOKESTATIC com.foo.KeyboardInput readInt()I
IADD // pop 2 int values, add them, push it back
INVOKESTATIC com.foo.BasicOutput println(I)V
Нет необходимости даже в одном слоте.
Аналогичным образом представьте себе этот метод:
void example() {
int a = ....;
int b = ....;
// Tons and tons of math with a and b.
println(a + b);
// from here on out, a and b are never used again.
int c = ....;
int d = ....;
// Tons and tons of math with c and d.
// note that a and b are not used in this code
}
Компилятор будет использовать не более двух слотов. Потому что «c» и «d» просто g там, где раньше были «a» и «b». Компилятор вполне способен прийти к выводу, что «a» и «b», поскольку они больше не используются, могут быть «перезаписаны».
Следовательно, однозначного сопоставления локальных переменных со слотами просто не существует:
this в методах экземпляра.Учитывая весь этот контекст, ваш вопрос стал бессмысленным: локальные переменные не существуют в файлах классов, поэтому вопрос «как JVM обращается с двумя локальными переменными с одинаковым именем» тривиален.
У вас небольшая ошибка в INVOKESTATIC com.foo.BasicOutput println(I)V. Неправильный класс и это не статический метод.
@talex Проблема в том, что байт-код для System.out.println на самом деле представляет собой целую процедуру песни и танца: это GETSTATIC для поиска поля out класса java.lang.System, затем вы вызываете метод экземпляра println для этого через (здесь по памяти) INVOKEINTERFACE. Это добавляет всевозможные странные детали, которые совершенно не имеют отношения к вопросу. Отсюда и упрощение.
Вам следует использовать просто printInt(c) вместо System.out.println(c);. Как вы это сделали с readInt().
Ответ @talex отредактирован :)
@rzwitserloot Огромное спасибо! Я искал подробное объяснение, подобное вашему. Есть ли у вас какое-нибудь название книги, которое вы могли бы порекомендовать по этой теме?
Пожалуйста, отредактируйте вопрос, чтобы ограничить его конкретной проблемой и указать достаточно подробностей, чтобы найти адекватный ответ.