Почему у JVM есть оба метода invokepecial и invokestatic?

В обеих инструкциях используется статическая, а не динамическая отправка. Похоже, единственное существенное различие заключается в том, что invokespecial всегда будет иметь в качестве первого аргумента объект, являющийся экземпляром класса, к которому принадлежит отправленный метод. Однако invokespecial фактически не помещает объект туда; компилятор отвечает за то, чтобы это произошло, испустив соответствующую последовательность операций стека перед выпуском invokespecial. Поэтому замена invokespecial на invokestatic не должна влиять на способ управления стеком/кучей времени выполнения, хотя я ожидаю, что это приведет к ошибке VerifyError за нарушение спецификации.

Мне интересно узнать о возможных причинах создания двух разных инструкций, которые делают практически одно и то же. Я взглянул на источник интерпретатора OpenJDK, и кажется, что invokespecial и invokestatic обрабатываются почти одинаково. Имеет ли две отдельные инструкции, чтобы компилятор JIT лучше оптимизировал код, или он помогает верификатору classfile более эффективно доказывать некоторые свойства безопасности? Или это просто причуда в дизайне JVM?

Ответ 1

Существуют определения:


Существуют значительные различия. Предположим, мы хотим разработать инструкцию invokesmart, которая бы решительно выбрала между inkovestatic и invokespecial:

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

Во-первых, что означает invokesmart foo/Bar.baz(I)I? Это может означать:

  • Статический вызов метода foo.Bar.baz, который потребляет int из стека операндов и добавляет еще один int. // (int) -> (int)дел >
  • Вызов метода экземпляра foo.Bar.baz, который потребляет foo.Bar и int из стека операндов и добавляет int. // (foo.Bar, int) -> (int)дел >

Как бы вы выбрали из них? Могут существовать оба метода.

Мы можем попытаться решить эту проблему, потребовав foo/Bar.baz(Lfoo/Bar;I) для статического вызова. Однако мы можем иметь как public static int baz(Bar, int), так и public int baz(int).


Мы можем сказать, что это не имеет значения и, возможно, отключает такую ​​ситуацию. (Я не думаю, что это хорошая идея, но просто чтобы представить.) Что это значит?

  • Если метод статичен, вероятно, никаких дополнительных ограничений не существует. С другой стороны, если метод не является статичным, существуют некоторые ограничения: "Наконец, если защищенный метод защищен (§4.6), и он либо является членом текущего класса, либо членом суперкласса текущего class, то класс objectref должен быть либо текущим классом, либо подклассом текущего класса."
  • Есть еще некоторые отличия, см. примечание о ACC_SUPER.
  • Это означало бы, что все ссылочные классы должны быть загружены до проверки байт-кода. Надеюсь, сейчас это не нужно, но я не уверен на 100%.

Таким образом, это будет означать очень непоследовательное поведение.

Ответ 2

Отказ от ответственности. Трудно сказать наверняка, так как я никогда не читал явное выражение Oracle об этом, но я в значительной степени думаю, что это причина:

Когда вы смотрите на код байта Java, вы можете задать тот же вопрос и о других инструкциях. Почему верификатор остановит вас, нажав два int на стек и рассматривая их как один long сразу после? (Попробуйте, это остановит вас.) Вы можете утверждать, что, разрешив это, вы могли бы выразить ту же логику с меньшим набором команд. (Чтобы идти дальше с этим аргументом, байт не может выражать слишком много инструкций, поэтому набор байтов Java должен, по возможности, сокращаться.)

Конечно, теоретически вам не понадобится инструкция байтового кода для нажатия int и long в стек, и вы правы в том, что вам не нужны две команды для INVOKESPECIAL и INVOKESTATIC чтобы выразить вызовы метода. Метод уникально идентифицируется его дескриптором метода (имя и сырые типы аргументов), и вы не могли определить статический и нестатический метод с одинаковым описанием внутри одного и того же класса. И чтобы проверить байтовый код, компилятор Java должен проверить, существует ли целевой метод static.

Примечание: Это противоречит ответу v6ak. Однако дескриптор методов нестатического метода не изменяется, чтобы включить ссылку на this.getClass(). Таким образом, среда выполнения Java всегда может вывести соответствующую привязку метода из дескриптора метода для гипотетической инструкции INVOKESMART. См. JVMS §4.3.3.

Так много для теории. Однако намерения, выраженные обоими типами вызовов, совершенно разные. И помните, что байт-код Java должен использоваться другими инструментами, чем javac для создания приложений JVM. С помощью байтового кода эти инструменты производят нечто более похожее на машинный код, чем исходный код Java. Но это все еще довольно высокий уровень. Например, байт-код все еще проверяется, и байт-код автоматически оптимизируется при компиляции в машинный код. Однако байт-код представляет собой абстракцию, которая преднамеренно содержит некоторую избыточность, чтобы сделать более понятным значение байтового кода. И так же, как язык Java использует разные имена для подобных вещей, чтобы сделать язык более удобочитаемым, набор команд байтового кода содержит некоторую избыточность. И как еще одно преимущество, проверка и компиляция кода проверки и байт-кода могут ускоряться, поскольку тип вызова метода не всегда должен быть выведен, но явно указан в байтовом коде. Это желательно, потому что проверка, интерпретация и компиляция выполняются во время выполнения.

В качестве заключительного анекдота я должен упомянуть, что статический инициализатор класса <clinit> не был помечен static до Java 5. В этом контексте статический вызов также мог быть выведен именем метода, но это могло бы привести к еще большему время выполнения.

Ответ 3

Чтобы получить четкую практическую идею об этих кодах, вам нужно добавить плагин eclipse для ASM в свою среду разработки eclipse и узнать, что генерируется байт-код для вашей созданной вами программы Hello World.