Почему invokevirtual Java должен разрешить класс времени компиляции вызываемого метода?

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

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

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

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

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

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

  1. Ищите метод нечто, определенный в классе времени компиляции MyClass. (Это включает сначала разрешение MyClass.)
  2. Сделайте некоторые проверки, включая: Проверьте, что c не является методом инициализации, и проверьте, что вызов MyClass.foo не нарушил бы защищенных модификаторов.
  3. Фигура, который метод на самом деле звонить. В частности, ищите тип выполнения c. Если тот тип имеет нечто (), назовите тот метод и возврат. В противном случае ищите суперкласс типа выполнения c; если тот тип имеет нечто, назовите тот метод и возврат. В противном случае ищите суперкласс суперкласса типа выполнения c; если тот тип имеет нечто, назовите тот метод и возврат. И т.д. Если никакой подходящий метод не может быть найден, то ошибка.

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

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

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

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

invokevirtual i

где я - индекс к пулу константы этапа выполнения MyClass. Та постоянная запись пула будет иметь тип CONSTANT_Methodref_info и укажет (возможно, косвенно) A) на название названного метода (т.е. нечто), B) на сигнатуру метода и C) на название класса времени компиляции, что к методу обращаются (т.е. MyClass).

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

11
задан Chris 7 April 2010 в 01:30
поделиться

5 ответов

Все дело в производительности. Когда, выясняя тип времени компиляции (также известный как статический тип), JVM может вычислить индекс вызванного метода в таблице виртуальных функций типа среды выполнения (также известного как динамический тип). Шаг 3 с помощью этого индекса просто превращается в доступ к массиву, который может быть выполнен за постоянное время. Никакого зацикливания не требуется.

Пример:

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

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

По умолчанию A расширяет 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 , равно , toString , finalize , clone , foo , bar . В этом списке foo является 6-й записью ( hashCode является 1-м, равно 2-м и т. Д.). Этот расчет индекса выполняется один раз - когда JVM загружает файл класса.

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

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

Обновление

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

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

4
ответ дан 3 December 2019 в 11:03
поделиться

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

1
ответ дан 3 December 2019 в 11:03
поделиться

Думаю, ответ "Б".

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

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

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

Если вы проверили только тип времени выполнения (через # 3), то тип времени выполнения может незаконно расширить доступность замещаемого метода (он же «плохая вещь»). Это правда, что компилятор должен предотвращать такой случай, но JVM, тем не менее, защищает себя от мошеннического кода (например, созданного вручную вредоносного кода).

1
ответ дан 3 December 2019 в 11:03
поделиться

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

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

Спасибо Итаю за то, что вдохновил меня на поиск в Google, необходимый, чтобы найти это.

0
ответ дан 3 December 2019 в 11:03
поделиться

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

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

1
ответ дан 3 December 2019 в 11:03
поделиться
Другие вопросы по тегам:

Похожие вопросы: