Что из себя представляет механизм полиморфизма в Java и как он работает? Если это зависит от реализации конкретной JVM, то хотелось бы увидеть это на примере какой-нибудь JVM, например HotSpot.
Для уточнения, в С++ полиморфизм реализуется за счет таблицы виртуальных функций для каждого класса, указатель на которую неявно хранится в каждом экземпляре класса. А как это сделано в Java?
Ответ
Полиморфизм метода, определённого в классе, достигается за счёт выбора по таблице виртуальных методов примерно как в C++. То есть накладные расходы на вызов — это взять у объекта classword (который записан в его заголовке), добавить к нему известное заранее смещение и вызвать метод по указанному адресу.
Полиморфизм метода, определённого в интерфейсе, достигается более сложным путём: в структуре описания класса ищется запись, относящаяся к данному интерфейсу, и в ней уже ищется нужный метод. То есть, скажем,
public static
public static
Первый случай может оказаться медленнее, потому что мы вызываем метод интерфейса. Хотя метод один и тот же, но разница есть. Даже в байткоде две разные инструкции — invokeinterface и invokevirtual.
JIT-компилятор HotSpot агрессивно использует технику девиртуализации. Естественно в обычный статический вызов превратится вызов final-метода или метода final-класса. Также если runtime-таблица типов говорит, что данный метод нигде не переопределён, вызов будет статическим:
public static
Хотя ArrayList не final-класс и метод get тоже не final, если мы знаем, что он не переопределён на данный момент ни в одном загруженном классе, мы можем сделать вызов статическим. Если будет загружен новый класс, который нарушит это условие, JIT-компилятор перекомпилирует этот метод.
Если есть варианты, используется профиль типов. К примеру, если этот код уже выполнялся 5000 раз и из них в 4990 случаях вызывался конкретный метод ArrayList.get, то JIT-компилированный код станет примерно таким:
public static
Проверка list.getClass() == ArrayList.class весьма быстрая — это достать classword (по скорости как прочитать поле объекта) и сравнить с константой (на момент JIT-компиляции точно известен classword для класса ArrayList). Branch-prediction тоже хорошо отработает, если условие в подавляющем большинстве случаев выполняется.
Если из 5000 вызовов было 3000 ArrayList и 1990 LinkedList, код будет примерно таким:
public static
Это биморфный вызов. Если же популярных вариантов было больше двух, то тогда уж вызов останется честным виртуальным.
Разумеется, если вы в одном методе вызываете несколько методов неизвестного объекта, переданного параметром (или в цикле вызываете метод много раз), то тип проверяться будет только один раз.
Если вызов удалось девиртуализовать (хотя бы в биморфный вариант), то дальше агрессивно применяется инлайнинг (видал своими глазами как в один метод инлайнилось штук 70 других на глубину вызовов до 8-9: в первый инлайнится второй, в него третий и т. д.). Инлайнинг открывает дорогу к тонне других оптимизаций.
Как вы уже поняли, первоначально код выполняется во-первых, медленнее, а во-вторых в режиме профилирования. То есть при каждом вызове метода не просто происходит вызов, но и обновляется таблица статистики, где указывается, какой конкретно класс тут был. Когда статистика собрана, метод перекомпилируется с учётом неё. При этом если есть быстрая и медленная ветка, то медленная будет обновлять статистику дальше. Например, если сценарий использования программы поменялся, то метод может быть снова перекомпилирован.
Комментариев нет:
Отправить комментарий