Зміст:

  1. Вступ
  2. Компіляція в байт-код
  3. Приклад компіляції та виконання програми
  4. Виконання програми віртуальною машиною
  5. Just-in-time (JIT) компіляція
  6. Висновок
Компіляція та виконання Java застосунків під капотом - 1

1. Вступ

Усім привіт! Сьогодні я хотів би поділитися знаннями про те, що відбувається під капотом JVM (Java Virtual Machine) після того, як ми запускаємо написаний Java-застосунок. У наш час існують наймодніші середовища розробки, які допомагають не думати про внутрішній устрій JVM, компіляцію та виконання Java-коду, через що нові розробники можуть упустити ці важливі аспекти. Водночас на співбесідах часто ставлять запитання щодо цієї теми, через що я і вирішив написати статтю.

2. Компіляція в байт-код

Компіляція та виконання 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. Компіляція та виконання Java застосунків під капотом - 3З вищесказаного можна зробити висновок про те, що для простих, клієнтських застосунків, вигідніше використовувати компілятор C1, оскільки в цьому разі нам важливо, як швидко стартує застосунок. Серверні, довгоживучі застосунки можуть стартувати протягом більшої кількості часу, проте надалі мають працювати і виконувати свою функцію швидко – тут нам підійде компілятор C2. Під час запуску Java-програми на x32 версії JVM ми вручну можемо вказати, який режим ми хочемо використовувати, за допомогою прапорів -client і -server. У разі зазначення прапора -client JVM не здійснюватиме складних оптимізацій із байт-кодом, що прискорить час старту застосунку і зменшить кількість споживаної пам'яті. У разі зазначення прапора -server застосунок стартуватиме протягом більшої кількості часу через складні оптимізації байт-коду та споживатиме більше пам'яті для зберігання машинного коду, проте надалі працюватиме така програма швидше. У x64 версії JVM прапор -client ігнорується і за замовчуванням використовується серверна конфігурація програми.

6. Висновок

Ось і добіг кінця мій короткий огляд того, як працює компіляція і виконання Java-застосунка. Основні поінти:
  1. Компілятор javac перетворює вихідний код програми на байт-код, який може бути виконаний на будь-якій платформі, на якій встановлена віртуальна машина Java;
  2. Після компіляції JVM інтерпретує отриманий байт-код;
  3. Для прискорення роботи Java-застосунків, JVM використовує механізм Just-In-Time компіляції, який перетворює найчастіше виконувані ділянки програми в машинний код і зберігає їх у пам'яті.
Я сподіваюся, що ця стаття допомогла вам глибше зрозуміти, як влаштована наша улюблена мова програмування. Дякую за прочитання, критика радо приймається!