GraalVM

Furkan Şahin Kulaksız
11 min readDec 30, 2023

--

Görseldeki QR kodu kullanarak iletişim kanallarıma ulaşabilirsiniz.

GraalVM’in iddiası şu: Uygulamaları 100 kata kadar varan bir hızda çalıştırmak.

GraalVM’in resmi dokümantasyonunu ziyaret ettiğimizde karşımıza aşağıdaki paragraflar çıkar:

GraalVM; Java uygulamalarını önceden, ısınma olmadan en yüksek performans sağlayan ve daha az kaynak kullanan bağımsız binary dosyalar halinde derler. Dosyaları binary olarak derlemesinin en büyük avantajı, uygulama 100 kata kadar daha hızlı başlayabilir.
Kullanılmayan sınıfları, methodları ve fieldları uygulama binary dosyalarından hariç tutar. Runtime’da bilinmeyen herhangi bir kod yüklenmez. Reflection ve diğer dinamik özellikleri sadece build time’da sınırlar.

GraalVM’e başlamadan önce bilmem gerekenler

JIT(Just-in-Time)Java tabanlı uygulamaların çalışma zamanında performans optimizasyonundan sorumlu olan JRE’nin (Java Çalışma Ortamı) önemli bir parçasıdır. Derleyici, hem son kullanıcı hem de uygulama geliştirici için bir uygulamanın performansını belirlemede anahtar unsurlardan biridir. Bytecode, Java’nın platformlar arası yürütme konusunda yardımcı olan en önemli özelliklerinden biridir. Bytecode’un makine diline dönüştürülme şekli, onun hızını büyük ölçüde etkiler. Bu bytecod’lar, talimat seti mimarisine bağlı olarak yorumlanmalı veya uygun makine talimatlarına derlenmelidir. Dahası, eğer talimat mimarisi bytecode tabanlıysa, bunlar doğrudan yürütülebilir. Bytecode’un yorumlanması, çalışma hızını etkiler. Performansı artırmak için, JIT compiler’lar runtime’da Java Virtual Machine (JVM) ile etkileşimde bulunur ve uygun bytecode dizilerini makine koduna derler. JIT compiler kullanılırken, donanım aynı bytecode dizisini tekrar tekrar yorumlamak ve çeviri süreci için fazladan yük getirmek yerine yerel kodu yürütebilir. Bu, derlenen yöntemler daha az sıklıkla yürütülmedikçe, yürütme hızında performans kazanımlarına yol açar.

JIT compiler, bytecode serisini makine diline derlerken bazı basit optimizasyonlar yapabilir. JIT compiler’lar tarafından yapılan bu optimizasyonlardan optimizasyon derecesi ne kadar yüksek olursa, JIT compiler yürütme aşamasında o kadar fazla zaman harcar. Bu nedenle, execution süresine eklenen fazladan yük nedeniyle ve programın sınırlı bir görünümüne sahip olduğu için, statik bir compiler’ın yapabileceği tüm optimizasyonları yapamaz.

AOT(Ahead-of-Time)`AOT compiler, Java programlarının ve özellikle JVM’nin başlangıç süresinin performansını artırmanın bir yoludur. JVM, Java bytecode’ını yürütür ve sıkça yürütülen kodu yerel koda derler. JVM, execution sırasında toplanan profil bilgilerine dayanarak hangi kodun JIT derlemesi yapılacağına karar verir.

Bu teknik, JVM’nin son derece optimize edilmiş kod üretmesini sağlarken ve zirve performansını artırırken, execution edilen kod henüz JIT derlenmediğinden başlangıç süresi muhtemelen optimal değildir. AOT, bunu iyileştirmeyi hedefler. AOT için kullanılan derleyici Graal’dır.

GraalVM’i bilgisayarıma nasıl yüklerim.?

Öncelikle ben macbook kullandığım için, buradaki kurulumları macbook’a göre yazıyorum.

GraalVM’in JDK 17 ve JDK 21 için desteği mevcut. Buraya tıklayarak GraalVM’in download sayfasına yönlenebilirsiniz.

Aslında macbook için download dokümantasyonunda gerekli adımlar mevcut fakat kısaca bahsetmek gerekirse;

sdk install java 21.0.1-graal
komutu ile terminal yardımıyla GraalVM kurulumunu sağlayabilirsiniz.
Eğer sdkman yüklü değilse de buradan sdkmani yükleyebilirsiniz.

Tabi yukarıdaki komut GraalVM’in java 21 için gerekli olan sürümü.

NOT: Yukarıdaki adımı yaptığınızda GraalVM default olarak setlenecektir. Ama bu herhangi bir problem teşkil etmez. Yani siz, GraalVM olmadan bir uygulama geliştirmek istediğinizde; kullandığınız IDE üzerinden normal spring boot & java sürümlerinizi ve bağımlılıklarınızı seçtiğinizde, run ettiğiniz zaman yine JVM üzerinde çalışacaktır. Çünkü GraalVM ile çalışmak için native image oluşturmak lazım.

Hello World Uygulaması

Intellij IDE üzerinden aşağıdaki seçeneği seçip ilerleyelim.

Ve artık tek yapmamız gereken herhangi bir kod yazmak.

File structure’ı ile birlikte yazdığım kod dünyanın en basit java kodu. Bunu GraalVM üzerinde çalıştırmak için aşağıdaki adımları izlemek lazım.

  1. Bulunduğumuz dizinde, idenin terminal ekranında HelloWorld.java dosyasının olduğu yere gelmemiz lazım.
    cd java/org/fsk
  2. Java file’ımızın bulunduğu dizine eriştikten sonra javac komutu yardımıyla .class uzantılı bir dosya elde etmemiz lazım.
    javac HelloWorld.java
  3. Bu adımdan sonra proje file structure’ımıza .class uzantılı bir file gelecektir.

4. Bu adımdan sonra projenin kök dizinine çıkıp native-image komutunu çalıştırmamız lazım. Ama burada çok önemli bir durum var. Bu komutu projemizin kök dizininde çalıştırmamız lazım.

5. native-image komutundan sonra aslında bir native image oluşturduk ve org.fsk.helloworld adında bir dosya geldi karşımıza. Ama burada şöyle bir durum var. Ben bu kodu mac’te yazdım. mac’te çalıştırılabilir bir dosya haline getirdi. Windows’ta .exe dosya oluşturması kuvvetle muhtemel.

./ komutu ile kök dizindeyken elde ettiğimiz dosyayı çalıştırırsak dilediğimiz sonuca varmış oluruz.

Bir Spring Boot uygulamasını GraalVM üzerinde nasıl çalıştırırım.?

GraalVM ile bir uygulamanın nasıl çalışacağına geçmeden önce çok basit bir uygulama yazacağım.

Bu uygulamadaki amacım, JVM üzerinde çalışan bir uygulama ile GraalVM üzerinde çalışan bir uygulamayı performans olarak karşılaştırmak. (Tabi burada dikkat edilmesi gereken nokta, daha sağlıklı sonuç alabilmek için her iki virtual machine üzerinde koşan uygulamanın aynı olmasına dikkat etmek gerekebilir.)

Tam bu noktada bir reklam arası verip projede kullanmak üzere Mockaroo dan bahsetmek istiyorum. Mockaroo aslında en temel haliyle bir fake data oluşturma sitesi. Tabi API ile ilgili işlemler de yapılabiliyor ama ben fake bir data generate etmek için kullandım.

Mockaroo ile buradan bir json data oluşturup dilediğiniz gibi kullanabilirsiniz. Ücretsiz sürümü ile 1000 tane fake data oluşturabilirsiniz.

1.ADIM:

2 farklı proje oluşturdum ve JVM üzerinde çalışacak projem için bir tane maven bağımlılık ekledim.

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

Bu bağımlılık bizim için mockaroo’dan oluşturduğumuz JSON datayı okuyacak.

Yukarıdaki görseldeki gibi 1000 tane data içeren bir JSON dosyası oluşturdum.

2.ADIM

Öncelikle bir tane controller class’ı oluşturup içerisine loadData() diye bir method yazdım. Bu method JSON data’yı okuyacak ve MongoDB’ye kaydedecek bir method yazdım.

Önemli Not: oluşturduğumuz JSON dosyası resources klasörünün direkt altında olmalı. Eğer başka bir konumda olursa bunu ClassPathResource’da belirtmeniz gerekir.

public void loadJsonData() throws Exception {

ObjectMapper mapper = new ObjectMapper();

for (int i = 0; i < 1000; i++) {
File file = new ClassPathResource("demo.json").getFile();
List<Person> persons = mapper.readValue(file, new TypeReference<>() {});
personRepository.saveAll(persons);
}

}

Bu method, eklediğimiz bağımlılıkla birlikte demo.json dosyamızı okuyacak ve MongoDB’ye kaydedecek. Ama ben bir döngü yazdım ve dolayısıyla üzerinde çalışacağım data sayısı 1000 * 1000 tane olacak.

3.ADIM

Bir tane document modeli yazmam lazım. Bu benim için, hem 2. adımdaki JSON dosyasını okumamı sağlayacak hem de yine 2. adımdaki mongoDB’ye kaydedebilmesi için MongoDB dokümanıma karşılık gelecek.

@Document
@Data
public class Person {

@Id
private String id;
private String firstName;
private String lastName;
private String email;
private String ipAddress;
private String city;
private int age;
private String country;
private String language;
private String university;
private String jobTitle;
private String companyName;
private String phone;
private String iban;

}

4.ADIM

Repository katmanını da oluşturmak gerekir.

public interface PersonRepository extends MongoRepository<Person, String> {
}

5.ADIM

controller katmanına bir method yazacağım ve bu method tetiklendiğinde (bu methoda bir istek geldiğinde) JSON datayı mongoDB’ye kaydedecek.

@RequestMapping("/person")
@RestController
@RequiredArgsConstructor
public class PersonController {

private final PersonService personService;

@GetMapping("/loadData")
public void saveFakeDataToMongoDb() throws Exception {
personService.loadJsonData();
}

}

Çok basit şekilde Get isteğimizi yaptık.

6.ADIM

Şimdi sırada application.yml dosyamızın konfigürasyonlarını yapmamız lazım.

#SERVER-CFG
server:
port: 8282

#MONGO-CFG
spring:
data:
mongodb:
host: localhost
port: 1905
database: graalvm-demo-mongodb

logging:
level:
org:
springframework:
data:
mongodb:
core:
MongoTemplate: DEBUG

docker üzerinde bir mongoDB instance’ı ayağa kaldırdıktan sonra (bunun nasıl yapıldığını detaylı bir şekilde anlamak için buradaki makaleyi okuyabilirsiniz.) bu yml ile aslında uygulama başlar bir hale gelebilir.

7.ADIM

Uygulamayı çalıştırıp başlattıktan sonra 8282 portuna bir istek atıldığında tüm JSON datasının mongoDB’ye yüklendiği görülebilir.

8.ADIM

Şimdi uygulamanın performans karşılaştırmasını yapmak üzere bir tane method tanımı yapacağım. Bu method parametre olarak aldığı bir String ifadeye göre tüm DB’de arama yapacak.

Controller katmanı için;

@GetMapping("/serchByParamFromFirstName/{param}")
public List<Person> searchByParamFromFirstName(@PathVariable("param") String param) {
return personService.findAllPersonSearchNameContains(param);
}

Service katmanı için;

public List<Person> findAllPersonSearchNameContains(String param) {

List<Person> personList = personRepository
.findAll();

return personList
.stream()
.filter(item -> item.getFirstName().contains(param))
.toList();

}

Yani aslında günün sonunda bizim controller katmanımız aşağıdaki gibi oldu.

package org.fsk.graalvmdemo.controller;

import lombok.RequiredArgsConstructor;
import org.fsk.graalvmdemo.models.Person;
import org.fsk.graalvmdemo.service.PersonService;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequestMapping("/person")
@RestController
@RequiredArgsConstructor
public class PersonController {

private final PersonService personService;

@GetMapping("/loadData")
public void saveFakeDataToMongoDb() throws Exception {
personService.loadJsonData();
}

@GetMapping("/serchByParamFromFirstName/{param}")
public List<Person> searchByParamFromFirstName(@PathVariable("param") String param) {
return personService.findAllPersonSearchNameContains(param);
}
}

Service katmanı ise aşağıdaki gibi oldu.

package org.fsk.graalvmdemo.service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.fsk.graalvmdemo.models.Person;
import org.fsk.graalvmdemo.repository.PersonRepository;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import java.io.File;
import java.util.List;

@Service
@RequiredArgsConstructor
public class PersonService {


private final PersonRepository personRepository;


public void loadJsonData() throws Exception {
ObjectMapper mapper = new ObjectMapper();
for (int i = 0; i < 1000; i++) {
File file = new ClassPathResource("demo.json").getFile();
List<Person> persons = mapper.readValue(file, new TypeReference<>() {});
personRepository.saveAll(persons);
}
}

public List<Person> findAllPersonSearchNameContains(String param) {

List<Person> personList = personRepository
.findAll();

return personList
.stream()
.filter(item -> item.getFirstName().contains(param))
.toList();

}
}

9.ADIM

Performans ölçümleri yapabilmek için bir referans noktasına ihtiyacımız var. GraalVM üzerinde çalıştıracağımız uygulama native-image ile docker üzerinde çalışacağı için aslında buna Visual VM ile istek atamayabiliriz. Bunun için bir aspect yazıp uygulamada method çalıştıktan sonraki ölçümlerine bakabilliriz.

  • Asıl paketimizin altına bir tane cfg paketi açıp içerisine bir tane annotation tanımı yazdım
package org.fsk.graalvmdemo.cfg;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MeasureTime {
}
  • Bu tanımdan sonra ise bir aspect yazdım.
package org.fsk.graalvmdemo.cfg;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;


@Aspect
@Component
public class MeasureTimeAdvice {

private static Logger logger = LoggerFactory.getLogger(MeasureTimeAdvice.class);

@Around("@annotation(org.fsk.graalvmdemo.cfg.MeasureTime)")
public Object measureTime(ProceedingJoinPoint point) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object object = point.proceed();
stopWatch.stop();
logger.info("Time take by " + point.getSignature().getName() + "() method is "
+ stopWatch.getTotalTimeMillis() + " ms");
return object;
}
}

Yukarıdaki aspect aslında şu işe yarayacak. @MeasureTime annotationu ile imlenmiş bir methodun çalışma süresini hesaplayacak.

Çok Önemli Not: Asla burada method içerisine System.currentMills() ile başlangıç ve bitiş değerleri verip, bu değerleri birbirinden çıkararak methodun çalışma süresini hesaplamaya çalışmayın çünkü doğru sonuç vermeyebilir.

  • Son adım olarak ise bu aspecti methodumuza eklememiz lazım.
@MeasureTime
public List<Person> findAllPersonSearchNameContains(String param) {

List<Person> personList = personRepository
.findAll();

return personList
.stream()
.filter(item -> item.getFirstName().contains(param))
.toList();

}

10. ADIM

Şimdi aynı uygulamayı GraalVM için yapalım. Bunun için intellij üzerinden Spring Initializr ile başlatırken aşağıdaki gibi uygulamayı başlatabiliriz. Sonrası zaten normal bağımlılık ekleme kısmı.

ÇOK ÖNEMLİ NOT: Tam olarak bu notu yazdığım tarihte (26.12.2023 Saat Gece 01.34) çalıştığım firmada GraalVM kullanıyoruz. GraalVM ile bir proje yapmış olsanız bile ek ayarlar yapmadığınız sürece normal run butonuna bastığınız zaman projeniz JVM üzerinde çalışır. Yani GraalVM üzerinde çalışacak bir projeyi bu resimdeki gibi en baştan bu şekilde yapıp sonra kodumuzu yazabilirdik. Ben daha rahat anlaşılması açısından bu şekilde yaptım.

ÇOK ÖNEMLİ NOT 2: Bir GraalVM uygulamasında kodu sunuculara direkt olarak atmadan önce (sunucuya kodu atma işlemi herhangi bir şekilde olabilir.) yazdığınız kodun native-image’ını almalısınız. Yazının ilerleyen kısmında bundan bahsediyorum.

11.ADIM

Önceki projedeki kodları direkt olarak kopyala yapıştır ile projeyi tekrar yazmaya gerek kalmadan oluşturabiliriz. Fakat copy-paste işleminden sonra bir kaç değişiklik yapmamız gerekecek.

  • Öncelikle ilk projedeki loadData methodunu kaldırabiliriz çünkü bizim projemiz data save işlemi yapmayacak. Sadece data okuması işlemi yapacak. (opsiyonel bir seçenek. Eğer verileri kaydetme hızını da test etmek isterseniz, burası durabilir.)
  • Application.yml dosyasını kendi projem için aşağıdaki gibi değiştirdim.
    management kısmını tracing için ekledim. host kısmı ise benim ip adresim. Siz de kendi ip adresinizi ekleyebilirsiniz. (Ben localhost ile de denedim, GraalVM üzerinde localhost value’si ile çalıuşmıyor.)
#SERVER-CFG
server:
port: 8282

#MONGO-CFG
spring:
data:
mongodb:
host: 192.16*.*.**
port: 1905
database: graalvm-demo-mongodb

logging:
level:
org:
springframework:
data:
mongodb:
core:
MongoTemplate: DEBUG


management:
tracing:
sampling:
probability: 1.0
endpoints:
web:
exposure:
include: "*"
info:
env:
enabled: true
  • build.gradle dosyam ise aşağıdaki gibi.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
id 'org.graalvm.buildtools.native' version '0.9.28'
}

group = 'org.fsk'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '21'
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '3.2.1'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
useJUnitPlatform()
}

bootBuildImage {
builder = 'paketobuildpacks/builder-jammy-full'
environment = [
"JAVA_TOOL_OPTIONS":"-Xmx16384M",
'BP_NATIVE_IMAGE': 'true'
]
}

12.ADIM

Her şey hazır olduğuna göre artık native-image almaya başlayabiliriz.

./gradlew bootBuildImage --imageName=graalvmdemo:v001

Yukarıdaki komut ile native-image almaya başlayabiliriz. Projenin kök dizinindeyken terminal ekranına yukarıdaki kodu yazmamız yeterli olacaktır. graalvmdemo:v001 adında bir native-image almaya başlayabiliriz.

ANALİZ ZAMANI

1.ANALİZ

Yukarıdaki komutu başlatıp çalıştırdığımız zaman, aşağıdaki şekillerde olduğu gibi terminal ekranında akışlar göreceğiz.

En son görseldeli kırmızı kutuya baktığımız zaman native-image alma süresi 29 dk 17 sn sürdü.

2.ANALİZ

Docker Desktop’u açtığımız zaman ise oluşturduğumuz image karşımıza çıktı.

Sarı kutunun içindeki işaretli olan image bizim image ve builder image’larını saymazsak kendisi için 829 mb’lık bir size ile image oluştu. Run butonuna basarak image’ı çalıştırıp analize devam edebiliriz.

3.ANALİZ

Şekilde görüldüğü üzere run butonuna bastığımız zaman uygulamamız native-image ile ayağa kalktı.

Stats istatistikleri de aslında yukarıdaki gibi.

4.ANALİZ

Şimdi 1 milyon veri içerisinden parametre olarak verdiğimiz değeri mongo’da arayacak requesti uygulamamıza gönderelim.

Ama burada öncelik olarak yapmamız gereken şey, çalıştırdığımız native-image’ın portunu expose etmek olmalı. Bunun için terminal ekranından bir tane docker komutu yazmamız lazım.

docker run -p 8282:8282 -d graalvmdemo:v001

Artık portu’da expose ettiğimize göre uygulamamızı docker desktop üzerinden çalıştırabiliriz.

localhost:8282/person/serchByParamFromFirstName/ab

bu isteği attığımızda,

Aşağıdaki kırmızı kutuda görüldüğü üzere, yazdığımız aspect çalıştı ve 279811 ms’de çıktı verdi.

Bu da aslında istek yürütülürkenki docker desktop grafikleri.

SONUÇ OLARAK;

GraalVM üzerinde koşan bir uygulama geliştirmek bazı use-case’ler için önemli olabilir. Benim local makinemde performans açısından kötü sonuçlar verse de bu tamamen ram ile alakalı bir durum. Aynı zamanda o anki kullanılan CPU’nun da etkisi çok fazla. Buradan hangi firmaların GraalVM kullandığını görebilirsiniz. Şimdiye kadar, “Şu firmanın projesi GraalVM üzerinde çalışıyormuş” diye şaşıra şaşıra bir cümle kullanmadık sohbet ettiğimiz masalarda, ama bence scaling için, hız için ilerleyen zamanlarda çok iyi konumda olacağını öngörüyorum. GraalVM çok bakir bir alan ve ilerisi için bu alana öğrenme olarak yatırım yapılabilir.

Okuduğunuz için teşekkür ederim. Projenin kaynak kodlarına buradan ulaşabilirsiniz.

İletişim, İşbirlikleri ve Teklifler için,

github: https://github.com/fsk

bitbucket: https://bitbucket.org/furkandev

twitter: https://twitter.com/0xfsk

mail: furkansahinkulaksiz@gmail.com

linkedIn: https://www.linkedin.com/in/frknshnklksz/

superpeer: https://superpeer.com/fsk

--

--