Как сборщик мусора обновляет ссылки, помещаемые в стек операндов?

JVM может легко обновлять ссылки на локальные переменные, статические ссылки, экземпляры классов или экземпляры массива объектов при перемещении объекта в куче. Но как он может обновить ссылки, помещенные в стек операндов?


person neoexpert    schedule 03.03.2020    source источник
comment
Как вы думаете, что может сделать это трудным?   -  person Louis Wasserman    schedule 03.03.2020
comment
Значение в стеке операндов может иметь любой тип. Как мы можем узнать, является ли это ссылкой или просто целым числом?   -  person neoexpert    schedule 03.03.2020
comment
Откуда МЫ можем знать, как ВЫ реализовали свою виртуальную машину?   -  person Gyro Gearless    schedule 03.03.2020
comment
Почему вы предполагаете, что JVM не отслеживает тип каждого значения в стеке?   -  person Louis Wasserman    schedule 03.03.2020
comment
Я знаю, что атрибут CodeAttribute метода может иметь атрибут StackMapTable. Но я не нашел никакой информации, можно ли ее использовать для сборки мусора.   -  person neoexpert    schedule 03.03.2020
comment
@Wasserman Я думаю, вы могли бы делать это с каждой инструкцией ALOAD, может быть, так оно и есть. Спасибо за совет.   -  person neoexpert    schedule 03.03.2020
comment
Дело в том, что JVM нужно отслеживать каждую LOAD-инструкцию во время выполнения. Это замедляет выполнение...   -  person neoexpert    schedule 03.03.2020
comment
HotSpot JVM использует структуры OopMap для поиска и обновления ссылок на объекты в стеке.   -  person apangin    schedule 03.03.2020


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

Это также подразумевает, что в скомпилированном оптимизированном коде достижимость может быть совершенно иной, чем в простом интерпретируемом выполнении, то есть неиспользуемые переменные могут отсутствовать, но даже объекты, используемые с точки зрения исходного кода, могут считаться неиспользуемыми, когда оптимизированный код работает с копии их полей, например. в регистрах процессора.

person Holger    schedule 03.03.2020
comment
Всегда ли типы таблицы локальных переменных и стека операндов в конкретной точке выполнения (счетчик программы) одинаковы (независимо от предыдущих ветвей выполнения)? Возможно, JVM могла бы заранее создать таблицу сопоставления типов для каждого метода для каждого возможного значения счетчика программы. - person neoexpert; 03.03.2020
comment
Да, именно это он и делает при проверке байт-кода, которая выполняется при загрузке класса. - person Antimony; 03.03.2020
comment
Именно это я имел в виду под «Реализация может даже иметь один анализирующий код, сохраняющий информацию о типе первого анализа», которая обычно представляет собой информацию, собранную во время проверки. Однако вы должны иметь в виду, что хранение его для каждой инструкции — это большой объем данных, в то время как вероятность того, что это текущая инструкция во время фазы остановки мира, очень мала для отдельной инструкции. Было бы разумнее сохранить их только для точек слияния ветвей (которые предоставляют таблицы карт стека), вызовов методов и инструкций по размещению, а другие делать вывод на лету. - person Holger; 04.03.2020