GraalVM Kurulumu ve Native Image Derleme Rehberi
Java uygulamalarını production’a alırken en sık duyduğum şikayet şu: “JVM başlayana kadar bir dakika geçiyor, bu kabul edilemez.” Mikroservis mimarilerinde, serverless fonksiyonlarda ya da CLI araçlarında bu gecikme gerçekten ciddi bir problem. İşte tam bu noktada GraalVM devreye giriyor ve “native image” kavramıyla oyunu değiştiriyor.
GraalVM, Oracle tarafından geliştirilen yüksek performanslı bir JDK dağıtımı. Ama onu özel kılan şey sadece hızlı bir JVM olması değil; Java, Kotlin, Scala gibi JVM dillerini doğrudan native binary’ye derleyebilmesi. Yani ortaya çıkan uygulama, JVM olmadan çalışan, anında başlayan, düşük memory kullanan bir executable oluyor. Bu yazıda GraalVM kurulumundan native image derlemeye kadar her şeyi adım adım ele alacağız.
GraalVM Nedir, Ne Değildir?
Birçok kişi GraalVM’i sadece “hızlı JVM” olarak tanımlıyor ama bu eksik bir tanım. GraalVM iki farklı kullanım senaryosu sunuyor:
JIT modu: Standart JVM gibi çalışır, ama daha gelişmiş bir JIT derleyici (Graal compiler) kullanır. Bazı iş yüklerinde HotSpot’tan belirgin şekilde daha iyi throughput elde edebilirsiniz.
Native Image modu: AOT (Ahead-of-Time) derleme yaparak uygulamayı doğrudan işletim sistemi binary’sine çevirir. Bu mod için ayrıca native-image aracını kurmanız gerekiyor.
Community Edition (CE) ücretsiz ve açık kaynaklı, Enterprise Edition (EE) ise ek optimizasyonlar sunuyor. Biz bu yazıda CE üzerine odaklanacağız.
Kurulum Öncesi Hazırlık
Kuruluma geçmeden önce sistemin native image derleme için hazır olması lazım. Native image derlemesi, C kütüphanelerine ve yerel derleyiciye ihtiyaç duyuyor.
Ubuntu/Debian için:
sudo apt-get update
sudo apt-get install -y build-essential libz-dev zlib1g-dev
RHEL/CentOS/Fedora için:
sudo dnf groupinstall "Development Tools"
sudo dnf install zlib-devel
macOS için:
xcode-select --install
Bu adımı atlamayın. Native image derleme sırasında gcc veya clang bulunamazsa derleme başarısız oluyor ve hata mesajları sizi yanıltıcı yerlere götürebiliyor.
SDKMAN ile GraalVM Kurulumu
GraalVM kurmanın en temiz yolu SDKMAN kullanmak. Farklı JVM sürümlerini yan yana yönetmek için bu araç gerçekten hayat kurtarıyor.
SDKMAN henüz kurulu değilse:
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
Mevcut GraalVM versiyonlarını listeleyelim:
sdk list java | grep graalvm
Çıktıda 21.0.2-graalce gibi versiyonlar göreceksiniz. GraalVM CE için -graalce suffix’ini kullanıyoruz:
sdk install java 21.0.2-graalce
sdk use java 21.0.2-graalce
Kurulumu doğrulayalım:
java -version
Çıktı şuna benzer bir şey olmalı:
java version "21.0.2" 2024-01-16 LTS
Java(TM) SE Runtime Environment GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30)
Java HotSpot(TM) 64-Bit Server VM GraalVM CE 21.0.2+13.1 (build 21.0.2+13-jvmci-23.1-b30, mixed mode, sharing)
Manuel Kurulum (Alternatif Yöntem)
SDKMAN kullanamıyorsanız ya da sistem genelinde kurulum yapmanız gerekiyorsa manuel yöntem daha uygun:
# GraalVM CE 21'i indir
wget https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-21.0.2/graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz
# Çıkart
tar -xzf graalvm-community-jdk-21.0.2_linux-x64_bin.tar.gz
# Uygun dizine taşı
sudo mv graalvm-community-openjdk-21.0.2+13.1 /opt/graalvm-ce-21
# Ortam değişkenlerini ayarla
echo 'export JAVA_HOME=/opt/graalvm-ce-21' >> ~/.bashrc
echo 'export PATH=$JAVA_HOME/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
GraalVM 21 ve sonrasında native-image artık otomatik olarak dahil geliyor. Eski sürümlerde ayrıca kurmanız gerekiyordu:
# GraalVM 17 ve öncesi için (artık gerekli değil ama referans olsun)
gu install native-image
native-image komutunu test edelim:
native-image --version
İlk Native Image Denemeleri
Teoriden pratiğe geçelim. Basit bir “Hello World” ile başlayalım ama gerçekçi bir senaryo için biraz daha anlamlı bir örnek kullanalım.
SystemInfo.java adında bir dosya oluşturalım:
public class SystemInfo {
public static void main(String[] args) {
System.out.println("=== System Information ===");
System.out.println("OS: " + System.getProperty("os.name") + " " + System.getProperty("os.version"));
System.out.println("Architecture: " + System.getProperty("os.arch"));
System.out.println("Java Version: " + System.getProperty("java.version"));
System.out.println("Available Processors: " + Runtime.getRuntime().availableProcessors());
System.out.println("Max Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + " MB");
long startTime = System.currentTimeMillis();
// Basit bir hesaplama
long sum = 0;
for (int i = 0; i < 1_000_000; i++) {
sum += i;
}
long elapsed = System.currentTimeMillis() - startTime;
System.out.println("Sum (0-999999): " + sum);
System.out.println("Calculation time: " + elapsed + "ms");
}
}
Önce standart Java ile derleyip çalıştıralım:
# Standart derleme ve çalıştırma
javac SystemInfo.java
time java SystemInfo
Şimdi native image oluşturalım:
# Native image derleme
native-image SystemInfo
# Çalıştır
time ./systeminfo
Farkı göreceksiniz. JVM ile başlangıç süresi genellikle 150-300ms civarındayken, native image ile bu süre 5-20ms’ye düşüyor. Üstelik oluşan binary’nin boyutu bağımsız çalışıyor, JVM gerektirmiyor.
Maven Projesi ile Native Image
Gerçek dünya uygulamaları single-file Java değil tabii ki. Bir Maven projesiyle nasıl çalışacağımıza bakalım. Basit bir HTTP health check aracı yapacağız.
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>health-checker</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.1</version>
<extensions>true</extensions>
<configuration>
<imageName>health-checker</imageName>
<mainClass>com.example.HealthChecker</mainClass>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
Native image build:
mvn -Pnative package
# ya da
mvn package -Pnative
Sonuçta target/health-checker binary’si oluşacak.
Reflection ve Dinamik Özellikler
Native image’in en büyük zorluğu reflection, dynamic class loading ve proxy kullanımı. AOT derleme sırasında hangi sınıfların reflection ile kullanılacağını bilmek gerekiyor.
GraalVM bu sorunu çözmek için tracing agent sunuyor. Agent, uygulamayı normal JVM üzerinde çalıştırırken hangi reflection, proxy ve resource erişimlerinin yapıldığını kaydediyor.
# Tracing agent ile çalıştır
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image
-jar target/health-checker-1.0-SNAPSHOT.jar
# Birden fazla çalıştırma sonuçlarını birleştirmek için
java -agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image
-jar target/health-checker-1.0-SNAPSHOT.jar --some-different-path
Bu komut src/main/resources/META-INF/native-image dizininde şu dosyaları oluşturuyor:
- reflect-config.json: Reflection ile erişilen sınıflar
- resource-config.json: Classpath’ten okunan kaynaklar
- proxy-config.json: Dinamik proxy tanımları
- jni-config.json: JNI erişimleri
- serialization-config.json: Serializasyon bilgileri
Bu dosyalar native image derlemesi sırasında otomatik olarak alınıyor. Uygulamanızı agent ile çalıştırırken mümkün olduğunca fazla kod path’ini tetiklemeniz önemli. Test suite’inizi agent ile çalıştırmak bu açıdan ideal.
Quarkus ile Native Image (Production Senaryosu)
Tek başına native image derlemek didaktik açıdan faydalı, ama production’da çoğunlukla bir framework kullanıyorsunuz. Quarkus, native image için en iyi framework desteğini sunuyor.
Yeni bir Quarkus projesi oluşturalım:
mvn io.quarkus.platform:quarkus-maven-plugin:3.8.1:create
-DprojectGroupId=com.example
-DprojectArtifactId=quarkus-native-demo
-DclassName="com.example.GreetingResource"
-Dpath="/hello"
-Dextensions="resteasy-reactive"
cd quarkus-native-demo
Küçük bir REST endpoint ekleyelim:
package com.example;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import java.time.Instant;
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public String hello(@QueryParam("name") String name) {
String greeting = (name != null && !name.isBlank()) ? name : "World";
return String.format(
"{"message": "Hello, %s!", "timestamp": "%s", "version": "1.0"}",
greeting,
Instant.now().toString()
);
}
@GET
@Path("/health")
@Produces(MediaType.TEXT_PLAIN)
public String health() {
return "UP";
}
}
Native image olarak derleyelim:
# Container kullanarak (GraalVM kurulu olmayan makinelerde de çalışır)
./mvnw package -Pnative -Dquarkus.native.container-build=true
# Yerel GraalVM ile
./mvnw package -Pnative
Container build seçeneği özellikle CI/CD ortamlarında çok kullanışlı. Docker container içinde derleme yapıyor ve Linux binary üretiyor.
Çalıştırıp karşılaştıralım:
# Native binary başlangıç süresi
time ./target/quarkus-native-demo-1.0.0-SNAPSHOT-runner
# JVM modunda başlangıç süresi (karşılaştırma için)
time java -jar target/quarkus-app/quarkus-run.jar
Quarkus native tipik olarak 10-50ms’de başlarken, JVM modu 1-3 saniye alıyor. Memory kullanımı da native’de belirgin şekilde düşük: JVM modunda 200-400MB RSS olabilirken, native’de 30-80MB görüyorsunuz.
Docker ile Native Image
Native binary’yi bir container’a koymak, hem dağıtımı kolaylaştırıyor hem de binary’yi izole ediyor. Distroless veya scratch image kullanabilirsiniz:
# Multi-stage build
FROM ghcr.io/graalvm/native-image-community:21 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
# Maven wrapper olmadan direkt maven ile
RUN microdnf install -y maven &&
mvn package -Pnative -DskipTests
# Minimal runtime image
FROM debian:12-slim
WORKDIR /app
COPY --from=builder /app/target/health-checker .
# Non-root user oluştur
RUN useradd -r -u 1001 appuser
USER appuser
EXPOSE 8080
ENTRYPOINT ["./health-checker"]
Build ve çalıştırma:
docker build -t health-checker:native .
docker run --rm -p 8080:8080 health-checker:native
# Image boyutunu karşılaştır
docker images | grep health-checker
Native image tabanlı bir Quarkus uygulaması için final Docker image boyutu genellikle 50-100MB civarında oluyor. JVM tabanlı versiyonu ise 300-500MB’a ulaşabiliyor.
Performans Profili ve Optimizasyon
Native image kullanırken dikkat etmeniz gereken bazı derleme bayrakları var:
native-image
--no-fallback
-O2
-march=native
--gc=G1
-H:+AddAllCharsets
-H:+ReportExceptionStackTraces
--initialize-at-build-time
-o optimized-app
com.example.MainClass
Önemli parametreler:
- –no-fallback: JVM fallback’i devre dışı bırakır. Eğer bazı özellikler native’e çevrilemezse derleme başarısız olur, sessizce JVM’e düşmez.
- -O2: Optimizasyon seviyesi. Derleme süresi artar ama binary daha hızlı çalışır.
- -march=native: Derlemenin yapıldığı CPU’ya özel optimizasyon yapar. Farklı CPU mimarisinde çalıştırılacaksa kullanmayın.
- –gc=G1: GC seçimi. Native image’de epsilon (GC yok), serial ve G1 arasında seçim yapabilirsiniz.
- -H:+AddAllCharsets: Tüm charset’lerin dahil edilmesini sağlar. İnternationalization gerektiren uygulamalar için önemli.
- -H:+ReportExceptionStackTraces: Hata ayıklama için exception stack trace bilgisini korur.
Profile-guided optimization (PGO) da native image performansını artırabiliyor:
# PGO profili topla
native-image --pgo-instrument -o app-instrumented MyApp
./app-instrumented # gerçek iş yükü ile çalıştır, profil dosyası oluşur
# Profil ile native image derle
native-image --pgo=default.iprof -o app-optimized MyApp
Yaygın Sorunlar ve Çözümleri
“Image heap too large” hatası: Derleme sırasında fazla fazla nesne heap’e alınıyor. --no-fallback ile birlikte --initialize-at-run-time=com.problematic.Class kullanın.
ClassNotFoundException runtime’da: Reflection config eksik. Tracing agent ile tekrar çalıştırın ve tüm kod path’lerini kapsadığınızdan emin olun.
Derleme çok uzun sürüyor: Normal. Küçük bir uygulama bile 2-5 dakika alabilir, büyük uygulamalar 10-20 dakikaya çıkabiliyor. CI/CD pipeline’ınızı buna göre ayarlayın. Paralel derleme için:
native-image -J-Xmx8g -J-XX:ActiveProcessorCount=8 MyApp
Derleme sırasında bellek yetersizliği: Native image derleme bellek açısından açgözlü. 8GB+ RAM öneriliyor. Container ortamında bellek limitini yeterince yüksek tutun.
SSL/TLS sorunları runtime’da: Native image SSL için ek konfigürasyon gerektirebilir:
native-image --enable-url-protocols=https -H:+JNI MyApp
CI/CD Entegrasyonu
GitHub Actions için örnek bir workflow:
name: Native Image Build
on:
push:
branches: [main]
jobs:
native-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup GraalVM
uses: graalvm/setup-graalvm@v1
with:
java-version: '21'
distribution: 'graalvm-community'
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Cache Maven
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
- name: Build Native Image
run: mvn package -Pnative -DskipTests
- name: Run Tests
run: ./target/my-app --test-mode
- name: Upload Binary
uses: actions/upload-artifact@v3
with:
name: native-binary
path: target/my-app
graalvm/setup-graalvm@v1 action’ı hem GraalVM kurulumunu hem de native-image’i otomatik halledıyor, ayrıca build cache entegrasyonu sunuyor.
Native Image Ne Zaman Kullanmalısınız?
Her senaryoda native image ideal değil. Kullanması gereken durumlar:
- CLI araçları: Hızlı başlangıç kritik, sürekli çalışmıyor.
- Serverless/FaaS: AWS Lambda, Google Cloud Functions gibi ortamlarda cold start önemli.
- Sidecar container’lar: Kubernetes sidecar’ları için minimal kaynak kullanımı önemli.
- Mikroservis örnekleri çok hızlı ölçekleniyor: Yeni pod’ların anında ayağa kalkması gerekiyor.
Dikkatli düşünmek gereken durumlar:
- Uzun süre çalışan, yoğun iş yükü: JVM JIT zamanla native’den daha iyi throughput verebilir.
- Yoğun reflection kullanan framework’ler: Hibernate gibi araçlar native’de ekstra konfigürasyon gerektiriyor.
- Derleme süresi kritik: Büyük projelerde native derleme çok uzun sürebilir.
Sonuç
GraalVM native image, doğru kullanıldığında gerçekten oyun değiştirici bir teknoloji. JVM başlangıç süresi ve bellek kullanımı sorununu kökten çözüyor ve Java ekosistemini Go veya Rust gibi dillerle aynı deployment senaryolarında rekabetçi kılıyor.
Kurulum tarafında SDKMAN ile GraalVM CE almak en temiz yol. İlk denemeler için single-file Java, production için Quarkus veya Micronaut gibi native-first framework’leri tercih edin. Reflection sorunları için tracing agent’ı ihmal etmeyin, agent olmadan büyük projelerde runtime hataları kaçınılmaz oluyor.
CI/CD tarafında derleme süresi ve bellek gereksinimini baştan planlayın. 8GB RAM ve 15-20 dakikalık build süresi için pipeline’ınızı hazırlayın. Bu bedeli ödediğinizde elde ettiğiniz: anında başlayan, az kaynak tüketen, JVM gerektirmeyen uygulamalar. Özellikle Kubernetes tabanlı mikroservis ortamlarında bu faydalar çok somut bir şekilde hissediliyor.
