Тут, мабуть, є щонайменше два різних можливих питання. Справді, це взагалі про компілятори, а Java - це лише приклад жанру. Інший більш специфічний для Java специфічних байтових кодів, які він використовує.
Компілятори взагалі
Розглянемо спочатку загальне питання: чому компілятор використовує якесь проміжне подання у процесі компіляції вихідного коду для запуску якогось конкретного процесора?
Зменшення складності
Одна відповідь на це досить проста: вона перетворює задачу O (N * M) в проблему O (N + M).
Якщо нам надано N мовних джерел і M цілей, і кожен компілятор повністю незалежний, тоді нам потрібні N * M компілятори для перекладу всіх цих мов на всі ці цілі (де "target" - це щось на зразок комбінації процесор і ОС).
Якщо, однак, усі ці компілятори домовляються про загальне проміжне представлення, то ми можемо мати N передніх кінців компілятора, які переводять вихідні мови в проміжне представлення, і M задніх кінців компілятора, які переводять проміжне подання на щось, що підходить для конкретної цілі.
Сегментація проблеми
Що ще краще, вона розділяє проблему на дві більш-менш ексклюзивні сфери. Люди, які знають / піклуються про мовний дизайн, розбір і подібні речі, можуть зосередитись на передній частині компілятора, тоді як люди, які знають про набори інструкцій, дизайн процесора та подібні речі, можуть концентруватися на задньому.
Так, наприклад, враховуючи щось на зразок LLVM, у нас є багато передніх кінців для різних мов. У нас також є резервні пристрої для безлічі різних процесорів. Мовний хлопець може написати новий фронт для своєї мови та швидко підтримати безліч цілей. Хлопець процесора може написати новий бек-енд для своєї цілі, не займаючись дизайном мови, розбором тощо.
Поділ компіляторів на передній і задній кінці з проміжним представленням для зв'язку між ними не є оригінальним для 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, досить важко виразити, що важко перевести з деяких мов джерела в машинний код, який працює (навіть близько до) оптимально на більшості типових машин.
Підсумок
Якщо ви запитуєте про те, навіщо взагалі використовувати проміжні представлення, два основні фактори:
- Зменшіть задачу O (N * M) до проблеми O (N + M) і
- Розбийте проблему на більш керовані частини.
Якщо ви запитуєте про специфіку байт-кодів Java і чому вони обрали саме це представлення замість якогось іншого, то я б сказав, що відповідь значною мірою повертається до їх початкового наміру та обмежень в Інтернеті на той час. , що веде до наступних пріоритетів:
- Компактне представлення.
- Швидке та просте розшифрування та виконання.
- Швидкий і простий в застосуванні на найбільш поширених машинах
Можливість представляти багато мов або оптимально виконувати їх на різноманітних цілях були значно нижчими пріоритетами (якщо вони взагалі вважалися пріоритетами).
- То чому P-система здебільшого забута? В основному ситуація з ціноутворенням. P-система продавалася досить пристойно на Apple II, Commodore SuperPets і т. Д. Коли вийшов IBM PC, P-система була підтримуваною ОС, але MS-DOS коштував дешевше (з точки зору більшості людей, по суті, кидався безкоштовно) і швидко було доступно більше програм, оскільки саме про це писали Microsoft та IBM (серед інших).
- Наприклад, саме так працює Сажа .