Если нужны разные JVM для разных архитектур, я не могу понять, какова логика внедрения этой концепции. В других языках нам нужны разные компиляторы для разных машин, но в Java нам требуются разные JVM, так какова логика введения концепции JVM или этого дополнительного шага?
37
Ответы:
Логика заключается в том, что байт-код JVM намного проще, чем исходный код Java.
На высоко абстрактном уровне можно считать, что компиляторы состоят из трех основных частей: синтаксического анализа, семантического анализа и генерации кода.
Синтаксический анализ заключается в чтении кода и превращении его в древовидное представление в памяти компилятора. Семантический анализ - это та часть, где он анализирует это дерево, выясняет, что оно означает, и упрощает все высокоуровневые конструкции до низкоуровневых. И генерация кода берет упрощенное дерево и записывает его в плоский вывод.
С файлом байт-кода фаза синтаксического анализа значительно упрощается, поскольку она написана в том же формате плоского потока байтов, который использует JIT, а не в рекурсивном (древовидном) исходном языке. Кроме того, большая часть тяжелого семантического анализа уже была выполнена компилятором Java (или другого языка). Таким образом, все, что нужно сделать, - это потоковое чтение кода, выполнить минимальный анализ и минимальный семантический анализ, а затем выполнить генерацию кода.
Это делает задачу, которую JIT должен выполнять намного проще, и, следовательно, намного быстрее, в то же время сохраняя метаданные и семантическую информацию высокого уровня, которые позволяют теоретически писать кроссплатформенный код с одним источником.
источник
Промежуточные представления различных видов все чаще встречаются в проектировании компилятора / среды выполнения по нескольким причинам.
В случае с Java причиной номер один изначально была, вероятно, мобильность : изначально Java широко продавался как «Пиши один раз, запускай где угодно». Хотя этого можно достичь, распространяя исходный код и используя разные компиляторы для разных платформ, у этого есть несколько недостатков:
Другие преимущества промежуточного представления включают в себя:
источник
Похоже, вы удивляетесь, почему мы не просто распространяем исходный код. Позвольте мне перевернуть этот вопрос: почему бы нам просто не распространять машинный код?
Ясно, что ответ здесь заключается в том, что Java по своей природе не предполагает, что она знает, на какой машине находится ваш код; это может быть настольный компьютер, суперкомпьютер, телефон или что-то среднее между ними и за их пределами. Java оставляет место для локального компилятора JVM, чтобы делать свое дело. В дополнение к увеличению переносимости вашего кода, у этого есть приятное преимущество: он позволяет компилятору делать такие вещи, как использование машинно-зависимых оптимизаций, если они существуют, или по-прежнему генерировать хотя бы работающий код, если их нет. Такие вещи, как инструкции SSE или аппаратное ускорение, могут использоваться только на тех машинах, которые их поддерживают.
С этой точки зрения обоснованность использования байт-кода поверх необработанного исходного кода более ясна. Приближаясь как можно ближе к необработанному машинному языку, мы можем реализовать или частично реализовать некоторые из преимуществ машинного кода, такие как:
Обратите внимание, что я не упоминаю более быстрое выполнение. И исходный код, и байт-код могут или могут (теоретически) быть полностью скомпилированы в один и тот же машинный код для фактического выполнения.
Кроме того, байт-код допускает некоторые улучшения по сравнению с машинным кодом. Конечно, есть независимость от платформы и аппаратная оптимизация, о которой я упоминал ранее, но есть и такие вещи, как обслуживание компилятора JVM для создания новых путей выполнения из старого кода. Это может быть для исправления проблем безопасности, или в случае обнаружения новых оптимизаций, или для использования преимуществ новых инструкций по оборудованию. На практике редко можно увидеть большие изменения таким образом, потому что это может выявить ошибки, но это возможно, и это то, что постоянно происходит маленькими способами.
источник
Кажется, здесь есть как минимум два разных возможных вопроса. Один действительно о компиляторах вообще, с Java в основном только пример жанра. Другой более специфичен для Java - конкретные байтовые коды, которые он использует.
Компиляторы в целом
Давайте сначала рассмотрим общий вопрос: почему компилятор использует некоторое промежуточное представление в процессе компиляции исходного кода для запуска на каком-то конкретном процессоре?
Снижение сложности
Один из ответов на этот вопрос довольно прост: он преобразует задачу O (N * M) в задачу O (N + M).
Если нам дано N исходных языков и M целей, и каждый компилятор полностью независим, то нам нужно N * M компиляторов для преобразования всех этих исходных языков во все эти цели (где «target» - это что-то вроде комбинации процессор и ОС).
Однако, если все эти компиляторы согласовывают общее промежуточное представление, тогда мы можем иметь N внешних интерфейсов компилятора, которые переводят исходные языки в промежуточное представление, и M внутренних частей компилятора, которые переводят промежуточное представление во что-то подходящее для конкретной цели.
Проблема сегментации
Более того, он разделяет проблему на два более или менее эксклюзивных домена. Люди, которые знают / заботятся о дизайне языка, разборе и подобных вещах, могут сосредоточиться на внешних интерфейсах компилятора, в то время как люди, которые знают о наборах команд, дизайне процессора и подобных вещах, могут сосредоточиться на серверной части.
Так, например, учитывая что-то вроде LLVM, у нас есть много внешних интерфейсов для разных языков. У нас также есть бэк-энды для множества разных процессоров. Специалист по языку может написать новый интерфейс для своего языка и быстро поддержать множество целей. Парень из процессора может написать новый бэкэнд для своей цели, не занимаясь языковым дизайном, анализом и т. Д.
Разделение компиляторов на внешний и внутренний интерфейсы с промежуточным представлением для взаимодействия между ними не является оригинальным в Java. Долгое время это было довольно распространенной практикой (во всяком случае, задолго до появления Java).
Модели распространения
В той мере, в которой Java добавил что-то новое в этом отношении, это было в модели распространения. В частности, даже если компиляторы были разделены на внутренние и внутренние части в течение длительного времени, они, как правило, распространялись как один продукт. Например, если вы купили компилятор Microsoft C, внутри он имел «C1» и «C2», которые были интерфейсом и бэкэндом соответственно - но вы купили только «Microsoft C», который включал оба части (с "драйвером компилятора", который координировал операции между ними). Несмотря на то, что компилятор был построен из двух частей, для обычного разработчика, использующего компилятор, это была всего лишь одна вещь, которая переводилась из исходного кода в объектный код, и между ними ничего не было видно.
Вместо этого Java распространяла интерфейс в Java Development Kit, а интерфейс в виртуальной машине Java. У каждого пользователя Java был серверный компилятор, предназначенный для любой системы, которую он использовал. Разработчики Java распространяли код в промежуточном формате, поэтому, когда пользователь загружал его, JVM делала все необходимое для его выполнения на своей конкретной машине.
Прецеденты
Обратите внимание, что эта модель распределения не была полностью новой. Например, P-система UCSD работала аналогично: внешние интерфейсы компилятора создавали P-код, и каждая копия P-системы включала виртуальную машину, которая выполняла то, что было необходимо для выполнения P-кода на этой конкретной цели 1 .
Java-байт-код
Java-байт-код очень похож на P-код. Это в основном инструкции для довольно простой машины. Предполагается, что эта машина является абстракцией существующих машин, поэтому ее довольно легко быстро перевести практически к любой конкретной цели. Простота перевода была важна на раннем этапе, потому что первоначальное намерение состояло в том, чтобы интерпретировать байтовые коды, как это делала P-System (и, да, именно так работали ранние реализации).
Сильные стороны
Java-байт-код легко создать для внешнего интерфейса компилятора. Если (например) у вас есть довольно типичное дерево, представляющее выражение, обычно довольно легко пройти по дереву и сгенерировать код достаточно непосредственно из того, что вы найдете в каждом узле.
Байт-коды Java довольно компактны - в большинстве случаев гораздо более компактны, чем исходный код или машинный код для большинства типичных процессоров (и, особенно для большинства процессоров RISC, таких как SPARC, продаваемый Sun при разработке Java). Это было особенно важно в то время, потому что одной из основных целей Java была поддержка апплетов - кода, встроенного в веб-страницы, который должен быть загружен перед выполнением - в то время, когда большинство людей обращалось к нам через модемы по телефонным линиям около 28,8. килобит в секунду (хотя, конечно, было еще немало людей, использующих более старые, более медленные модемы).
Слабые стороны
Основным недостатком байт-кодов Java является то, что они не особенно выразительны. Хотя они могут достаточно хорошо выражать концепции, представленные в Java, они не так хорошо работают для выражения концепций, не являющихся частью Java. Точно так же, хотя на большинстве машин легко выполнять байт-коды, гораздо сложнее сделать это таким образом, чтобы в полной мере использовать преимущества любой конкретной машины.
Например, довольно обычным делом является то, что если вы действительно хотите оптимизировать байтовые коды Java, вы в основном выполняете реверс-инжиниринг, чтобы перевести их обратно из представления, подобного машинному коду, и превратить их обратно в инструкции SSA (или что-то подобное) 2 . Затем вы манипулируете инструкциями SSA, чтобы выполнить оптимизацию, а затем переводите что-то, что соответствует архитектуре, которая вас действительно интересует. Однако даже с этим довольно сложным процессом некоторые концепции, которые чужды Java, достаточно сложно выразить, что трудно перевести из некоторых исходных языков в машинный код, который оптимально работает (даже близко) на большинстве типичных машин.
Резюме
Если вы спрашиваете, зачем вообще использовать промежуточные представления, то есть два основных фактора:
Если вы спрашиваете об особенностях байт-кодов Java и о том, почему они выбрали именно это представление вместо какого-то другого, то я бы сказал, что ответ в значительной степени возвращается к их первоначальному замыслу и ограничениям сети в то время. , что приводит к следующим приоритетам:
Возможность представлять много языков или оптимально выполнять самые разные задачи была гораздо более низкими приоритетами (если они вообще считались приоритетами).
источник
В дополнение к преимуществам, на которые указывали другие люди, байт-код намного меньше, поэтому его легче распространять и обновлять, и он занимает меньше места в целевой среде. Это особенно важно в условиях ограниченного пространства.
Это также облегчает защиту защищенного авторским правом исходного кода.
источник
Смысл в том, что компиляция из байтового кода в машинный код происходит быстрее, чем интерпретация исходного кода в машинный код как раз вовремя. Но нам нужны интерпретации, чтобы сделать наше приложение кроссплатформенным, потому что мы хотим использовать наш оригинальный код на каждой платформе без изменений и без каких-либо подготовительных действий (компиляций). Итак, сначала javac компилирует наш исходный код в байтовый код, затем мы можем запустить этот байтовый код где угодно, и виртуальная машина Java будет интерпретировать его для машинного кода быстрее. Ответ: это экономит время.
источник
Первоначально JVM был чистым переводчиком . И вы получите переводчика с наилучшими характеристиками, если язык, который вы переводите, максимально прост . Это была цель байт-кода: обеспечить эффективно интерпретируемый ввод в среду выполнения. Это единственное решение поместило Java ближе к скомпилированному языку, чем к интерпретируемому языку, если судить по его производительности.
Лишь позже, когда стало очевидно, что производительность интерпретирующих JVM все еще не оправдала себя, люди вложили усилия в создание высокопроизводительных компиляторов точно в срок. Это несколько сократило разрыв с более быстрыми языками, такими как C и C ++. (Однако некоторые проблемы со скоростью, свойственные Java, остаются, поэтому вы, вероятно, никогда не получите среду Java, которая работает так же хорошо, как хорошо написанный код C).
Конечно, имея в своем распоряжении методы своевременной компиляции, мы могли бы вернуться к фактическому распространению исходного кода и его своевременной компиляции в машинный код. Однако это сильно снизит производительность при запуске, пока все соответствующие части кода не будут скомпилированы. Байтовый код по-прежнему очень полезен, потому что анализировать его намного проще, чем эквивалентный Java-код.
источник
Текстовый исходный код - это структура, которая должна легко читаться и изменяться человеком.
Байт-код - это структура, которая должна быть легко читаемой и исполняемой машиной.
Поскольку все, что JVM делает с кодом, читается и исполняется, байт-код лучше подходит для использования JVM.
Я заметил, что еще не было примеров. Глупые псевдо-примеры:
Конечно, байт-код - это не только оптимизация. Большая часть этого заключается в способности выполнять код, не заботясь о сложных правилах, таких как проверка, содержит ли класс член с именем "foo" где-то ниже в файле, когда метод ссылается на "foo".
источник