TSM - Compilarea AOT în Java

Alin Bobeica - Java Software Engineer @ Accesa

Modul în care construim aplicații software a evoluat din 1995, când a fost introdus pentru prima dată Java, ca limbaj de programare. Am progresat de la utilizarea monoliților, la microservicii și, în ultimii ani, la serverless. Cu toate acestea, aceste schimbări aduc, de asemenea, noi provocări, cum ar fi timpul de pornire rapid și instant peak performance, esențiale pentru dezvoltarea aplicațiilor software. Pentru a aborda aceste provocări, Java a introdus Native Images.

Ce sunt Native Images?

Native Images desemnează o tehnologie care utilizează compilarea ahead-of-time pentru a transforma codul Java în executabile autonome numite native images. Acest lucru duce la timpi de pornire mai mici, o utilizare mai eficientă a resurselor și o securitate îmbunătățită.

Compilarea ahead-of-time (AOT)

În aplicațiile Java tradiționale, metoda de compilare utilizată este just-in-time (JIT). JVM este responsabil de executarea bytecode-ului Java și de compilarea codului utilizat frecvent în native code pe baza informațiilor rezultate în urma profilingului din timpul execuției. Avantajul acestui tip de compilare este că permite ca JVM-ul să livreze cod optimizat pentru îmbunătățirea performanțelor de vârf și pentru așa-numita aplicație platform-independent. Dar aceste avantaje vin și cu anumite dezavantaje: JIT folosește mult JVM în timpul execuției, ceea ce duce la un timp de pornire mai mare și la introducerea unui warm-up time. În plus, JIT necesită un JVM pentru a executa codul, ceea ce implică o utilizare crescută a resurselor și la o amprentă de memorie mai mare. AOT urmărește să îmbunătățească aceste aspecte.

Ahead-of-time compilation se referă la tehnica de traducere a unui limbaj high-level, în cazul nostru Java, într-un limbaj low-level, înainte ca aplicația să fie executată, de regulă, la build time. Rezultatul AOT va fi un binar, specific platformei utilizate pentru compilare, care nu necesită compilare ulterioară. 

Compilarea AOT este utilizată în aplicațiile Java pentru a îmbunătăți performanța generală, dar, mai ales, pentru a reduce timpul de pornire. Cel mai cunoscut compilator AOT în Java este GraalVM.

GraalVM

GraalVM a început ca un proiect de cercetare la Oracle, în cadrul ramurii de cercetare și dezvoltare a companiei, Oracle Labs. Oracle Labs este responsabil de cercetarea limbajelor de programare, a mașinilor virtuale, învățarea automată, securitate, procesare grafică și alte domenii. 

Principalul produs al GraalVM este compilatorul Graal, un compilator cu optimizare ridicată, creat de la zero. În mod surprinzător, acest compilator este scris în cea mai mare parte în Java și, în multe scenarii, datorită numeroaselor optimizări, este mai performant decât compilatorul C2. Acest lucru demonstrează încă o dată cât de puternic este Java ca limbaj de programare.

Cum funcționează?

GraalVM primește ca input bytecode-ul Java și generează un executabil. Acest executabil este specific platformei utilizate pentru compilare. Cum face GraalVM acest lucru? Ei bine, realizează ceva numit analiză statică, în care compilatorul analizează codul folosit în aplicație și include doar acel cod în rezultatul nativ. 

Pentru a face acest lucru, compilatorul utilizează aceste trei tehnici:

Aceste trei etape se repetă până când se ajunge la un punct fix, unde toate clasele și metodele necesare rulării aplicației au fost incluse.

Pentru a suporta funcționalități cum ar fi gestionarea memoriei și programarea firelor de execuție, Native Images includ un lightweight VM numit SubstrateVM. Acest VM special este, de asemenea, scris în Java.

Fig. 1 Proces de compilare GraalVM

Care sunt beneficiile imaginilor native?

Native images au mai multe avantaje. Câteva dintre principalele avantaje ale utilizării lor sunt:

Exemplu de native image cu Spring Native

În cele ce urmează, o să analizăm un exemplu simplu ce folosește Spring Native și Maven pentru construirea de native images. Pentru aceasta, voi folosi ediția Community GraalVM, care se bazează pe OpenJDK. Versiunea Java utilizată pentru acest exemplu este 21. GraalVM vine, de asemenea, cu un plugin Maven ce facilitează crearea și testarea executabilelor native. Spring Native este un proiect din cadrul Spring Framework care oferă suport pentru crearea aplicațiilor Spring ca imagini native.

Fig. 2 JIT vs. AOT startup

Am scris o aplicație simplă pentru gestionarea produselor, care expune 4 endpoint-uri pentru crearea, ștergerea și listarea produselor. Vom crea două versiuni ale acestei aplicații, una compilată JIT, care rulează folosind JVM și cealaltă va fi compilată AOT, ca imagine nativă, apoi voi compara performanța acestor două aplicații.

În primul rând, trebuie să adăugăm pluginul graalVM în pom.xml - acest lucru ne va oferi capacitatea de a crea imagini native folosind Maven.

<build>
  <plugins>
    <plugin>
      <groupId>org.graalvm.buildtools</groupId>
      <artifactId>native-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

Acum, să compilăm aplicația folosind comanda maven:

./mvnw -Pnative native:compile

După ce compilarea este finalizată, putem observa imaginea nativă sub forma unui executabil.

Timp de pornire: Deoarece codul Java nu trebuie interpretat și compilat, imaginea nativă pornește imediat în comparație cu aplicația JVM. De asemenea, datorită heapului prepopulat și compilării directe în cod mașină, aplicația funcționează la performanțe maxime.

În imaginile de mai jos, putem vedea logurile din timpul pornirii aplicațiilor Spring, timpul de pornire al aplicației compilată ca imagine nativă a avut timpul de pornire de doar 143 ms, în timp ce timpul de pornire al aplicației ce rulează folosind JVM este de 2,834 secunde.

Eficientizarea resurselor. Deoarece codul java este compilat în avans, executabilul nativ nu are nevoie de JVM, de infrastructura de compilare JIT a acestuia sau de memorie pentru codul compilat, ci are nevoie de memorie doar pentru executabil și pentru datele aplicației.

Fig. 3. 1 Native Image startup log

Fig. 3.2 JVM application startup log

Graficele reflectă consumul de CPU și al memoriei celor două aplicații. Linia albastră reprezintă utilizarea memoriei, aproximativ 390 MB pentru aplicația JVM față de 120 MB pentru imaginea nativă. Linia roșie reprezintă activitatea procesorului. Putem observa că aplicația JVM consumă mult mai mult CPU în timpul perioadei de warm-up, din cauza tuturor operațiunilor JIT care trebuie efectuate; pe de altă parte, imaginea nativă utilizează mult mai puțin CPU, toate operațiunile ce necesită putere de procesare intensivă efectuându-se în timpul compilării.

Dezavantaje ale imaginilor native

Probabil că acum putem spune că este prea frumos pentru a fi adevărat, dar imaginile native vin și cu unele dezavantaje și lucruri de care trebuie să ținem cont atunci când lucrăm cu acestea în Java. 

Iată câteva dezavantaje ale imaginilor native:

Fig. 4 Native Image vs. JVM, consum cpu și memorie

Concluzie

În concluzie, introducerea imaginilor native în Java marchează un progres semnificativ în abordarea provocărilor aplicațiilor moderne, cum ar fi timpii de pornire rapizi și utilizarea eficientă a resurselor. Prin utilizarea compilării ahead-of-time și a tehnologiilor precum GraalVM, dezvoltatorii Java pot produce acum executabile independente cu performanțe ridicate și o amprentă de memorie redusă.

Beneficiile imaginilor native, inclusiv timpul de pornire instant, utilizarea optimizată a resurselor și vulnerabilitatea redusă fac din acestea o opțiune pentru aplicațiile cu durată de viață scurtă și pentru cele care necesită măsuri de securitate mai bune. Cu toate acestea, este esențial să recunoaștem dezavantajele asociate cu imaginile native, cum ar fi creșterea build time-ului, suportul limitat pentru reflection și problemele de compatibilitate cu anumite funcționalități și librării Java. Abordarea acestor provocări poate necesita un efort suplimentar și o analiză atentă în timpul dezvoltării.

În timp ce imaginile native oferă avantaje promițătoare, dezvoltatorii ar trebui să evalueze adecvarea lor pentru proiecte specifice pe baza cerințelor de performanță, a considerentelor de compatibilitate și a constrângerilor de resurse. Cu o înțelegere adecvată a provocărilor potențiale, imaginile native pot fi un instrument valoros în dezvoltarea aplicațiilor Java moderne, permițând o performanță și o scalabilitate îmbunătățite în diverse medii de implementare.

Referințe

  1. Alina Yurenko, Revolutionizing Java with GraalVM Native Image

  2. Points to Analysis

  3. Class Initialization in Native Image