Зміст:
- Вступ
- Компіляція в байт-код
- Приклад компіляції та виконання програми
- Виконання програми віртуальною машиною
- Just-in-time (JIT) компіляція
- Висновок
1. Вступ
Усім привіт! Сьогодні я хотів би поділитися знаннями про те, що відбувається під капотом JVM (Java Virtual Machine) після того, як ми запускаємо написаний Java-застосунок. У наш час існують наймодніші середовища розробки, які допомагають не думати про внутрішній устрій JVM, компіляцію та виконання Java-коду, через що нові розробники можуть упустити ці важливі аспекти. Водночас на співбесідах часто ставлять запитання щодо цієї теми, через що я і вирішив написати статтю.
2. Компіляція в байт-код
Почнемо з теорії. Коли ми пишемо якийсь застосунок, ми створюємо файл із розширенням
.java і поміщаємо в нього код мовою програмування Java. Такий файл, що містить код, зрозумілий людині, називається
файлом із вихідним кодом. Після того, як файл з вихідним кодом готовий, потрібно його виконати! Але на стадії в ньому міститься інформація, зрозуміла тільки людині.
Java – мультиплатформна мова програмування. Це означає, що програми, написані мовою Java, можна виконувати на будь-якій платформі, де встановлена спеціальна система для виконання Java. Така система називається Java Virtual Machine (JVM). Для того, щоб перевести програму з вихідного коду в код, зрозумілий JVM, потрібно її скомпілювати. Код, зрозумілий JVM, називається байт-кодом і містить набір інструкцій, які надалі виконуватиме віртуальна машина.
Для компіляції вихідного коду в байт-код існує компілятор
javac, що міститься в комплекті постачання JDK (Java Development Kit). На вхід компілятор приймає файл із розширенням
.java, що містить вихідний код програми, а на виході видає файл із розширенням
.class, що містить байт-код, необхідний для виконання програми віртуальною машиною.
Після того, як програма була скомпільована в байт-код, її можна виконати за допомогою віртуальної машини.
3. Приклад компіляції та виконання програми
Припустимо, що ми маємо просту програму, яка міститься у файлі
Calculator.java, яка приймає 2 чисельні аргументи командного рядка і друкує результат їхнього додавання:
class Calculator {
public static void main(String[] args){
int a = Integer.valueOf(args[0]);
int b = Integer.valueOf(args[1]);
System.out.println(a + b);
}
}
Для того, щоб скомпілювати цю програму в байт-код, скористаємося компілятором
javac у командному рядку:
javac Calculator.java
Після компіляції на виході ми отримуємо файл із байт-кодом
Calculator.class, який ми можемо виконати за допомогою встановленої на нашому комп'ютері java-машини командою java в командному рядку:
java Calculator 1 2
Зауважимо, що після назви файлу було вказано 2 аргументи командного рядка – числа 1 і 2. Після виконання програми, у командному рядку буде виведено число 3.
У прикладі вище у нас був простий клас, який живе сам собою. Але що, якщо клас знаходиться в якомусь пакеті? Змоделюємо таку ситуацію: створимо директорії
src/ru/javarush і помістимо туди наш клас. Тепер він має такий вигляд (додали ім'я пакета на початку файлу):
package ru.javarush;
class Calculator {
public static void main(String[] args){
int a = Integer.valueOf(args[0]);
int b = Integer.valueOf(args[1]);
System.out.println(a + b);
}
}
Скомпілюємо такий клас такою командою:
javac -d bin src/ru/javarush/Calculator.java
У цьому прикладі ми використовували додаткову опцію компілятора
-d bin, яка складає скомпільовані файли в директорію
bin зі структурою, аналогічною директорії
src, водночас директорія
bin має бути створена заздалегідь. Такий прийом використовується, щоб не плутати файли з вихідним кодом із файлами з байт-кодом.
Перед запуском скомпільованої програми варто пояснити поняття
classpath.
Classpath – це шлях, щодо якого віртуальна машина шукатиме пакети та скомпільовані класи. Тобто, таким чином ми кажемо віртуальній машині, які директорії у файловій системі є кореневими для ієрархії пакета Java.
Classpath можна вказати під час запуску програми за допомогою прапора
-classpath. Запуск програми здійснюємо за допомогою команди:
java -classpath ./bin ru.javarush.Calculator 1 2
У цьому прикладі нам потрібно було вказати повне ім'я класу, зокрема з ім'ям пакета, в якому він знаходиться. Фінальне дерево файлів має такий вигляд:
├── src
│ └── ru
│ └── javarush
│ └── Calculator.java
└── bin
└── ru
└── javarush
└── Calculator.class
4. Виконання програми віртуальною машиною
Отже, ми запустили написану програму. Але що ж відбувається в момент запуску скомпільованої програми віртуальною машиною? Для початку розберемося, що означають поняття компіляції та інтерпретації коду.
Компіляція – трансляція програми, складеної вихідною мовою високого рівня, в еквівалентну програму низькорівневою мовою, близькою до машинного коду.
Інтерпретація – пооператорний (покомандний, рядковий) аналіз, оброблення і одразу ж виконання вихідної програми або запиту (на відміну від компіляції, під час якої програма транслюється без її виконання).
Мова Java має як компілятор (
javac), так і інтерпретатор, у ролі якого виступає віртуальна машина, що порядково перетворює байт-код на машинний код і одразу ж його виконує. Таким чином, коли ми запускаємо скомпільовану програму, віртуальна машина починає її інтерпретацію, тобто рядкове перетворення байт-коду в машинний код, а також його виконання.
На жаль, чиста інтерпретація байт-коду є досить довгим процесом і робить мову java повільною як порівняти з її конкурентами. Щоб уникнути цього, був впроваджений механізм, що дає змогу прискорити інтерпретацію байт-коду віртуальною машиною. Цей механізм називається Just-in-time компіляцією (JITC).
5. Just-in-time (JIT) компіляція
Простими словами, механізм Just-In-Time компіляції полягає в такому: якщо в програмі присутні частини коду, які виконуються багато разів, то їх можна скомпілювати один раз у машинний код, щоб у майбутньому прискорити їхнє виконання. Після компіляції такої частини програми в машинний код, під час кожного наступного виклику цієї частини програми віртуальна машина одразу виконуватиме скомпільований машинний код, а не інтерпретуватиме його, що, звісно, прискорить виконання програми.
Прискорення роботи програми досягається через збільшення споживання пам'яті (десь же нам потрібно зберігати скомпільований машинний код!) і через збільшення витрат часу на компіляцію під час виконання програми.
JIT компіляція – доволі складний механізм, тому ознайомимося з основами. Загалом існує 4 рівні JIT компіляції байт-коду в машинний код. Що вищий рівень компіляції, то він складніший, але й водночас виконання такої ділянки буде швидшим, ніж ділянки з меншим рівнем. JIT – компілятор самостійно вирішує, який рівень компіляції задати для кожного фрагмента програми, ґрунтуючись на тому, як часто виконується цей фрагмент. Під капотом JVM використовує 2 JIT-компілятори – C1 і C2. C1 компілятор так само називається клієнтським компілятором і здатний скомпілювати код тільки до 3-го рівня. За 4-ий, найскладніший і найшвидший рівень компіляції відповідає компілятор C2.
З вищесказаного можна зробити висновок про те, що для простих, клієнтських застосунків, вигідніше використовувати компілятор C1, оскільки в цьому разі нам важливо, як швидко стартує застосунок. Серверні, довгоживучі застосунки можуть стартувати протягом більшої кількості часу, проте надалі мають працювати і виконувати свою функцію швидко – тут нам підійде компілятор C2.
Під час запуску Java-програми на x32 версії JVM ми вручну можемо вказати, який режим ми хочемо використовувати, за допомогою прапорів
-client і
-server. У разі зазначення прапора
-client JVM не здійснюватиме складних оптимізацій із байт-кодом, що прискорить час старту застосунку і зменшить кількість споживаної пам'яті. У разі зазначення прапора
-server застосунок стартуватиме протягом більшої кількості часу через складні оптимізації байт-коду та споживатиме більше пам'яті для зберігання машинного коду, проте надалі працюватиме така програма швидше. У x64 версії JVM прапор
-client ігнорується і за замовчуванням використовується серверна конфігурація програми.
6. Висновок
Ось і добіг кінця мій короткий огляд того, як працює компіляція і виконання Java-застосунка.
Основні поінти:
- Компілятор javac перетворює вихідний код програми на байт-код, який може бути виконаний на будь-якій платформі, на якій встановлена віртуальна машина Java;
- Після компіляції JVM інтерпретує отриманий байт-код;
- Для прискорення роботи Java-застосунків, JVM використовує механізм Just-In-Time компіляції, який перетворює найчастіше виконувані ділянки програми в машинний код і зберігає їх у пам'яті.
Я сподіваюся, що ця стаття допомогла вам глибше зрозуміти, як влаштована наша улюблена мова програмування. Дякую за прочитання, критика радо приймається!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ