Почему в invokevirtual Java требуется разрешить класс времени компиляции вызываемого метода? - PullRequest
11 голосов
/ 02 апреля 2010

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

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

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

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

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

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

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

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

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

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

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

invokevirtual i

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

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

Ответы [ 5 ]

4 голосов
/ 02 апреля 2010

Все дело в производительности. Когда, вычисляя тип времени компиляции (aka: статический тип), JVM может вычислить индекс вызванного метода в таблице виртуальных функций типа времени выполнения (aka: динамический тип). Используя этот индекс, шаг 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, 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, шестой метод по-прежнему foo (хотя на этот раз он будет указывать на B.foo()).

Обновление

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

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

1 голос
/ 02 апреля 2010

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

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

1 голос
/ 02 апреля 2010

Я угадываю ответ "B".

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

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

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

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

1 голос
/ 02 апреля 2010

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

0 голосов
/ 02 апреля 2010

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

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

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

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