Почему у 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.