Почему Java invokevirtual необходимо разрешить класс метода compile-time?

Рассмотрим этот простой класс Java:

class MyClass {
  public void bar(MyClass c) {
    c.foo();
  }
}

Я хочу обсудить, что происходит на линии c.foo().

Оригинальный, вводящий в заблуждение вопрос

Примечание. Не все это происходит с каждым индивидуальным кодом invokevirtual. Подсказка. Если вы хотите понять вызов Java-метода, не читайте только документацию для invokevirtual!

На уровне байт-кода мясо c.foo() будет invokevirtual opcode и, согласно документации для invokevirtual, более или менее произойдет следующее:

  • Посмотрите метод foo, определенный в классе компиляции MyClass. (Это включает в себя сначала разрешение MyClass.)
  • Проделайте некоторые проверки, в том числе: Убедитесь, что c не является методом инициализации и убедитесь, что вызов MyClass.foo не будет нарушать защищенные модификаторы.
  • Выясните, какой метод на самом деле вызывать. В частности, найдите c тип времени выполнения. Если этот тип имеет foo(), вызовите этот метод и верните его. Если нет, найдите c суперкласс класса runtime; если этот тип имеет foo, вызовите этот метод и верните его. Если нет, просмотрите c суперкласса суперкласса типа c; если этот тип имеет foo, вызовите этот метод и верните его. Etc.. Если подходящий метод не найден, тогда ошибка.

Этап № 3 сам по себе кажется достаточным для определения способа вызова и проверки того, что указанный метод имеет правильные типы аргументов/возвратов. Поэтому мой вопрос заключается в том, почему в первую очередь выполняется шаг №1. Возможные ответы:

  • У вас недостаточно информации для выполнения шага № 3 до тех пор, пока не будет выполнен шаг №1. (Это кажется невероятным с первого взгляда, поэтому, пожалуйста, объясните.)
  • Проверки модификатора ссылок или доступа, выполненные в # 1 и # 2, необходимы для предотвращения некоторых плохих вещей, и эти проверки должны выполняться на основе типа времени компиляции, а не иерархии типов выполнения. (Пожалуйста, объясните.)

Пересмотренный вопрос

Ядром вывода компилятора javac для строки c.foo() будет такая инструкция:

invokevirtual i

где я - индекс для пула постоянной среды MyClass. Эта константная запись пула будет иметь тип CONSTANT_Methodref_info и укажет (возможно, косвенно) A) имя метода (т.е. foo), B) подпись метода и C) имя класса времени компиляции, вызываемого методом on (т.е. MyClass).

Вопрос в том, зачем нужна ссылка на тип времени компиляции (MyClass)? Поскольку invokevirtual собирается выполнять динамическую отправку по типу времени выполнения c, не избыточно ли хранить ссылку на класс времени компиляции?

Ответ 1

Это все о производительности. Когда, вычисляя тип времени компиляции (aka: static type), JVM может вычислить индекс вызываемого метода в таблице виртуальных функций типа времени выполнения (aka: dynamic type). Использование этого шага индекса 3 просто становится доступом к массиву, который может быть выполнен в постоянное время. Не требуется цикл.

Пример:

class A {
   void foo() { }
   void bar() { }
}

class B extends A {
  void foo() { } // Overrides A.foo()
}

По умолчанию A extends Object, который определяет эти методы (окончательные методы опущены, поскольку они вызываются через invokespecial):

class Object {
  public int hashCode() { ... }
  public boolean equals(Object o) { ... }
  public String toString() { ... }
  protected void finalize() { ... }
  protected Object clone() { ... }
}

Теперь рассмотрим этот вызов:

A x = ...;
x.foo();

Выяснив, что x статический тип A, JVM также может определить список методов, доступных на этом сайте: hashCode, equals, toString, finalize, clone, foo, bar. В этом списке foo - 6-я запись (hashCode - 1-й, equals - 2-й и т.д.). Этот расчет индекса выполняется один раз - когда JVM загружает файл класса.

После этого всякий раз, когда JVM-процессам x.foo() просто нужно получить доступ к 6-й записи в списке методов, которые предлагает x, эквивалентно x.getClass().getMethods[5] (который указывает на A.foo(), если x динамический тип A) и вызовите этот метод. Не нужно исчерпывающе искать этот массив методов.

Обратите внимание, что индекс метода остается неизменным независимо от динамического типа x. То есть: даже если x указывает на экземпляр B, 6-й метод все еще foo (хотя на этот раз он будет указывать на B.foo()).

Обновление

[В свете вашего обновления]: Ты прав. Для выполнения виртуального метода отправки все потребности JVM - это имя + подпись метода (или смещение внутри vtable). Однако JVM не выполняет слепо. Сначала он проверяет, что загружаемые в него загружаемые файлы верны в процессе, называемом проверка (см. Также здесь).

Проверка выражает один из принципов проектирования JVM: он не полагается на компилятор для создания правильного кода. Он проверяет сам код, прежде чем он разрешит его выполнить. В частности, верификатор проверяет, что каждый вызванный виртуальный метод фактически определяется статическим типом объекта-получателя. Очевидно, для выполнения такой проверки необходим статический тип приемника.

Ответ 2

Это не так, как я понимаю это после прочтения документации. Я думаю, что вы выполнили шаги 2 и 3, что сделало бы последовательность событий более логичной.

Ответ 3

Предположительно, # 1 и # 2 уже произошли с компилятором. Я подозреваю, что, по крайней мере, часть цели состоит в том, чтобы убедиться, что они все еще сохраняются с версией класса в среде выполнения, которая может отличаться от версии, с которой был скомпилирован код.

Я не переварил документацию invokevirtual, чтобы проверить ваше резюме, однако Rob Heiser может быть прав.

Ответ 4

Я предполагаю ответ "B".

Проверки модификатора ссылок или доступа, выполненные в # 1 и # 2, необходимы для предотвращения некоторых плохих вещей, и эти проверки должны выполняться на основе типа времени компиляции, а не иерархии типов выполнения. (Пожалуйста, объясните.)

# 1 описывается 5.4.3.3 Разрешение метода, что делает некоторые важные проверки. Например, # 1 проверяет доступность метода в типе времени компиляции и может возвращать IllegalAccessError, если это не так:

... В противном случае, если ссылочный метод недоступен (п. 5.4.4) до D, разрешение метода вызывает IllegalAccessError....

Если вы проверили только тип времени выполнения (через # 3), тогда тип времени выполнения мог бы незаконно расширить доступность переопределенного метода (a.k.a. a "bad thing" ). Его истинность заключается в том, что компилятор должен предотвратить такой случай, но JVM тем не менее защищает себя от кода изгоев (например, написанного вручную злонамеренного кода).

Ответ 5

Чтобы полностью понять этот материал, вам нужно понять, как работает разрешение метода в Java. Если вы ищете подробное объяснение, я предлагаю взглянуть на книгу "Внутри виртуальной машины Java". Следующие разделы главы 8 "Модель связывания" доступны в Интернете и кажутся особенно актуальными:

(записи CONSTANT_Methodref_info - это записи в заголовке файла класса, описывающие методы, вызываемые этим классом.)

Благодаря Итай за то, что он вдохновил меня на то, чтобы найти Google.