Как Java обрабатывает память в отношении одноименных локальных переменных, объявленных внутри разных невложенных блоков кода внутри метода?

Я новичок в 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 обрабатывает память в отношении одноименных переменных, существующих внутри одного и того же метода, но без перекрывающихся областей. Как я уже говорил ранее, я узнал, что локальные переменные хранятся внутри кадра стека метода до тех пор, пока метод не завершится, но если это действительно так, то как две переменные с одинаковым именем могут храниться внутри одного кадра стека? Действительно ли они оба живут в кадре стека метода до тех пор, пока метод не завершится, или же, как только область действия локальной переменной определенно заканчивается, эта переменная удаляется из кадра стека, даже если метод еще не завершен, что позволяет создать другой локальной переменной с тем же именем?

Пожалуйста, отредактируйте вопрос, чтобы ограничить его конкретной проблемой и указать достаточно подробностей, чтобы найти адекватный ответ.

Community 16.06.2024 20:16

используйте javap, чтобы увидеть байт-код, сгенерированный компилятором

user85421 16.06.2024 21:28
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
2
77
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Во время выполнения на переменные ссылаются не по их именам, а по идентификатору слота (это int). Поэтому проблем с перекрытием не возникает.

Но если вы углубитесь в территорию JIT, идентификатор слота превратится в смещение от начала кадра стека.

Ответ принят как подходящий

Локальные переменные не существуют. Совсем.

Java — это многоэтапный процесс. Сначала компилятор (javac.exe) превращает .java файлы в .class файлы. Затем среда выполнения (java.exe) запускает файл класса.

Во время этого первого шага (компиляции), который очень шаблонен (в спецификации точно указано, как работает javac). В отличие от компиляторов C, которым разрешено, например, выполнять глубокий анализ вашего кода, определять, что цикл не имеет сторон. никаких эффектов и просто полностью исключите их из исполняемого файла, который создает gcc), локальные переменные теряются.

В частности, в формате файла класса система использует нечто совсем другое — стек и слоты.

Любой метод объявляет, сколько «слотов» ему нужно. Спецификация Java работает следующим образом: спецификация определяет, что должно произойти и какие гарантии должны быть предоставлены. Никогда не разъясняется, как все делается. Но иногда объяснить одно конкретное «как» проще, чем пытаться углубиться в спецификацию. Помните, что здесь описано, как это делает большинство JVM, но реализация JVM не обязана:

  • Любой метод, который объявляет, скажем, «Я хочу 5 слотов», приводит к тому, что при входе указатель стека увеличивается на 5. Верификатор класса Java уже проверяет, что любой данный метод не может «вытащить» из стека больше значений, чем он «толкает».
  • Методы всегда начинаются с параметров, уже помещенных в стек. Среда выполнения решает, как рифмовать этот пункт и предыдущий - он может поменять их местами со «слотами», или сохранить «слоты» в другом месте, или сохранить слоты в конце, или проверить, сколько места в стеке ему нужно, и оставьте «слоты» за этим.
  • Метод состоит из байт-кода и выполняется.

Байт-код не относится к «локальным переменным», поскольку они не существуют в байт-коде. Вместо этого байт-код может:

  • Обратитесь к стеку, например 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», поскольку они больше не используются, могут быть «перезаписаны».

Следовательно, однозначного сопоставления локальных переменных со слотами просто не существует:

  • Локальная переменная может вообще не привести к созданию слота.
  • Один слот может служить местом хранения нескольких локальных файлов.
  • Слот может быть объявлен, даже если не существует ни одной локальной переменной.
  • Одна локальная переменная может в конечном итоге использоваться в разных позициях слота.
  • Слот 0 всегда зарезервирован для ссылки this в методах экземпляра.
  • Имена локальных переменных не находятся в файлах классов. Файлу класса не обязательно знать, что переменные в наших математических частях называются «a» и «b». Это освобождает — называйте переменные как хотите. Предпочтительно, что-то, что четко указывает на то, что они делают. Если вы выберете более длинные имена, это не повлияет ни на размер файла класса, ни на скорость его работы. В отличие от, например, javascript, где теоретически это возможно, и минимизаторы JS любят продвигать идею о том, что это не только делает вещи меньше, но и ускоряет их. Ничего из этого не относится к Java. Java всегда быстрая.

Учитывая весь этот контекст, ваш вопрос стал бессмысленным: локальные переменные не существуют в файлах классов, поэтому вопрос «как JVM обращается с двумя локальными переменными с одинаковым именем» тривиален.

У вас небольшая ошибка в INVOKESTATIC com.foo.BasicOutput println(I)V. Неправильный класс и это не статический метод.

talex 16.06.2024 20:59

@talex Проблема в том, что байт-код для System.out.println на самом деле представляет собой целую процедуру песни и танца: это GETSTATIC для поиска поля out класса java.lang.System, затем вы вызываете метод экземпляра println для этого через (здесь по памяти) INVOKEINTERFACE. Это добавляет всевозможные странные детали, которые совершенно не имеют отношения к вопросу. Отсюда и упрощение.

rzwitserloot 16.06.2024 21:36

Вам следует использовать просто printInt(c) вместо System.out.println(c);. Как вы это сделали с readInt().

talex 16.06.2024 21:39

Ответ @talex отредактирован :)

rzwitserloot 16.06.2024 22:12

@rzwitserloot Огромное спасибо! Я искал подробное объяснение, подобное вашему. Есть ли у вас какое-нибудь название книги, которое вы могли бы порекомендовать по этой теме?

Ignis 17.06.2024 14:31

Другие вопросы по теме