Какая польза от преобразования исходного кода в байт-код Java?

37

Если нужны разные JVM для разных архитектур, я не могу понять, какова логика внедрения этой концепции. В других языках нам нужны разные компиляторы для разных машин, но в Java нам требуются разные JVM, так какова логика введения концепции JVM или этого дополнительного шага?

Пранджал Кумар
источник
12
@gnat: На самом деле это не дубликат. Это «исходный код против байтового кода», т.е. только первое преобразование. В языковых терминах это Javascript или Java; Ваша ссылка будет C ++ против Java.
MSalters
2
Вы бы предпочли написать простой интерпретатор байт-кода для тех 50 моделей устройств, к которым вы добавляете цифровое кодирование для обновления, или 50 компиляторов для 50 различных аппаратных средств. Ява была первоначально разработана для приборов и машин. Это был его сильный костюм. Помните об этом, когда читаете эти ответы, так как в настоящее время Java не имеет истинного преимущества (из-за неэффективности процесса интерпретации). Это просто модель, которую мы продолжаем использовать.
Великая утка
1
Вы , кажется , не понимают , что виртуальная машина находится . Это машина. Это может быть реализовано на аппаратном уровне с компиляторами собственного кода (и в случае с JVM это было). Здесь важна «виртуальная» часть: вы, по сути, эмулируете эту архитектуру поверх другой. Скажем, я написал эмулятор 8088 для запуска на x86. Вы не собираетесь переносить старый код 8088 на x86, вы просто собираетесь запустить его на эмулированной платформе. JVM - это машина, на которую вы ориентируетесь, как и любая другая, с той разницей, что она работает поверх других платформ.
Джаред Смит
7
@TheGreatDuck Процесс интерпретации? В настоящее время большинство JVM выполняют компиляцию точно в срок для машинного кода. Не говоря уже о том, что «интерпретация» - это довольно широкий термин в наши дни. Сам процессор просто «интерпретирует» код x86 в свой собственный внутренний микрокод, и он используется для повышения эффективности. Последние процессоры Intel очень хорошо подходят и для переводчиков в целом (хотя вы, конечно, найдете эталоны, чтобы доказать все, что вы хотите доказать).
Луаан

Ответы:

79

Логика заключается в том, что байт-код JVM намного проще, чем исходный код Java.

На высоко абстрактном уровне можно считать, что компиляторы состоят из трех основных частей: синтаксического анализа, семантического анализа и генерации кода.

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

С файлом байт-кода фаза синтаксического анализа значительно упрощается, поскольку она написана в том же формате плоского потока байтов, который использует JIT, а не в рекурсивном (древовидном) исходном языке. Кроме того, большая часть тяжелого семантического анализа уже была выполнена компилятором Java (или другого языка). Таким образом, все, что нужно сделать, - это потоковое чтение кода, выполнить минимальный анализ и минимальный семантический анализ, а затем выполнить генерацию кода.

Это делает задачу, которую JIT должен выполнять намного проще, и, следовательно, намного быстрее, в то же время сохраняя метаданные и семантическую информацию высокого уровня, которые позволяют теоретически писать кроссплатформенный код с одним источником.

Мейсон Уилер
источник
7
Некоторые другие ранние попытки распространения апплета, такие как SafeTCL, действительно распространяли исходный код. Использование Java простого и строго определенного байт-кода делает проверку программы более удобной, и это была сложная проблема, которая решалась. Такие байт-коды, как p-код, уже были известны как часть решения проблемы переносимости (и ANDF, вероятно, находился в стадии разработки).
Тоби Спейт
9
Точно. Время запуска Java уже немного проблематично из-за шага байт-кода -> машинного кода. Запустите javac для своего (нетривиального) проекта, а затем представьте, что выполняйте весь этот машинный код Java -> при каждом запуске.
Пол Дрэйпер
24
Это имеет еще одно огромное преимущество: если когда-нибудь мы все захотим перейти на гипотетический новый язык - назовем его «Scala» - нам нужно написать только один компилятор байт-кода Scala, а не десятки машинного кода Scala. компиляторы. В качестве бонуса мы получаем все оптимизации для платформы JVM бесплатно.
BlueRaja - Дэнни Пфлюгофт
8
Некоторые вещи все еще невозможны в байт-коде JVM, такие как оптимизация хвостового вызова. Напоминаю, что это сильно компрометирует функциональный язык, который компилируется в JVM.
JDługosz
8
@ JDługosz верно: JVM, к сожалению, накладывает некоторые ограничения / идиомы дизайна, которые, хотя они могут быть совершенно естественными, если вы пришли из императивного языка, могут стать довольно искусственным препятствием, если вы хотите написать компилятор для языка, который работает фундаментально другой. Таким образом, я считаю LLVM лучшей целью для будущего повторного использования языка - у него тоже есть свои ограничения, но они более или менее соответствуют ограничениям, которые есть у нынешних (и, вероятно, когда-нибудь в будущем) процессоров.
leftaround около
27

Промежуточные представления различных видов все чаще встречаются в проектировании компилятора / среды выполнения по нескольким причинам.

В случае с Java причиной номер один изначально была, вероятно, мобильность : изначально Java широко продавался как «Пиши один раз, запускай где угодно». Хотя этого можно достичь, распространяя исходный код и используя разные компиляторы для разных платформ, у этого есть несколько недостатков:

  • компиляторы - это сложные инструменты, которые должны понимать все удобные синтаксисы языка; байт-код может быть более простым языком, поскольку он ближе к машинно-исполняемому коду, чем к читаемому человеком источнику; это означает:
    • компиляция может быть медленной по сравнению с выполнением байт-кода
    • компиляторы, нацеленные на разные платформы, могут в конечном итоге приводить к разному поведению или не успевать за изменениями языка
    • создать компилятор для новой платформы намного сложнее, чем создать виртуальную машину (или компилятор с байт-кодом для этой платформы) для этой платформы
  • распространение исходного кода не всегда желательно; Байт-код обеспечивает некоторую защиту от обратного инжиниринга (хотя декомпилировать его довольно легко, если его не запутать намеренно)

Другие преимущества промежуточного представления включают в себя:

  • оптимизация , при которой шаблоны могут быть обнаружены в байт-коде и скомпилированы до более быстрых эквивалентов или даже оптимизированы для особых случаев во время работы программы (с использованием компилятора «JIT» или «Just In Time»)
  • совместимость между несколькими языками в одной виртуальной машине; это стало популярным в JVM (например, Scala) и является явной целью .net framework
IMSoP
источник
1
Java также была ориентирована на встраиваемые системы. В таких системах у оборудования было несколько ограничений памяти и процессора.
17
Могут ли разработчики быть разработаны таким образом, чтобы они сначала компилировали исходный код Java в байт-код, а затем компилировали байт-код в машинный код? Устранит ли это большинство недостатков, которые вы упомянули?
Sher10ck
@ Sher10ck Да, AFAIK вполне может написать компилятор, который статически преобразует байт-код JVM в машинные инструкции для конкретной архитектуры. Но это имело бы смысл только в том случае, если бы оно улучшило производительность настолько, чтобы перевесить либо дополнительные усилия для дистрибьютора, либо дополнительное время для первого использования для пользователя. Встраиваемая система с низким энергопотреблением может выиграть; современный ПК, загружающий и запускающий множество различных программ, вероятно, был бы лучше с хорошо настроенным JIT. Я думаю, что Android идет куда-то в этом направлении, но не знаю деталей.
IMSoP
8

Похоже, вы удивляетесь, почему мы не просто распространяем исходный код. Позвольте мне перевернуть этот вопрос: почему бы нам просто не распространять машинный код?

Ясно, что ответ здесь заключается в том, что Java по своей природе не предполагает, что она знает, на какой машине находится ваш код; это может быть настольный компьютер, суперкомпьютер, телефон или что-то среднее между ними и за их пределами. Java оставляет место для локального компилятора JVM, чтобы делать свое дело. В дополнение к увеличению переносимости вашего кода, у этого есть приятное преимущество: он позволяет компилятору делать такие вещи, как использование машинно-зависимых оптимизаций, если они существуют, или по-прежнему генерировать хотя бы работающий код, если их нет. Такие вещи, как инструкции SSE или аппаратное ускорение, могут использоваться только на тех машинах, которые их поддерживают.

С этой точки зрения обоснованность использования байт-кода поверх необработанного исходного кода более ясна. Приближаясь как можно ближе к необработанному машинному языку, мы можем реализовать или частично реализовать некоторые из преимуществ машинного кода, такие как:

  • Более быстрые времена запуска, так как некоторые из компиляции и анализа уже сделаны.
  • Безопасность, поскольку формат байт-кода имеет встроенный механизм для подписи дистрибутивных файлов (источник может сделать это по соглашению, но механизм для достижения этого не встроен, как это делается с байт-кодом).

Обратите внимание, что я не упоминаю более быстрое выполнение. И исходный код, и байт-код могут или могут (теоретически) быть полностью скомпилированы в один и тот же машинный код для фактического выполнения.

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

Джоэл Коухорн
источник
8

Кажется, здесь есть как минимум два разных возможных вопроса. Один действительно о компиляторах вообще, с 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, достаточно сложно выразить, что трудно перевести из некоторых исходных языков в машинный код, который оптимально работает (даже близко) на большинстве типичных машин.

Резюме

Если вы спрашиваете, зачем вообще использовать промежуточные представления, то есть два основных фактора:

  1. Свести задачу O (N * M) к задаче O (N + M) и
  2. Разбейте проблему на более управляемые части.

Если вы спрашиваете об особенностях байт-кодов Java и о том, почему они выбрали именно это представление вместо какого-то другого, то я бы сказал, что ответ в значительной степени возвращается к их первоначальному замыслу и ограничениям сети в то время. , что приводит к следующим приоритетам:

  1. Компактное представление.
  2. Быстро и легко декодировать и выполнять.
  3. Быстро и легко внедряется на большинстве распространенных машин.

Возможность представлять много языков или оптимально выполнять самые разные задачи была гораздо более низкими приоритетами (если они вообще считались приоритетами).


  1. Так почему же P-система в основном забыта? В основном ценовая ситуация. P-система продавалась довольно прилично на Apple II, Commodore SuperPets и т. Д. Когда вышел IBM PC, P-система была поддерживаемой ОС, но MS-DOS стоила дешевле (с точки зрения большинства людей, по сути, была добавлена ​​бесплатно) и быстро стало доступно больше программ, поскольку именно для этого писали Microsoft и IBM (среди прочих).
  2. Например, так работает Сажа .
Джерри Гроб
источник
Совсем близко с веб-апплетами: первоначальная цель заключалась в том, чтобы распространять код на устройства (приставки ...), так же, как RPC распределяет вызовы функций, а CORBA - объекты.
ниндзя
2
Это отличный ответ и хорошее понимание того, как разные промежуточные представления делают разные компромиссы. :)
IMSoP
@ninjalj: Это был действительно Дуб. К тому времени, когда он превратился в Java, я считаю, что идеи телевизионных приставок (и аналогичные) были отложены (хотя я первый, кто признает, что есть справедливый аргумент, что Oak и Java - это одно и то же).
Джерри Коффин
@TobySpeight: Да, выражение здесь, вероятно, лучше подходит. Спасибо.
Джерри Коффин
0

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

Это также облегчает защиту защищенного авторским правом исходного кода.

EJoshuaS - Восстановить Монику
источник
2
Байт-код Java (и .NET) настолько легко превратить в достаточно разборчивый источник, что есть продукты, позволяющие искажать имена и иногда другую информацию, чтобы сделать это сложнее - что-то часто делается с JavaScript, чтобы сделать его меньше, так как мы сейчас возможно установка байт-кода для веб-браузеров.
LnxPrgr3
0

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

Сергей Орлов
источник
0

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

Лишь позже, когда стало очевидно, что производительность интерпретирующих JVM все еще не оправдала себя, люди вложили усилия в создание высокопроизводительных компиляторов точно в срок. Это несколько сократило разрыв с более быстрыми языками, такими как C и C ++. (Однако некоторые проблемы со скоростью, свойственные Java, остаются, поэтому вы, вероятно, никогда не получите среду Java, которая работает так же хорошо, как хорошо написанный код C).

Конечно, имея в своем распоряжении методы своевременной компиляции, мы могли бы вернуться к фактическому распространению исходного кода и его своевременной компиляции в машинный код. Однако это сильно снизит производительность при запуске, пока все соответствующие части кода не будут скомпилированы. Байтовый код по-прежнему очень полезен, потому что анализировать его намного проще, чем эквивалентный Java-код.

cmaster
источник
Пожалуйста, возражайте против объяснения почему ?
Мастер
-5

Текстовый исходный код - это структура, которая должна легко читаться и изменяться человеком.

Байт-код - это структура, которая должна быть легко читаемой и исполняемой машиной.

Поскольку все, что JVM делает с кодом, читается и исполняется, байт-код лучше подходит для использования JVM.

Я заметил, что еще не было примеров. Глупые псевдо-примеры:

//Source code
i += 1 + 5 * 2 + x;

// Byte code
i += 11, i += x
____

//Source code
i = sin(1);

// Byte code
i = 0.8414709848
_____

//Source code
i = sin(x)^2+cos(x)^2;

// Byte code (actually that one isn't true)
i = 1

Конечно, байт-код - это не только оптимизация. Большая часть этого заключается в способности выполнять код, не заботясь о сложных правилах, таких как проверка, содержит ли класс член с именем "foo" где-то ниже в файле, когда метод ссылается на "foo".

Питер
источник
2
Эти «примеры» байт-кода понятны человеку. Это совсем не байт-код. Это вводит в заблуждение и не затрагивает заданный вопрос.
Wildcard
@Wildcard Возможно, вы пропустили этот форум, прочитанный людьми. Вот почему я помещаю контент в удобочитаемую форму. Учитывая, что форум посвящен разработке программного обеспечения, попросить читателей понять концепцию простой абстракции не так уж много.
Питер
Человекочитаемая форма - это исходный код, а не байт-код. Вы иллюстрируете исходный код с предварительно вычисленными выражениями, а не с байтовым кодом. И я не пропустил, что это читаемый человеком форум: вы тот, кто критиковал других ответчиков за то, что они не включали в себя примеры байт-кода, а не меня. Итак, вы говорите: «Я заметил, что примеров еще не было», а затем продолжаем давать не- примеры, которые вообще не иллюстрируют байт-код. И это все еще не решает вопрос вообще. Перечитайте вопрос.
Wildcard