<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Java arşivleri - Ali Karahisar</title>
	<atom:link href="https://alikarahisar.com/blog/category/java/feed/" rel="self" type="application/rss+xml" />
	<link>https://alikarahisar.com/blog/category/java/</link>
	<description>Yazılım ve AI Hakkında Karalamalar</description>
	<lastBuildDate>Thu, 03 Sep 2026 11:16:28 +0000</lastBuildDate>
	<language>tr</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1</generator>

<image>
	<url>https://alikarahisar.com/blog/wp-content/uploads/2026/08/cropped-Code_Generated_Image-32x32.png</url>
	<title>Java arşivleri - Ali Karahisar</title>
	<link>https://alikarahisar.com/blog/category/java/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>GraalVM Native Image ile Serverless ve Mikroservislerde Cold Start Sorununu Çözmek</title>
		<link>https://alikarahisar.com/blog/graalvm-native-image-cold-start-spring-boot-3/</link>
					<comments>https://alikarahisar.com/blog/graalvm-native-image-cold-start-spring-boot-3/#respond</comments>
		
		<dc:creator><![CDATA[Ali Karahisar]]></dc:creator>
		<pubDate>Thu, 03 Sep 2026 11:16:22 +0000</pubDate>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[Yazılım]]></category>
		<category><![CDATA[AOT derleme]]></category>
		<category><![CDATA[cloud-native Java]]></category>
		<category><![CDATA[GraalVM Native Image]]></category>
		<category><![CDATA[Java cold start]]></category>
		<category><![CDATA[JIT derleme]]></category>
		<category><![CDATA[memory footprint]]></category>
		<category><![CDATA[mikroservis performansı]]></category>
		<category><![CDATA[native executable]]></category>
		<category><![CDATA[serverless Java]]></category>
		<category><![CDATA[Spring AOT]]></category>
		<category><![CDATA[Spring Boot 3 native image]]></category>
		<guid isPermaLink="false">https://alikarahisar.com/blog/?p=44</guid>

					<description><![CDATA[<p>Java; güçlü ekosistemi, olgun araçları ve yüksek çalışma zamanı performansıyla mikroservis geliştirmede en güvenilir seçeneklerden biri. Ancak konu serverless fonksiyonlar, kısa ömürlü container’lar ve hızla ölçeklenen Kubernetes iş yükleri olduğunda klasik JVM modelinin iki maliyeti daha görünür hâle gelir: başlangıç süresi ve bellek tüketimi. Yeni bir instance’ın JVM’i başlatması, sınıfları yüklemesi, framework context’ini oluşturması ve [&#8230;]</p>
<p><a href="https://alikarahisar.com/blog/graalvm-native-image-cold-start-spring-boot-3/">GraalVM Native Image ile Serverless ve Mikroservislerde Cold Start Sorununu Çözmek</a> yazısı ilk önce <a href="https://alikarahisar.com/blog">Ali Karahisar</a> üzerinde ortaya çıktı.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Java; güçlü ekosistemi, olgun araçları ve yüksek çalışma zamanı performansıyla mikroservis geliştirmede en güvenilir seçeneklerden biri. Ancak konu serverless fonksiyonlar, kısa ömürlü container’lar ve hızla ölçeklenen Kubernetes iş yükleri olduğunda klasik JVM modelinin iki maliyeti daha görünür hâle gelir: başlangıç süresi ve bellek tüketimi.</p>



<span id="more-44"></span>



<p class="wp-block-paragraph">Yeni bir instance’ın JVM’i başlatması, sınıfları yüklemesi, framework context’ini oluşturması ve kodu optimize etmesi zaman alır. İstek trafiğinin olmadığı bir sürenin ardından fonksiyon yeniden oluşturuluyorsa kullanıcı bu maliyeti doğrudan “cold start” gecikmesi olarak hisseder.</p>



<p class="wp-block-paragraph">GraalVM Native Image, Java uygulamasını dağıtım öncesinde makine koduna derleyerek bu denklemi değiştirir. Uygulama, çalışma anında JAR ve tam bir JVM başlatmak yerine işletim sistemi üzerinde doğrudan çalışan platforma özgü bir executable olarak açılır. GraalVM belgelerine göre native executable yalnızca erişilebilir uygulama ve JDK kodunu, gerekli çalışma zamanı bileşenlerini ve statik olarak bağlanan yerel kodu içerir; bunun sonucu milisaniyeler düzeyinde başlangıç, warm-up gerektirmeyen performans ve daha düşük kaynak tüketimidir. (<a href="https://www.graalvm.org/latest/reference-manual/native-image/">GraalVM Native Image belgeleri</a>)</p>



<h2 class="wp-block-heading">Cold start neden oluşur?</h2>



<p class="wp-block-paragraph">Bir Java mikroservisinin ilk isteğe hazır hâle gelmeden önce tipik olarak şu işlemleri tamamlaması gerekir:</p>



<ol start="1" class="wp-block-list">
<li>JVM süreci başlatılır.</li>



<li>Uygulama sınıfları ve bağımlılıklar yüklenir.</li>



<li>Bytecode doğrulanır ve yorumlanmaya başlanır.</li>



<li>Spring ApplicationContext oluşturulur.</li>



<li>Bean tanımları, koşullu konfigürasyonlar ve proxy’ler hazırlanır.</li>



<li>Sık çalışan kod yolları belirlenir ve JIT compiler tarafından optimize edilir.</li>



<li>Bağlantı havuzları ve harici servis istemcileri hazırlanır.</li>
</ol>



<p class="wp-block-paragraph">Uzun süre çalışan bir serviste bu maliyet yalnızca bir kez ödenir ve JIT optimizasyonları zaman içinde önemli throughput avantajı sağlar. Buna karşılık serverless platformlarda, scale-to-zero kullanan Kubernetes ortamlarında veya çok sık pod oluşturan sistemlerde başlangıç maliyeti tekrar tekrar ödenebilir.</p>



<p class="wp-block-paragraph">Cold start yalnızca uygulamanın <code>main</code> metoduna ulaşma süresi değildir. Gerçek ölçüm, yeni instance’ın ilk isteğe doğru yanıt verebildiği ana kadar geçen süreyi kapsamalıdır.</p>



<h2 class="wp-block-heading">JIT ve AOT arasındaki mimari fark</h2>



<h3 class="wp-block-heading">JIT: Çalışırken öğrenen optimizasyon</h3>



<p class="wp-block-paragraph">Geleneksel JVM modelinde Java kaynak kodu önce bytecode’a çevrilir:</p>



<pre class="wp-block-code"><code>Java kaynak kodu
        ↓
     Bytecode
        ↓
 JVM + Interpreter
        ↓
   JIT Compiler
        ↓
Optimize makine kodu</code></pre>



<p class="wp-block-paragraph">JIT, uygulamanın gerçek çalışma davranışını gözlemler. Hangi metotların sık çağrıldığını, dallanmaların hangi yönde ilerlediğini ve hangi kod yollarının “hot” olduğunu belirleyerek bu bölümleri çalışma sırasında optimize eder.</p>



<p class="wp-block-paragraph">Bunun önemli bir avantajı vardır: Optimizasyon kararları gerçek trafik profilinden beslenir. Uzun süre çalışan ve CPU yoğun işlem yapan servislerde JVM, yeterli warm-up sonrasında çok yüksek tepe performansına ulaşabilir.</p>



<p class="wp-block-paragraph">Bedeli ise JVM’in, derleyicinin, profil verilerinin ve sınıf metadata’sının bellekte bulunmasıdır. Uygulama ayrıca en iyi performansına hemen değil, belirli bir ısınma süresinden sonra ulaşır.</p>



<h3 class="wp-block-heading">AOT: Çalışmadan önce tamamlanan derleme</h3>



<p class="wp-block-paragraph">GraalVM Native Image, bytecode’u build aşamasında analiz eder ve platforma özgü makine koduna dönüştürür:</p>



<pre class="wp-block-code"><code>Uygulama + bağımlılıklar + JDK sınıfları
                    ↓
      Erişilebilirlik analizi
                    ↓
       AOT optimizasyon ve derleme
                    ↓
 Kod + image heap + native runtime
                    ↓
        Native executable</code></pre>



<p class="wp-block-paragraph">Analiz <code>main</code> metodu gibi giriş noktalarından başlar. Erişilebilen sınıflar, metotlar ve alanlar tekrarlı biçimde bulunur; kullanılmadığı belirlenen parçalar native executable’a eklenmez. Derlemeden sonra yeni program elemanlarının çalışma anında sisteme eklenememesi “closed-world assumption”, yani kapalı dünya varsayımı olarak adlandırılır. (<a href="https://www.graalvm.org/latest/reference-manual/native-image/basics/">Native Image çalışma modeli</a>)</p>



<p class="wp-block-paragraph">Bazı sınıflar build sırasında initialize edilebilir. Oluşan nesneler ve statik durum “image heap” içerisine kaydedilir. Uygulama açıldığında bu işlerin yeniden yapılmaması startup süresini kısaltır.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><th>Özellik</th><th>JVM + JIT</th><th>GraalVM Native Image + AOT</th></tr><tr><td>Derleme zamanı</td><td>Çalışma sırasında devam eder</td><td>Büyük ölçüde build sırasında tamamlanır</td></tr><tr><td>Başlangıç</td><td>Class loading ve warm-up nedeniyle daha yavaş</td><td>Genellikle çok hızlı</td></tr><tr><td>Bellek</td><td>JVM, JIT ve metadata maliyeti vardır</td><td>Genellikle daha düşük RSS</td></tr><tr><td>Tepe throughput</td><td>Uzun ömürlü işlerde çok güçlü</td><td>İş yüküne göre JIT’in gerisinde kalabilir</td></tr><tr><td>Dinamik sınıf yükleme</td><td>Doğal olarak desteklenir</td><td>Kapalı dünya varsayımıyla sınırlıdır</td></tr><tr><td>Build süresi</td><td>Görece kısa</td><td>Daha uzun ve daha fazla kaynak tüketebilir</td></tr><tr><td>Dağıtım</td><td>JAR ve uyumlu JVM gerekir</td><td>İşletim sistemi ve mimariye özel binary gerekir</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">AOT her durumda JIT’ten daha hızlı demek değildir. Native Image’ın temel avantajı başlangıç ve kaynak verimliliğidir. Uzun süre çalışan, CPU yoğun servislerde JIT’in çalışma zamanı profiline dayanan optimizasyonları daha yüksek throughput sağlayabilir. GraalVM’in Profile-Guided Optimization özelliği, temsilî trafikten profil toplayıp bunu AOT derlemeye taşıyarak aradaki farkı azaltabilir. (<a href="https://www.graalvm.org/dev/reference-manual/native-image/optimizations-and-performance/">GraalVM optimizasyon belgeleri</a>)</p>



<h2 class="wp-block-heading">Startup süresi ve memory footprint üzerindeki etkisi</h2>



<p class="wp-block-paragraph">Native executable çalıştırılırken tam bir HotSpot JVM’in başlatılmasına ve uygulama kodunun JIT tarafından ısıtılmasına gerek kalmaz. Gereksiz sınıfların binary’ye alınmaması da class metadata, JIT code cache ve derleyici altyapısının çalışma zamanı maliyetlerini azaltır.</p>



<p class="wp-block-paragraph">Cloud-native sistemlerde bu özellikler üç alanda değer üretir:</p>



<ul class="wp-block-list">
<li>Serverless fonksiyonlar ilk isteğe daha kısa sürede cevap verebilir.</li>



<li>Kubernetes pod’ları daha hızlı hazır duruma gelerek scale-out gecikmesini azaltabilir.</li>



<li>Aynı bellek bütçesi içerisinde daha fazla servis instance’ı çalıştırılabilir.</li>
</ul>



<p class="wp-block-paragraph">Ancak sonucu tek bir evrensel yüzdeyle ifade etmek doğru değildir. Kazanç; kullanılan framework’e, dependency sayısına, heap ayarlarına, GC seçimine, container limitlerine ve uygulamanın başlangıç sırasında yaptığı I/O işlemlerine bağlıdır.</p>



<p class="wp-block-paragraph">Örneğin uygulama açılışta veritabanı migrasyonu çalıştırıyor veya uzak bir secret servisine bağlanıyorsa native executable bu ağ gecikmesini ortadan kaldırmaz. GraalVM kod başlatma maliyetini azaltır; dış sistemlerin gecikmesini sihirli biçimde çözmez.</p>



<p class="wp-block-paragraph">Bu nedenle JVM ve native sürümleri aynı koşullarda karşılaştırın:</p>



<pre class="wp-block-code"><code># JVM sürümü
/usr/bin/time -v java -jar target/cold-start-demo.jar

# Native sürüm
/usr/bin/time -v ./target/cold-start-demo</code></pre>



<p class="wp-block-paragraph">macOS üzerinde ayrıntılı süreç ölçümü için:</p>



<pre class="wp-block-code"><code>/usr/bin/time -l ./target/cold-start-demo</code></pre>



<p class="wp-block-paragraph">Yalnızca konsoldaki “Started” mesajını değil, aşağıdaki metrikleri kaydedin:</p>



<ul class="wp-block-list">
<li>Process başlangıcından readiness durumuna kadar geçen süre</li>



<li>İlk isteğin p50, p95 ve p99 gecikmesi</li>



<li>Warm request gecikmesi</li>



<li>Peak RSS ve steady-state RSS</li>



<li>CPU tüketimi</li>



<li>Binary veya container image boyutu</li>



<li>Native image build süresi</li>



<li>Gerçek trafik altındaki throughput</li>
</ul>



<h2 class="wp-block-heading">GraalVM Native Image’ın zorlandığı alanlar</h2>



<h3 class="wp-block-heading">1. Reflection</h3>



<p class="wp-block-paragraph">Reflection kullanılan kodda çağrılacak sınıf veya metot adı çalışma anında bir konfigürasyondan gelebilir. Statik analiz bu ilişkiyi göremiyorsa ilgili sınıfın kullanılmadığını düşünerek onu binary dışında bırakabilir.</p>



<p class="wp-block-paragraph">Sonuç olarak uygulama başarıyla derlense bile yalnızca belirli bir endpoint çağrıldığında hata verebilir. JSON serileştirme, ORM, dependency injection ve plugin tabanlı kütüphaneler bu açıdan özellikle test edilmelidir.</p>



<p class="wp-block-paragraph">Spring Boot 3’ün AOT motoru controller dönüş tipleri, bean tanımları ve pek çok framework kullanımına ait hint’leri otomatik üretir. Otomatik çıkarımın yetmediği durumlarda <code>@RegisterReflectionForBinding</code> veya <code>RuntimeHintsRegistrar</code> kullanılabilir. (<a href="https://docs.spring.io/spring-boot/reference/packaging/native-image/advanced-topics.html">Spring Boot özel native hint rehberi</a>)</p>



<p class="wp-block-paragraph">Örneğin çalışma anında JSON’a dönüştürülecek bir sınıfı açıkça kaydetmek için:</p>



<pre class="wp-block-code"><code>import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.context.annotation.Configuration;

@Configuration
@RegisterReflectionForBinding(PaymentRequest.class)
class NativeConfiguration {
}</code></pre>



<h3 class="wp-block-heading">2. Dynamic proxy</h3>



<p class="wp-block-paragraph">Spring AOP, declarative transaction yönetimi ve bazı HTTP client kütüphaneleri çalışma anında proxy oluşturabilir. JVM üzerinde ihtiyaç anında üretilebilen bu sınıfların native executable içinde önceden bilinmesi gerekir.</p>



<p class="wp-block-paragraph">Spring AOT, kendi tanıyabildiği proxy’leri build sırasında üretir. Uygulamaya özgü bir JDK proxy için runtime hint eklenebilir:</p>



<pre class="wp-block-code"><code>import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;

class ApplicationRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(
            RuntimeHints hints,
            ClassLoader classLoader) {

        hints.proxies().registerJdkProxy(PaymentClient.class);
    }
}</code></pre>



<p class="wp-block-paragraph">Registrar’ı uygulamaya bağlayalım:</p>



<pre class="wp-block-code"><code>import org.springframework.context.annotation.ImportRuntimeHints;

@SpringBootApplication
@ImportRuntimeHints(ApplicationRuntimeHints.class)
public class PaymentApplication {

    public static void main(String&#91;] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}</code></pre>



<p class="wp-block-paragraph">Spring’in CGLIB tabanlı proxy’leri de JVM’de çalışma anında oluşturulurken native build sürecinde önceden üretilir. Oluşturulan proxy sınıfları Maven projelerinde <code>target/spring-aot/main/classes</code> altında incelenebilir. (<a href="https://docs.spring.io/spring-boot/reference/packaging/native-image/introducing-graalvm-native-images.html">Spring AOT proxy üretimi</a>)</p>



<h3 class="wp-block-heading">3. Classpath kaynakları</h3>



<p class="wp-block-paragraph">JSON şablonları, sertifikalar, SQL dosyaları, localization bundle’ları veya statik dosyalar Java kodu olmadığı için yalnızca erişilebilirlik analiziyle her zaman bulunamaz.</p>



<p class="wp-block-paragraph">Bir kaynağı Spring runtime hints API ile eklemek mümkündür:</p>



<pre class="wp-block-code"><code>@Override
public void registerHints(
        RuntimeHints hints,
        ClassLoader classLoader) {

    hints.resources().registerPattern("templates/*.json");
    hints.resources().registerPattern("db/migration/**");
}</code></pre>



<p class="wp-block-paragraph">GraalVM ayrıca <code>META-INF/native-image</code> altındaki <code>reachability-metadata.json</code> dosyalarını okuyabilir:</p>



<pre class="wp-block-code"><code>{
  "resources": &#91;
    {
      "glob": "templates/*.json"
    }
  ]
}</code></pre>



<p class="wp-block-paragraph">Native executable içine gömülen kaynaklar gerçek bir dosya sistemi yolu olmayabilir. Bu nedenle <code>URL#getFile()</code> yerine <code>getResourceAsStream()</code> veya <code>URL#openStream()</code> tercih edilmelidir. Gerekli metadata bulunmayan dinamik erişimler çalışma zamanı hatasına dönüşebilir. (<a href="https://www.graalvm.org/jdk25/reference-manual/native-image/metadata/">GraalVM Reachability Metadata</a>)</p>



<h2 class="wp-block-heading">Spring Boot 3 ile native executable oluşturma</h2>



<p class="wp-block-paragraph">Aşağıdaki örnek Maven tabanlı bir Spring Boot 3 projesini temel alır.</p>



<h3 class="wp-block-heading">Adım 1: Projeyi oluşturun</h3>



<p class="wp-block-paragraph"><a href="https://start.spring.io/">start.spring.io</a> üzerinden şu seçimlerle bir proje oluşturabilirsiniz:</p>



<ul class="wp-block-list">
<li>Java 21 gibi desteklenen bir LTS sürümü</li>



<li>Spring Boot 3.x</li>



<li>Maven</li>



<li>Spring Web</li>



<li>GraalVM Native Support</li>
</ul>



<p class="wp-block-paragraph">Basit bir endpoint ekleyelim:</p>



<pre class="wp-block-code"><code>package com.example.coldstart;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class ColdStartApplication {

    @GetMapping("/hello")
    String hello() {
        return "Hello from GraalVM Native Image";
    }

    public static void main(String&#91;] args) {
        SpringApplication.run(ColdStartApplication.class, args);
    }
}</code></pre>



<h3 class="wp-block-heading">Adım 2: Maven plugin’lerini kontrol edin</h3>



<p class="wp-block-paragraph">Spring Boot parent’ın sağladığı <code>native</code> profilinden yararlanmak için build bölümünde şu plugin’ler bulunmalıdır:</p>



<pre class="wp-block-code"><code>&lt;build&gt;
    &lt;plugins&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
        &lt;/plugin&gt;

        &lt;plugin&gt;
            &lt;groupId&gt;org.graalvm.buildtools&lt;/groupId&gt;
            &lt;artifactId&gt;native-maven-plugin&lt;/artifactId&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;</code></pre>



<p class="wp-block-paragraph">Spring AOT işlemi ApplicationContext’i build aşamasında analiz eder; bean tanımlarına ve kapalı dünya varsayımına uygun optimize edilmiş kod ile GraalVM hint dosyaları üretir. Maven çıktıları <code>target/spring-aot</code> altında incelenebilir. (<a href="https://docs.spring.io/spring-boot/3.3/maven-plugin/aot.html">Spring Boot AOT belgeleri</a>)</p>



<h3 class="wp-block-heading">Adım 3: Yerel native executable üretin</h3>



<p class="wp-block-paragraph">Makinede kullandığınız Java sürümüyle uyumlu bir GraalVM dağıtımı ve gerekli native toolchain kurulu olmalıdır. Ardından:</p>



<pre class="wp-block-code"><code>./mvnw -Pnative native:compile</code></pre>



<p class="wp-block-paragraph">Executable varsayılan olarak <code>target</code> dizininde oluşur:</p>



<pre class="wp-block-code"><code>./target/cold-start-demo</code></pre>



<p class="wp-block-paragraph">Servisi doğrulayın:</p>



<pre class="wp-block-code"><code>curl http://localhost:8080/hello</code></pre>



<p class="wp-block-paragraph">Gradle kullanan projelerde GraalVM Native Build Tools plugin’i uygulandıktan sonra eşdeğer komutlar şunlardır:</p>



<pre class="wp-block-code"><code>./gradlew nativeCompile
./build/native/nativeCompile/cold-start-demo</code></pre>



<h3 class="wp-block-heading">Adım 4: Buildpack ile native container image oluşturun</h3>



<p class="wp-block-paragraph">Yerel GraalVM kurulumu yerine Docker ve Cloud Native Buildpacks kullanabilirsiniz:</p>



<pre class="wp-block-code"><code>./mvnw -Pnative spring-boot:build-image</code></pre>



<p class="wp-block-paragraph">Oluşan image’ı çalıştırın:</p>



<pre class="wp-block-code"><code>docker run --rm -p 8080:8080 \
  docker.io/library/cold-start-demo:0.0.1-SNAPSHOT</code></pre>



<p class="wp-block-paragraph">Gradle karşılığı:</p>



<pre class="wp-block-code"><code>./gradlew bootBuildImage</code></pre>



<p class="wp-block-paragraph">Spring Boot, Maven ve Gradle üzerinden Paketo buildpack ile JVM içermeyen native container image üretilmesini destekler. (<a href="https://docs.spring.io/spring-boot/3.4/how-to/native-image/developing-your-first-application.html">Spring Boot native uygulama rehberi</a>)</p>



<p class="wp-block-paragraph">Native Image doğrudan cross-compilation yapmadığından executable hedef işletim sistemi ve CPU mimarisi için üretilmelidir. Örneğin Linux AMD64 ile Linux ARM64 artefact’larını ayrı runner’larda veya uygun multi-platform build altyapısında oluşturmak gerekir.</p>



<h3 class="wp-block-heading">Adım 5: Native test çalıştırın</h3>



<p class="wp-block-paragraph">Normal JVM testleri hızlı geri bildirim sağlar; ancak kapalı dünya varsayımından kaynaklanan sorunları yakalamak için native test de CI sürecine eklenmelidir:</p>



<pre class="wp-block-code"><code>./mvnw -Pnative native:test</code></pre>



<p class="wp-block-paragraph">Gradle için:</p>



<pre class="wp-block-code"><code>./gradlew nativeTest</code></pre>



<p class="wp-block-paragraph">Özellikle şu yolları test edin:</p>



<ul class="wp-block-list">
<li>JSON serialization ve deserialization</li>



<li>Reflection kullanan kod</li>



<li>AOP ve transaction proxy’leri</li>



<li>Dosya, template ve resource bundle erişimi</li>



<li>SSL ve güvenlik sağlayıcıları</li>



<li>ORM entity’leri</li>



<li>Dinamik olarak seçilen implementasyonlar</li>
</ul>



<h3 class="wp-block-heading">Adım 6: Eksik metadata’yı tracing agent ile bulun</h3>



<p class="wp-block-paragraph">Spring’in otomatik hint üretiminin yetmediği eski veya uyumsuz bir kütüphanede uygulamayı önce JVM üzerinde tracing agent ile çalıştırabilirsiniz:</p>



<pre class="wp-block-code"><code>java -Dspring.aot.enabled=true \
  -agentlib:native-image-agent=config-output-dir=target/native-agent \
  -jar target/cold-start-demo.jar</code></pre>



<p class="wp-block-paragraph">Uygulamanın kritik akışlarını çalıştırdıktan sonra oluşan metadata’yı inceleyin. Gerekli dosyalar kontrollü biçimde <code>src/main/resources/META-INF/native-image/</code> altına taşınabilir.</p>



<p class="wp-block-paragraph">Tracing agent yalnızca çalıştırılan yolları görür. Eksiksiz test kapsamı yoksa gerekli metadata’nın bir kısmı yine atlanabilir; üretilen dosyaları körlemesine kabul etmek yerine sadeleştirmek ve doğrulamak gerekir. GraalVM’in agent’ı reflection, proxy, JNI ve resource erişimlerini kaydedebilir. (<a href="https://www.graalvm.org/jdk25/reference-manual/native-image/metadata/AutomaticMetadataCollection/">GraalVM tracing agent rehberi</a>)</p>



<h2 class="wp-block-heading">Üretim ortamı için mimari öneriler</h2>



<p class="wp-block-paragraph">GraalVM Native Image özellikle şu iş yüklerinde güçlü bir seçenektir:</p>



<ul class="wp-block-list">
<li>Trafiği düzensiz serverless fonksiyonlar</li>



<li>Scale-to-zero kullanan servisler</li>



<li>Çok hızlı yatay ölçeklenmesi gereken API’ler</li>



<li>Düşük bellek limitli container’lar</li>



<li>CLI uygulamaları ve kısa ömürlü batch işler</li>



<li>Edge veya yoğun çoklu servis ortamları</li>
</ul>



<p class="wp-block-paragraph">Buna karşılık aşağıdaki durumlarda geleneksel JVM seçeneğini ayrıca benchmark etmek gerekir:</p>



<ul class="wp-block-list">
<li>Günlerce çalışan CPU yoğun servisler</li>



<li>Yoğun dinamik class loading veya plugin mimarisi</li>



<li>Native desteği zayıf üçüncü taraf kütüphaneler</li>



<li>Çok sık değişen runtime bean yapıları</li>



<li>Derleme süresi ve CI kaynak maliyetinin kritik olduğu projeler</li>
</ul>



<p class="wp-block-paragraph">İyi bir cloud-native strateji bütün servisleri zorunlu olarak native’e çevirmek değildir. Cold start ve bellek baskısı yaşayan sınır servislerini native executable olarak çalıştırırken, uzun ömürlü ve throughput odaklı servisleri JVM üzerinde bırakmak daha dengeli olabilir.</p>



<h2 class="wp-block-heading">Sonuç</h2>



<p class="wp-block-paragraph">GraalVM Native Image, Java’nın serverless ve mikroservis mimarilerindeki en görünür dezavantajlarından ikisini doğrudan hedefler: cold start gecikmesi ve yüksek memory footprint.</p>



<p class="wp-block-paragraph">AOT derleme; erişilebilir kodu, gerekli runtime bileşenlerini ve belirli başlangıç durumlarını build aşamasında native executable içine taşır. Böylece uygulama hızlı açılır, warm-up ihtiyacı azalır ve daha küçük bir çalışma zamanı belleğiyle hizmet verebilir.</p>



<p class="wp-block-paragraph">Bu kazanımın karşılığında closed-world assumption nedeniyle reflection, dynamic proxy, serialization ve classpath kaynaklarının daha bilinçli yönetilmesi gerekir. Spring Boot 3’ün AOT altyapısı bu işin büyük bölümünü otomatikleştirir; <code>RuntimeHints</code>, reachability metadata, tracing agent ve native testler ise kalan boşlukları kapatır.</p>



<p class="wp-block-paragraph">Doğru karar için son adım her zaman aynıdır: JVM ve native sürümü gerçek trafik profiliniz, hedef container limitleriniz ve ölçeklenme modeliniz altında ölçün. Cold start kritikse GraalVM Native Image çoğu zaman yalnızca bir optimizasyon değil, doğrudan mimari bir avantajdır.</p>



<h2 class="wp-block-heading">Sık Sorulan Sorular</h2>



<h3 class="wp-block-heading">GraalVM Native Image tamamen JVM’siz mi çalışır?</h3>



<p class="wp-block-paragraph">Üretilen executable’ın yanında ayrı bir HotSpot JVM bulunmaz. Ancak binary; garbage collector, thread desteği ve gerekli çalışma zamanı servisleri gibi Native Image runtime bileşenlerini içerir.</p>



<h3 class="wp-block-heading">Native Image her Java uygulamasında daha hızlı mıdır?</h3>



<p class="wp-block-paragraph">Hayır. Başlangıç süresi genellikle çok daha iyidir; fakat uzun süre çalışan ve CPU yoğun uygulamalarda JIT daha yüksek tepe throughput sağlayabilir. Karar benchmark sonucuna göre verilmelidir.</p>



<h3 class="wp-block-heading">Spring Boot 3 reflection sorunlarını otomatik çözer mi?</h3>



<p class="wp-block-paragraph">Spring AOT birçok yaygın kullanım için gerekli hint’leri otomatik üretir. Uygulamaya özel reflection, dinamik proxy veya üçüncü taraf kütüphane kullanımlarında ek runtime hint gerekebilir.</p>



<h3 class="wp-block-heading">Native executable bir kez üretilip her platformda çalıştırılabilir mi?</h3>



<p class="wp-block-paragraph">Hayır. Binary hedef işletim sistemi ve CPU mimarisine özeldir. Linux, macOS, AMD64 ve ARM64 hedefleri için uygun ortamlarda ayrı build alınmalıdır.</p>



<h3 class="wp-block-heading">GraalVM Native Image serverless maliyetini azaltır mı?</h3>



<p class="wp-block-paragraph">Daha hızlı startup ve düşük bellek tüketimi, platformun fiyatlandırma modeline bağlı olarak süre ve kaynak maliyetini azaltabilir. Kesin kazanç ancak gerçek provider, trafik ve bellek ayarlarıyla yapılan ölçüm sonucunda belirlenebilir.</p>



<p class="wp-block-paragraph"></p>
<p><a href="https://alikarahisar.com/blog/graalvm-native-image-cold-start-spring-boot-3/">GraalVM Native Image ile Serverless ve Mikroservislerde Cold Start Sorununu Çözmek</a> yazısı ilk önce <a href="https://alikarahisar.com/blog">Ali Karahisar</a> üzerinde ortaya çıktı.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://alikarahisar.com/blog/graalvm-native-image-cold-start-spring-boot-3/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Garbage Collector Seçimi ve Akort Edilmesi: G1 GC vs. ZGC</title>
		<link>https://alikarahisar.com/blog/garbage-collector-secimi-g1-gc-vs-zgc/</link>
					<comments>https://alikarahisar.com/blog/garbage-collector-secimi-g1-gc-vs-zgc/#respond</comments>
		
		<dc:creator><![CDATA[Ali Karahisar]]></dc:creator>
		<pubDate>Wed, 02 Sep 2026 19:05:32 +0000</pubDate>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[Yazılım]]></category>
		<category><![CDATA[G1 GC]]></category>
		<category><![CDATA[Garbage Collector seçimi]]></category>
		<category><![CDATA[GC pause time]]></category>
		<category><![CDATA[Generational ZGC]]></category>
		<category><![CDATA[Java performans optimizasyonu]]></category>
		<category><![CDATA[JVM flags]]></category>
		<category><![CDATA[JVM tuning]]></category>
		<category><![CDATA[large heap]]></category>
		<category><![CDATA[low latency Java]]></category>
		<category><![CDATA[ZGC]]></category>
		<guid isPermaLink="false">https://alikarahisar.com/blog/?p=39</guid>

					<description><![CDATA[<p>Java uygulamalarında Garbage Collector seçimi yalnızca bellek yönetimiyle ilgili teknik bir ayrıntı değildir. GC; API yanıt sürelerini, işlem hacmini, altyapı maliyetini ve sistemin yoğun trafik altındaki kararlılığını doğrudan etkiler. Özellikle onlarca veya yüzlerce gigabaytlık heap kullanan sistemlerde ortalama GC süresine bakmak yeterli değildir. Kullanıcı deneyimini ve servis seviyesi hedeflerini çoğunlukla p99, p99.9 ve p99.99 gecikmeleri [&#8230;]</p>
<p><a href="https://alikarahisar.com/blog/garbage-collector-secimi-g1-gc-vs-zgc/">Garbage Collector Seçimi ve Akort Edilmesi: G1 GC vs. ZGC</a> yazısı ilk önce <a href="https://alikarahisar.com/blog">Ali Karahisar</a> üzerinde ortaya çıktı.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Java uygulamalarında Garbage Collector seçimi yalnızca bellek yönetimiyle ilgili teknik bir ayrıntı değildir. GC; API yanıt sürelerini, işlem hacmini, altyapı maliyetini ve sistemin yoğun trafik altındaki kararlılığını doğrudan etkiler.</p>



<span id="more-39"></span>



<p class="wp-block-paragraph">Özellikle onlarca veya yüzlerce gigabaytlık heap kullanan sistemlerde ortalama GC süresine bakmak yeterli değildir. Kullanıcı deneyimini ve servis seviyesi hedeflerini çoğunlukla p99, p99.9 ve p99.99 gecikmeleri belirler. Bu nedenle doğru soru “Hangi GC daha hızlı?” değil, “Bu iş yükünün gecikme, throughput ve kaynak tüketimi hedefleri için hangi GC daha uygun?” olmalıdır.</p>



<p class="wp-block-paragraph">G1 GC, dengeli throughput ve öngörülebilir duraklamalar sağlamayı amaçlayan genel kullanıma yönelik bir collector’dır. ZGC ise daha fazla işi uygulamayla eş zamanlı yürüterek çok büyük heap’lerde dahi ultra düşük duraklama sürelerini hedefler.</p>



<p class="wp-block-paragraph">Oracle’ın güncel sınıflandırmasına göre G1 “mostly concurrent”, ZGC ise “fully concurrent” bir collector’dır. Oracle, yanıt süresinin en yüksek öncelik olduğu sistemlerde ZGC’yi başlangıç seçeneği olarak önermektedir. Bununla birlikte ZGC’nin düşük duraklama avantajı, iş yüküne bağlı CPU ve bellek maliyetleriyle birlikte değerlendirilmelidir. <a href="https://docs.oracle.com/en/java/javase/25/gctuning/available-collectors.html" target="_blank" rel="noreferrer noopener">Oracle Java 25 GC seçim rehberi</a></p>



<h2 class="wp-block-heading">Kısa cevap: G1 GC mi, ZGC mi?</h2>



<p class="wp-block-paragraph">Aşağıdaki yaklaşım pratik bir başlangıç noktasıdır:</p>



<ul class="wp-block-list">
<li>Dengeli throughput, makul bellek tüketimi ve yüzlerce milisaniyenin altında tutulmak istenen GC duraklamaları için G1 GC güçlü bir varsayılan seçimdir.</li>



<li>Onlarca ya da yüzlerce gigabaytlık heap, yüksek canlı veri miktarı ve tek haneli milisaniyelik uçtan uca gecikme hedefleri varsa ZGC öncelikli adaydır.</li>



<li>CPU kapasitesi sınırlıysa veya en önemli hedef maksimum işlem hacmiyse, ZGC’nin concurrent çalışma maliyeti mutlaka ölçülmelidir.</li>



<li>GC değişikliği sentetik mikro benchmark sonucuyla değil, üretime benzeyen yük, veri dağılımı ve allocation profiliyle doğrulanmalıdır.</li>
</ul>



<p class="wp-block-paragraph">ZGC düşük gecikme için güçlü bir mimari avantaj sağlar; fakat gerçek zamanlı bir collector değildir ve uygulama için kesin bir SLA garantisi vermez. İşletim sistemi zamanlaması, safepoint işlemleri, JIT derlemesi, disk erişimi, sayfa hataları, kilit rekabeti ve dış servisler de tail latency üretebilir.</p>



<h2 class="wp-block-heading">G1 GC nasıl çalışır?</h2>



<p class="wp-block-paragraph">Garbage-First Garbage Collector, heap’i eşit büyüklükte bölgelere ayırır. Bu bölgeler sabit biçimde Eden, Survivor veya Old olarak ayrılmış değildir; ihtiyaç oldukça farklı roller üstlenebilir.</p>



<p class="wp-block-paragraph">G1; generational, bölge tabanlı, evacuating ve çoğunlukla eş zamanlı çalışan bir collector’dır. Global canlılık analizi gibi pahalı işlerin bir bölümünü uygulama thread’leri çalışırken gerçekleştirir. Buna karşılık canlı nesnelerin seçilmiş bölgelerden başka bölgelere kopyalanması ağırlıklı olarak Stop-The-World duraklamaları içinde yapılır.</p>



<p class="wp-block-paragraph">Oracle’ın tanımına göre G1, belirlenen pause-time hedefine yüksek olasılıkla ulaşmaya çalışır; ancak bu hedef her GC duraklaması için kesin bir üst sınır değildir. <a href="https://docs.oracle.com/en/java/javase/25/gctuning/garbage-first-g1-garbage-collector1.html" target="_blank" rel="noreferrer noopener">Oracle G1 GC mimarisi</a></p>



<h3 class="wp-block-heading">G1 GC döngüsü</h3>



<p class="wp-block-paragraph">G1’in çalışma modeli iki ana dönem halinde düşünülebilir.</p>



<h4 class="wp-block-heading">1. Young-only dönemi</h4>



<p class="wp-block-paragraph">Yeni nesilde biriken nesneler young collection ile toplanır. Yaşamaya devam eden nesneler Survivor bölgelerine veya yaşlarına göre Old Generation’a taşınır.</p>



<p class="wp-block-paragraph">Young collection sırasında uygulama thread’leri durdurulur. G1, bir sonraki young generation boyutunu geçmiş duraklama sürelerinden ve kopyalanan canlı nesne miktarından yararlanarak hesaplar.</p>



<h4 class="wp-block-heading">2. Concurrent marking ve space-reclamation dönemi</h4>



<p class="wp-block-paragraph">Old Generation doluluğu Initiating Heap Occupancy eşiğine ulaştığında G1 concurrent marking döngüsünü başlatır:</p>



<ol start="1" class="wp-block-list">
<li>Bir Concurrent Start young collection yapılır.</li>



<li>Old Generation içindeki erişilebilir nesneler uygulamayla eş zamanlı işaretlenir.</li>



<li>Remark duraklamasında işaretleme sonuçlandırılır.</li>



<li>Cleanup aşamasında bölgelerin canlılık oranları hesaplanır.</li>



<li>Mixed collection’larda young bölgelerle birlikte seçilmiş old bölgeler de boşaltılır.</li>
</ol>



<p class="wp-block-paragraph">“Garbage-First” adı, collector’ın önce en fazla çöp içeren ve en verimli biçimde geri kazanılabilecek bölgeleri seçmesinden gelir.</p>



<h3 class="wp-block-heading">G1’in pause-time modeli</h3>



<p class="wp-block-paragraph"><code>-XX:MaxGCPauseMillis</code> bir zaman sınırı değil, ergonomik bir hedeftir. G1, collection set’e eklenecek bölgelerin sayısını ve young generation büyüklüğünü bu hedefe göre ayarlamaya çalışır.</p>



<p class="wp-block-paragraph">Örneğin 100 milisaniyelik hedef vermek, hiçbir GC duraklamasının 100 milisaniyeyi geçmeyeceği anlamına gelmez. Çok yüksek canlı nesne oranı, ani allocation artışı, reference processing, remembered set taraması veya humongous object yoğunluğu hedefin aşılmasına neden olabilir.</p>



<h3 class="wp-block-heading">Remembered set ve card table maliyeti</h3>



<p class="wp-block-paragraph">G1, bölgeler arasındaki referansları bulabilmek için card table ve remembered set yapılarından yararlanır. Uygulama bir referans alanını değiştirdiğinde write barrier ilgili kartı işaretler. Böylece young collection sırasında bütün Old Generation’ın taranması gerekmez.</p>



<p class="wp-block-paragraph">Bu mekanizma toplama işini sınırlar; ancak uygulama thread’lerine barrier maliyeti, ek metadata tüketimi ve remembered set bakım yükü getirir. Nesne grafiğinde çok yoğun bölge geçişleri bulunan sistemlerde bu maliyet görünür hale gelebilir.</p>



<h3 class="wp-block-heading">Evacuation ve Full GC riski</h3>



<p class="wp-block-paragraph">G1, canlı nesneleri seçilmiş bölgelerden boş alanlara taşıyarak belleği sıkıştırır. Taşıma için yeterli boş bölge kalmazsa evacuation failure oluşabilir.</p>



<p class="wp-block-paragraph">Concurrent marking allocation hızına yetişemez ve yeterli alan geri kazanılamazsa G1, son çare olarak Stop-The-World Full GC gerçekleştirebilir. Büyük heap’lerde bu durum saniyelere ulaşabilen ciddi latency sıçramaları oluşturabilir.</p>



<h3 class="wp-block-heading">Humongous object davranışı</h3>



<p class="wp-block-paragraph">Boyutu bir G1 bölgesinin yarısından büyük olan nesneler “humongous” olarak değerlendirilir ve ardışık bölgelerde tutulur. Büyük byte dizileri, sıkıştırılmış mesajlar, cache blokları ve büyük JSON payload’ları bu kategoriye girebilir.</p>



<p class="wp-block-paragraph">Humongous allocation:</p>



<ul class="wp-block-list">
<li>Heap parçalanmasını artırabilir.</li>



<li>Concurrent marking döngüsünü erken başlatabilir.</li>



<li>Ardışık boş bölge ihtiyacı doğurabilir.</li>



<li>Full GC ve hatta <code>OutOfMemoryError</code> riskini yükseltebilir.</li>
</ul>



<p class="wp-block-paragraph">GC loglarında humongous region sayısı yüksekse ilk çözüm JVM bayrağı eklemek değil, büyük allocation kaynaklarını uygulama seviyesinde incelemek olmalıdır.</p>



<h2 class="wp-block-heading">ZGC nasıl çalışır?</h2>



<p class="wp-block-paragraph">ZGC, ölçeklenebilir ve düşük gecikme odaklı, concurrent bir collector’dır. Marking, reference processing ve nesne relocation gibi pahalı işlerin büyük bölümünü uygulama thread’leri çalışırken gerçekleştirir.</p>



<p class="wp-block-paragraph">Oracle’ın güncel rehberine göre ZGC’nin duraklama süreleri kullanılan heap boyutundan bağımsızdır ve collector birkaç yüz megabayttan 16 TB’a kadar heap’lerde çalışabilir. JDK 25 dokümantasyonu maksimum duraklamaları bir milisaniyenin altında hedefleyen çalışma modelini açıklamaktadır. <a href="https://docs.oracle.com/en/java/javase/24/gctuning/z-garbage-collector.html" target="_blank" rel="noreferrer noopener">Oracle ZGC rehberi</a></p>



<p class="wp-block-paragraph">Buradaki kritik ifade “heap boyutundan bağımsız” olmasıdır. Bu, heap büyüdükçe concurrent toplam iş miktarının hiç artmadığı anlamına gelmez. Artan canlı veri daha fazla CPU zamanı ve daha uzun concurrent cycle gerektirebilir. Mimari avantaj, pahalı işin Stop-The-World penceresine taşınmamasıdır.</p>



<h3 class="wp-block-heading">Generational ZGC ve JDK sürümü</h3>



<p class="wp-block-paragraph">ZGC’nin nesilleri kullanan sürümü JDK 21 ile kullanıma sunuldu. Generational ZGC, çoğu nesnenin genç yaşta öldüğü gözleminden yararlanarak young ve old nesilleri farklı sıklıklarda toplar.</p>



<p class="wp-block-paragraph">Sürüm davranışı önemlidir:</p>



<ul class="wp-block-list">
<li>JDK 21 ve 22’de generational mod <code>-XX:+ZGenerational</code> ile etkinleştirilir.</li>



<li>JDK 23’te generational mod varsayılan hale gelmiştir.</li>



<li>JDK 24 itibarıyla non-generational ZGC kaldırılmış ve <code>ZGenerational</code> seçeneğine ihtiyaç kalmamıştır.</li>
</ul>



<p class="wp-block-paragraph">Dolayısıyla internetten kopyalanan eski bir JVM başlangıç satırı yeni JDK’da geçersiz olabilir. Bayraklar hedef JDK dağıtımı ve sürümü üzerinde doğrulanmalıdır. <a href="https://docs.oracle.com/en/java/javase/25/migrate/significant-changes-jdk-21.html" target="_blank" rel="noreferrer noopener">Oracle JDK 21 değişiklikleri</a> ve <a href="https://docs.oracle.com/en/java/javase/25/migrate/significant-changes-jdk-24.html" target="_blank" rel="noreferrer noopener">Oracle JDK 24 değişiklikleri</a></p>



<h3 class="wp-block-heading">Colored pointers</h3>



<p class="wp-block-paragraph">ZGC’nin temel yapı taşlarından biri colored pointer yaklaşımıdır. Nesne referansları yalnızca bellek adresi taşımaz; marking ve relocation durumuyla ilgili metadata da içerir.</p>



<p class="wp-block-paragraph">Bu bilgi sayesinde JVM bir referansın:</p>



<ul class="wp-block-list">
<li>Geçerli GC görünümüne ait olup olmadığını,</li>



<li>İşaretleme işlemi gerektirip gerektirmediğini,</li>



<li>Nesnenin taşınmış olup olmadığını,</li>



<li>Referansın güncellenmesi gerekip gerekmediğini</li>
</ul>



<p class="wp-block-paragraph">kontrol edebilir.</p>



<p class="wp-block-paragraph">Colored pointer yaklaşımı, ZGC’nin nesneleri uygulama thread’leri çalışırken taşıyabilmesinin önemli nedenlerinden biridir.</p>



<h3 class="wp-block-heading">Load ve store barrier’lar</h3>



<p class="wp-block-paragraph">Uygulama bir nesne referansını okuduğunda veya güncellediğinde JIT tarafından üretilen barrier kodu devreye girebilir. Barrier, referansın GC açısından güvenli ve güncel olmasını sağlar.</p>



<p class="wp-block-paragraph">Bir nesne concurrent relocation sırasında başka bir konuma taşınmışsa barrier doğru konumu bulabilir ve referansı düzeltebilir. Bu nedenle bütün uygulama thread’lerini durdurup heap genelindeki tüm referansları tek seferde güncellemek gerekmez.</p>



<p class="wp-block-paragraph">Generational ZGC ayrıca young ve old nesiller arasındaki referansları verimli biçimde izlemek için optimize edilmiş barrier mekanizmaları kullanır. Bunun karşılığı, uygulamanın normal nesne erişim yollarında belirli bir CPU maliyetidir.</p>



<h3 class="wp-block-heading">Concurrent marking</h3>



<p class="wp-block-paragraph">ZGC, erişilebilir nesneleri uygulama çalışırken işaretler. Kısa Stop-The-World evreleri temel olarak kök durumunu güvenli biçimde yakalamak ve bazı faz geçişlerini koordine etmek için kullanılır.</p>



<p class="wp-block-paragraph">Duraklamalarda bütün canlı nesne kümesini dolaşmak yerine sınırlı ve heap boyutuna doğrudan bağlı olmayan işler yapılması, large heap sistemlerde ZGC’nin en önemli avantajıdır.</p>



<h3 class="wp-block-heading">Concurrent relocation</h3>



<p class="wp-block-paragraph">ZGC, seçilen bölgelerdeki canlı nesneleri uygulama çalışırken taşıyabilir. Uygulama eski konumu gösteren bir referansla karşılaştığında barrier mekanizması erişimi doğru adrese yönlendirir.</p>



<p class="wp-block-paragraph">Böylece:</p>



<ul class="wp-block-list">
<li>Heap concurrent biçimde sıkıştırılabilir.</li>



<li>Uzun bir Stop-The-World compaction aşaması önlenebilir.</li>



<li>Büyük canlı veri kümelerinde pause süresinin heap boyutuyla büyümesi engellenebilir.</li>



<li>Ayrı ve uzun süren klasik Full GC ihtiyacı ortadan kaldırılabilir.</li>
</ul>



<h2 class="wp-block-heading">Large heap sistemlerde ZGC’nin mimari avantajları</h2>



<h3 class="wp-block-heading">1. Duraklama süresi heap boyutuyla ölçeklenmez</h3>



<p class="wp-block-paragraph">G1’de concurrent marking heap genelinde çalışsa da canlı nesnelerin tahliyesi ve collection set işleme gibi önemli işler Stop-The-World aşamalarında yapılır. Heap ve canlı veri büyüdükçe bu aşamaların kuyruk gecikmesine etkisi artabilir.</p>



<p class="wp-block-paragraph">ZGC’de pahalı marking ve relocation işleri concurrent yürütüldüğü için 32 GB’tan 256 GB’a geçiş, GC pause süresini aynı oranda büyütmez. Büyük in-memory cache, risk hesaplama motoru, fiyatlama sistemi, oyun sunucusu veya gerçek zamanlı analitik platformlarında bu özellik belirleyicidir.</p>



<h3 class="wp-block-heading">2. Concurrent compaction</h3>



<p class="wp-block-paragraph">Low-pause collector’ların yalnızca erişilemeyen nesneleri bulması yetmez; fragmentation sorununu da uzun duraklama üretmeden çözmesi gerekir. ZGC’nin concurrent relocation yaklaşımı bellek sıkıştırmayı uygulama çalışırken gerçekleştirebilir.</p>



<h3 class="wp-block-heading">3. Daha düşük tail-latency riski</h3>



<p class="wp-block-paragraph">Ortalama yanıt süresi iyi olsa bile uzun bir Full GC p99.9 değerini bozabilir. ZGC’nin tasarımı bu tür büyük GC kaynaklı sıçramaları azaltmaya yöneliktir.</p>



<p class="wp-block-paragraph">Bununla birlikte “GC pause düştü” sonucu, uçtan uca latency’nin otomatik olarak düştüğü anlamına gelmez. Concurrent GC thread’leri CPU üzerinde uygulamayla rekabet ederse servis süresi artabilir. Bu nedenle GC pause, CPU utilization ve request latency birlikte izlenmelidir.</p>



<h3 class="wp-block-heading">4. Allocation spike’larına karşı heap headroom</h3>



<p class="wp-block-paragraph">ZGC concurrent çalıştığı için GC bir cycle’ı tamamlayana kadar uygulama yeni nesne ayırmaya devam eder. Maksimum heap; canlı veri kümesini, cycle boyunca yapılacak allocation’ları ve trafik sıçramalarını karşılayacak boşluğu içermelidir.</p>



<p class="wp-block-paragraph">Örneğin canlı veri kümesi 40 GB olan bir servise yalnızca 42 GB heap vermek, teorik olarak yeterli görünse de yüksek allocation rate altında allocation stall yaratabilir. ZGC tuning’in en önemli parametresi çoğu zaman karmaşık bir bayrak değil, doğru <code>-Xmx</code> ve yeterli headroom’dur.</p>



<h3 class="wp-block-heading">5. Adaptif generational yönetim</h3>



<p class="wp-block-paragraph">Modern ZGC, generational yapıda nesil boyutlarını, GC thread sayısını ve tenuring eşiklerini iş yüküne göre dinamik biçimde ayarlar. Bu nedenle başlangıç yaklaşımı az sayıda bayrak kullanmak ve yalnızca ölçüm sonucunda müdahale etmek olmalıdır.</p>



<h2 class="wp-block-heading">G1 GC ve ZGC çalışma mekanizmalarının karşılaştırması</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><th>Teknik özellik</th><th>G1 GC</th><th>ZGC</th></tr><tr><td>Temel hedef</td><td>Throughput ve makul pause süresi dengesi</td><td>Ultra düşük GC pause ve ölçeklenebilirlik</td></tr><tr><td>Çalışma modeli</td><td>Mostly concurrent</td><td>Pahalı GC işlerinde fully concurrent yaklaşım</td></tr><tr><td>Heap organizasyonu</td><td>Region tabanlı, generational</td><td>Region tabanlı, JDK 24+ yalnızca generational</td></tr><tr><td>Marking</td><td>Büyük ölçüde concurrent; başlangıç, remark ve cleanup duraklamaları içerir</td><td>Büyük ölçüde concurrent, kısa faz geçişleri içerir</td></tr><tr><td>Nesne taşıma</td><td>Collection set evacuation işlemi Stop-The-World</td><td>Relocation büyük ölçüde concurrent</td></tr><tr><td>Referans takibi</td><td>Card table, remembered set ve write barrier</td><td>Colored pointers ile load/store barrier’lar</td></tr><tr><td>Pause hedefi</td><td><code>MaxGCPauseMillis</code> ile ergonomik hedef</td><td>Heap boyutundan bağımsız, çok kısa pause tasarımı</td></tr><tr><td>Full GC riski</td><td>Allocation veya evacuation failure durumunda uzun Full GC oluşabilir</td><td>Klasik uzun Full GC ihtiyacını concurrent relocation ile önlemeyi amaçlar</td></tr><tr><td>Throughput</td><td>Genellikle daha yüksek</td><td>Concurrent çalışma ve barrier’lar nedeniyle daha düşük olabilir</td></tr><tr><td>CPU gereksinimi</td><td>Dengeli; pause sırasında yüksek paralellik</td><td>GC cycle boyunca uygulamayla paylaşılacak CPU headroom ister</td></tr><tr><td>Heap headroom ihtiyacı</td><td>Orta; reserve alan yine kritiktir</td><td>Concurrent cycle sırasında allocation için daha yüksek boşluk gerekebilir</td></tr><tr><td>Large heap uygunluğu</td><td>Onlarca GB ve üzeri heap’lerde kullanılabilir</td><td>Yüzlerce GB ve TB ölçeğinde özellikle güçlüdür</td></tr><tr><td>Tuning yaklaşımı</td><td>Pause hedefi, IHOP, reserve ve region davranışı incelenebilir</td><td>Öncelikle heap boyutu ve allocation headroom ayarlanır</td></tr><tr><td>Operasyonel olgunluk</td><td>Uzun süredir varsayılan ve yaygın</td><td>Üretime hazır; sürüme özgü generational geçişi dikkate alınmalı</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Prod ortamında GC seçim kriterleri</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Kriter</td><td>G1 GC lehine durum</td><td>ZGC lehine durum</td><td>Ölçülmesi gereken metrik</td></tr><tr><td>Gecikme SLO’su</td><td>50–300 ms düzeyindeki GC duraklamaları tolere edilebiliyor</td><td>Tek haneli milisaniyelik tail-latency kritik</td><td>p99, p99.9, maksimum pause</td></tr><tr><td>Heap büyüklüğü</td><td>Orta ve büyük heap</td><td>Çok büyük heap veya yüksek live-set</td><td>Heap occupancy, live-set</td></tr><tr><td>Throughput</td><td>Maksimum işlem hacmi öncelikli</td><td>Bir miktar throughput maliyeti kabul edilebilir</td><td>İşlem/saniye, CPU/request</td></tr><tr><td>Allocation rate</td><td>Düşük veya orta</td><td>Yüksek fakat yeterli CPU ve heap headroom mevcut</td><td>MB/s veya GB/s allocation</td></tr><tr><td>CPU kapasitesi</td><td>CPU limiti sıkı</td><td>Concurrent GC için rezerv CPU bulunuyor</td><td>CPU saturation, steal time</td></tr><tr><td>Bellek maliyeti</td><td>Sıkı bellek limiti</td><td>Latency için daha fazla headroom ayrılabilir</td><td>RSS, committed heap, native memory</td></tr><tr><td>Trafik yapısı</td><td>Öngörülebilir ve dengeli</td><td>Ani allocation artışları yaşanıyor</td><td>Allocation spike, burst süresi</td></tr><tr><td>Büyük nesneler</td><td>Humongous allocation düşük</td><td>Büyük nesneler G1’de fragmentation yaratıyor</td><td>Humongous region sayısı</td></tr><tr><td>Konteyner limiti</td><td>Küçük ve sıkı pod limitleri</td><td>Büyük, CPU garantili instance veya pod</td><td>cgroup memory/CPU limitleri</td></tr><tr><td>Operasyonel deneyim</td><td>Ekip G1 loglarını ve davranışını iyi tanıyor</td><td>Ekip ZGC benchmark ve kapasite testlerini yapabiliyor</td><td>Incident ve tuning tecrübesi</td></tr><tr><td>JDK sürümü</td><td>Eski veya kurumsal olarak standartlaştırılmış sürüm</td><td>Güncel LTS veya JDK 24+ kullanılabiliyor</td><td>Dağıtım, sürüm ve flag uyumluluğu</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Temel JVM bayrakları ve tuning örnekleri</h2>



<p class="wp-block-paragraph">Aşağıdaki değerler doğrudan üretime kopyalanacak reçeteler değildir. Heap büyüklüğü, CPU sayısı, NUMA yapısı, container limitleri ve allocation profili test edilmeden kesinleştirilmemelidir.</p>



<h3 class="wp-block-heading">G1 GC için güvenli başlangıç profili</h3>



<pre class="wp-block-code"><code>java \
  -XX:+UseG1GC \
  -Xms32g \
  -Xmx32g \
  -XX:MaxGCPauseMillis=100 \
  -XX:+AlwaysPreTouch \
  -Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100M \
  -jar application.jar</code></pre>



<p class="wp-block-paragraph">Bayrakların amacı:</p>



<ul class="wp-block-list">
<li><code>-XX:+UseG1GC</code>: G1 collector’ı etkinleştirir.</li>



<li><code>-Xms32g -Xmx32g</code>: Heap resizing ve çalışma anındaki sayfa tahsis sürprizlerini azaltır.</li>



<li><code>-XX:MaxGCPauseMillis=100</code>: G1’e 100 ms’lik ergonomik pause hedefi verir.</li>



<li><code>-XX:+AlwaysPreTouch</code>: Heap sayfalarını başlangıçta fiziksel belleğe dokundurur; açılışı uzatabilir fakat runtime latency varyansını azaltabilir.</li>



<li><code>-Xlog:gc*,safepoint</code>: GC ve safepoint davranışını analiz etmeyi sağlar.</li>
</ul>



<p class="wp-block-paragraph">G1 için young generation’ı <code>-Xmn</code> ile sabitlemek genellikle önerilmez. G1, pause hedefine ulaşmak için young generation boyutunu adaptif biçimde kullanır; sabit young generation bu kontrol mekanizmasını zayıflatabilir. Oracle’ın önerisi, önce yalnızca heap boyutu ve gerekiyorsa pause hedefiyle başlamaktır. <a href="https://docs.oracle.com/en/java/javase/22/gctuning/garbage-first-garbage-collector-tuning.html">Oracle G1 tuning rehberi</a></p>



<h3 class="wp-block-heading">G1’de Full GC riski görülüyorsa</h3>



<p class="wp-block-paragraph">GC loglarında <code>Pause Full (G1 Compaction Pause)</code> veya evacuation failure görülmesi durumunda aşağıdaki seçenekler kontrollü olarak değerlendirilebilir:</p>



<pre class="wp-block-code"><code>-XX:G1ReservePercent=20
-XX:ConcGCThreads=8</code></pre>



<p class="wp-block-paragraph"><code>G1ReservePercent</code>, allocation ve evacuation için daha fazla güvenlik alanı bırakabilir. <code>ConcGCThreads</code>, marking’in allocation hızına yetişemediği doğrulandıysa artırılabilir. Her ikisi de maliyetsiz değildir: kullanılabilir heap alanını veya uygulamaya kalan CPU’yu azaltabilir.</p>



<p class="wp-block-paragraph">Adaptive IHOP tahminleri iş yükündeki ani değişikliklere yetişemiyorsa, son çare olarak işaretlemenin daha erken başlatılması denenebilir:</p>



<pre class="wp-block-code"><code>-XX:-G1UseAdaptiveIHOP
-XX:InitiatingHeapOccupancyPercent=30</code></pre>



<p class="wp-block-paragraph">Bu ayar statik davranış üretir. Sezonsal veya burst karakterli iş yüklerinde adaptif mekanizmadan daha kötü sonuç verebileceği için uzun süreli karşılaştırmalı test gerektirir.</p>



<h3 class="wp-block-heading">Çok sayıda yinelenen String için</h3>



<p class="wp-block-paragraph">Heap dump ve profiler, yüksek miktarda yinelenen <code>String</code> bulunduğunu gösteriyorsa:</p>



<pre class="wp-block-code"><code>-XX:+UseStringDeduplication</code></pre>



<p class="wp-block-paragraph">Bu seçenek bellek tüketimini azaltabilir; ancak concurrent deduplication işleminin CPU maliyeti vardır. Kanıt olmadan varsayılan tuning listesine eklenmemelidir.</p>



<h3 class="wp-block-heading">JDK 24+ ZGC düşük gecikme profili</h3>



<pre class="wp-block-code"><code>java \
  -XX:+UseZGC \
  -Xms64g \
  -Xmx64g \
  -XX:+AlwaysPreTouch \
  -Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100M \
  -jar application.jar</code></pre>



<p class="wp-block-paragraph"><code>-Xms</code> ile <code>-Xmx</code> değerlerinin eşitlenmesi:</p>



<ul class="wp-block-list">
<li>Heap’in çalışma sırasında büyüme ihtiyacını azaltır.</li>



<li>ZGC’nin belleği işletim sistemine iade etmesini fiilen sınırlar.</li>



<li>Commit/uncommit ve page fault kaynaklı latency varyansını azaltabilir.</li>



<li>Başlangıç süresini ve kalıcı bellek tüketimini artırır.</li>
</ul>



<p class="wp-block-paragraph">Ultra düşük gecikme hedeflerinde bu değiş tokuş çoğu zaman anlamlıdır; bellek yoğunluğu öncelikli ortamlarda ise esnek heap daha uygun olabilir.</p>



<h3 class="wp-block-heading">Esnek bellek kullanan ZGC profili</h3>



<pre class="wp-block-code"><code>java \
  -XX:+UseZGC \
  -Xms16g \
  -Xmx64g \
  -XX:SoftMaxHeapSize=48g \
  -XX:ZUncommitDelay=300 \
  -Xlog:gc*,safepoint:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100M \
  -jar application.jar</code></pre>



<p class="wp-block-paragraph"><code>SoftMaxHeapSize</code>, ZGC’nin normal koşullarda altında kalmaya çalışacağı yumuşak sınırdır. Collector, allocation stall riskini azaltmak için gerekirse <code>-Xmx</code> değerine kadar büyüyebilir.</p>



<p class="wp-block-paragraph">Örnekte ZGC 48 GB altında kalmaya çalışır; ancak trafik sıçramasında 64 GB’a kadar çıkabilir. Bu, bellek maliyetiyle burst dayanıklılığı arasında kullanışlı bir denge sağlar.</p>



<h3 class="wp-block-heading">JDK 21 ve 22’de Generational ZGC</h3>



<pre class="wp-block-code"><code>java \
  -XX:+UseZGC \
  -XX:+ZGenerational \
  -Xms64g \
  -Xmx64g \
  -XX:+AlwaysPreTouch \
  -jar application.jar</code></pre>



<p class="wp-block-paragraph">Bu örnekteki <code>-XX:+ZGenerational</code> yalnızca ilgili eski sürüm davranışı için kullanılmalıdır. JDK 24 ve sonrasında bu seçenek kaldırılmıştır.</p>



<h3 class="wp-block-heading">ZGC’de thread sayısını değiştirmek</h3>



<p class="wp-block-paragraph">ZGC, GC thread sayısını adaptif olarak ayarlayabilir. Bu nedenle <code>-XX:ConcGCThreads</code> başlangıç profilinde sabitlenmemelidir.</p>



<p class="wp-block-paragraph">GC allocation hızına yetişemiyor, allocation stall oluşuyor ve sistemde boş CPU bulunuyorsa kontrollü bir deney yapılabilir:</p>



<pre class="wp-block-code"><code>-XX:ConcGCThreads=12</code></pre>



<p class="wp-block-paragraph">Değer çok düşükse collector geride kalabilir. Çok yüksekse GC, uygulama thread’lerinin CPU’sunu tüketerek request latency ve throughput’u bozabilir.</p>



<h3 class="wp-block-heading">Explicit large pages</h3>



<p class="wp-block-paragraph">Büyük ve sabit heap’lerde, işletim sistemi önceden doğru biçimde hazırlanmışsa:</p>



<pre class="wp-block-code"><code>-XX:+UseLargePages</code></pre>



<p class="wp-block-paragraph">Large pages, TLB baskısını azaltarak throughput ve latency’ye yardımcı olabilir. Ancak host üzerinde yeterli sayıda sayfa ayrılmadan yalnızca JVM bayrağını eklemek yeterli değildir.</p>



<p class="wp-block-paragraph">Transparent Huge Pages ile explicit large pages aynı şey değildir. Oracle, latency-sensitive ZGC sistemlerinde Transparent Huge Pages kullanımının istenmeyen latency sıçramaları oluşturabileceğini belirtmektedir. Bu nedenle kernel ayarı, JVM konfigürasyonu ve gerçek workload birlikte test edilmelidir. <a href="https://docs.oracle.com/en/java/javase/24/gctuning/z-garbage-collector.html">Oracle ZGC large pages açıklaması</a></p>



<h2 class="wp-block-heading">GC loglarında hangi sinyaller izlenmeli?</h2>



<p class="wp-block-paragraph">Collector seçiminden bağımsız olarak aşağıdaki metrikler birlikte değerlendirilmelidir:</p>



<ul class="wp-block-list">
<li>GC pause p50, p95, p99 ve maksimum değerleri</li>



<li>Young, mixed, old ve full collection sayısı</li>



<li>GC cycle süresi ve cycle sıklığı</li>



<li>Allocation rate</li>



<li>Promotion rate</li>



<li>Collection sonrası heap occupancy</li>



<li>Canlı veri kümesi büyüklüğü</li>



<li>Allocation stall sayısı ve süresi</li>



<li>Application throughput</li>



<li>CPU kullanımı ve CPU throttling</li>



<li>Container memory limiti ile toplam RSS farkı</li>



<li>Safepoint süresi ve safepoint’e ulaşma gecikmesi</li>



<li>Reference processing süresi</li>



<li>G1 humongous region ve evacuation failure sayısı</li>
</ul>



<p class="wp-block-paragraph">Yalnızca “GC toplam süresi” metriğini izlemek yanıltıcıdır. ZGC’de kısa duraklamalara rağmen concurrent GC CPU maliyeti artabilir. G1’de toplam GC süresi makul görünürken tek bir Full GC tail-latency SLO’sunu ihlal edebilir.</p>



<h2 class="wp-block-heading">Üretime geçiş için önerilen test yöntemi</h2>



<p class="wp-block-paragraph">GC değişimi kontrollü bir performans mühendisliği çalışması olarak ele alınmalıdır:</p>



<ol start="1" class="wp-block-list">
<li>Mevcut collector ile temel ölçüm alın.</li>



<li>Aynı JDK dağıtımı, heap ve CPU limitleriyle ikinci collector’ı test edin.</li>



<li>Gerçekçi canlı veri kümesini test başlamadan önce oluşturun.</li>



<li>JVM ısınması ve JIT derlemesi tamamlandıktan sonra ölçüm alın.</li>



<li>Ortalama yükün yanı sıra trafik sıçraması ve uzun süreli soak test uygulayın.</li>



<li>p99.9 latency, throughput, CPU ve RSS değerlerini birlikte karşılaştırın.</li>



<li>Heap dump, JFR ve GC loglarını allocation kaynakları açısından inceleyin.</li>



<li>Canary deployment ile sınırlı üretim trafiğinde doğrulayın.</li>



<li>Otomatik rollback eşiğini GC pause yerine uçtan uca servis SLO’suna bağlayın.</li>
</ol>



<p class="wp-block-paragraph">Konteyner ortamında yalnızca <code>-Xmx</code> değerini pod bellek limitine eşitlemeyin. Metaspace, code cache, thread stack’leri, direct buffer’lar, JNI kütüphaneleri ve JVM’nin native yapıları da RSS tüketir. Heap dışında yeterli güvenlik payı bırakılmalıdır.</p>



<h2 class="wp-block-heading">Hangi senaryoda hangi GC seçilmeli?</h2>



<h3 class="wp-block-heading">G1 GC tercih edin:</h3>



<ul class="wp-block-list">
<li>Gecikme hedefi yüzlerce milisaniye ölçeğindeyse,</li>



<li>Throughput, ultra düşük pause süresinden daha önemliyse,</li>



<li>CPU limiti sıkıysa,</li>



<li>Heap orta veya büyük olsa da canlı veri kümesi yönetilebiliyorsa,</li>



<li>Operasyon ekibi yerleşik ve yaygın bir collector istiyorsa,</li>



<li>Gerçek workload altında G1 Full GC veya evacuation problemi üretmiyorsa.</li>
</ul>



<h3 class="wp-block-heading">ZGC tercih edin:</h3>



<ul class="wp-block-list">
<li>p99.9 ve üzeri tail-latency kritikse,</li>



<li>Tek haneli milisaniyelik servis hedefleri bulunuyorsa,</li>



<li>Heap onlarca veya yüzlerce gigabaytsa,</li>



<li>Canlı veri kümesi büyükse,</li>



<li>Uzun G1 evacuation ya da Full GC duraklamaları görülüyorsa,</li>



<li>Concurrent GC için CPU kapasitesi ayrılabiliyorsa,</li>



<li>Heap’e yeterli allocation headroom verilebiliyorsa.</li>
</ul>



<h2 class="wp-block-heading">Sonuç</h2>



<p class="wp-block-paragraph">G1 GC ve ZGC arasında evrensel bir kazanan yoktur. G1 GC, yüksek throughput ile nispeten kısa ve tahmin edilebilir duraklamalar arasında güçlü bir denge sağlar. Bu nedenle çoğu kurumsal Java servisi için hâlâ iyi bir başlangıç noktasıdır.</p>



<p class="wp-block-paragraph">ZGC’nin ayrıştığı alan, büyük heap ve katı tail-latency hedefleridir. Colored pointers, barrier mekanizmaları, concurrent marking ve concurrent relocation sayesinde pahalı GC işlemlerinin büyük bölümünü uygulamayı durdurmadan gerçekleştirir. Böylece heap büyüklüğünün Stop-The-World süresine etkisini önemli ölçüde azaltır.</p>



<p class="wp-block-paragraph">Bunun karşılığında ZGC’ye yeterli CPU ve heap headroom sağlanmalıdır. Collector seçimi yalnızca maksimum GC pause karşılaştırmasına indirgenmemeli; throughput, p99.9 yanıt süresi, allocation rate, CPU tüketimi, RSS ve altyapı maliyeti birlikte değerlendirilmelidir.</p>



<p class="wp-block-paragraph">Kurumsal üretim ortamı için en güvenilir yaklaşım şudur: Önce sade bir JVM konfigürasyonu kurun, ölçüm alın, üretime benzeyen yük altında G1 ve ZGC’yi karşılaştırın ve yalnızca gözlemlenmiş bir problemi çözen bayrağı ekleyin.</p>
<p><a href="https://alikarahisar.com/blog/garbage-collector-secimi-g1-gc-vs-zgc/">Garbage Collector Seçimi ve Akort Edilmesi: G1 GC vs. ZGC</a> yazısı ilk önce <a href="https://alikarahisar.com/blog">Ali Karahisar</a> üzerinde ortaya çıktı.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://alikarahisar.com/blog/garbage-collector-secimi-g1-gc-vs-zgc/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Java Virtual Threads: Daha Fazla Thread Değil, Daha Doğru Bir Eşzamanlılık Modeli</title>
		<link>https://alikarahisar.com/blog/java-virtual-threads/</link>
					<comments>https://alikarahisar.com/blog/java-virtual-threads/#respond</comments>
		
		<dc:creator><![CDATA[Ali Karahisar]]></dc:creator>
		<pubDate>Mon, 31 Aug 2026 12:02:01 +0000</pubDate>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[Yazılım]]></category>
		<category><![CDATA[Java Virtual Threads]]></category>
		<guid isPermaLink="false">https://alikarahisar.com/blog/?p=34</guid>

					<description><![CDATA[<p>Java sunucu uygulamalarında yıllardır aynı temel gerilimle yaşadık: Senkron, bloklayan kod kolay anlaşılırdı; fakat yüksek eşzamanlılık altında yeterince ölçeklenmezdi. Non-blocking ve reactive kod ise donanımı verimli kullanabiliyor, ancak uygulamanın kontrol akışını daha karmaşık hâle getiriyordu. Java 21 ile kalıcı hâle gelen Virtual Threads, bu denklemi değiştiriyor. Amaç, bloklayan kodu ortadan kaldırmak değil; bloklayan bir işin [&#8230;]</p>
<p><a href="https://alikarahisar.com/blog/java-virtual-threads/">Java Virtual Threads: Daha Fazla Thread Değil, Daha Doğru Bir Eşzamanlılık Modeli</a> yazısı ilk önce <a href="https://alikarahisar.com/blog">Ali Karahisar</a> üzerinde ortaya çıktı.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Java sunucu uygulamalarında yıllardır aynı temel gerilimle yaşadık: Senkron, bloklayan kod kolay anlaşılırdı; fakat yüksek eşzamanlılık altında yeterince ölçeklenmezdi. Non-blocking ve reactive kod ise donanımı verimli kullanabiliyor, ancak uygulamanın kontrol akışını daha karmaşık hâle getiriyordu.</p>



<span id="more-34"></span>



<p class="wp-block-paragraph">Java 21 ile kalıcı hâle gelen Virtual Threads, bu denklemi değiştiriyor. Amaç, bloklayan kodu ortadan kaldırmak değil; bloklayan bir işin bekleme süresince pahalı bir işletim sistemi thread’ini işgal etmesini önlemek.</p>



<p class="wp-block-paragraph">Bu nedenle Virtual Threads’i yalnızca “daha hafif thread” şeklinde tanımlamak eksik kalır. Asıl mimari değişiklik şudur:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Uygulamanın eşzamanlılık birimi yeniden <code>Thread</code> ile temsil edilebilir; fakat her Java thread’i için ayrı bir işletim sistemi thread’i ayırmak gerekmez.</p>
</blockquote>



<h2 class="wp-block-heading">Platform Thread ve Virtual Thread arasındaki fark</h2>



<p class="wp-block-paragraph">Geleneksel Java thread’leri, yani platform thread’leri, işletim sistemi thread’leri üzerine kuruludur. Basitleştirilmiş olarak ilişki 1:1’dir:</p>



<pre class="wp-block-code"><code>Java Platform Thread ─── OS Thread</code></pre>



<p class="wp-block-paragraph">Bir platform thread ağdan veri, veritabanından yanıt veya dosya sisteminden sonuç beklerken işletim sistemi thread’i de ona bağlı kalır. Thread pool kullanmak thread oluşturma maliyetini azaltır; fakat aynı anda çalışabilecek iş sayısını havuz büyüklüğüyle sınırlar.</p>



<p class="wp-block-paragraph">Virtual Thread mimarisinde ise ilişki M biçimindedir:</p>



<pre class="wp-block-code"><code>Çok sayıda Virtual Thread
            │
            ▼
Az sayıda Carrier Platform Thread
            │
            ▼
       OS Thread’leri</code></pre>



<p class="wp-block-paragraph">Virtual thread çalışacağı zaman JVM tarafından bir platform thread’e, yani <em>carrier thread</em>’e bağlanır. Desteklenen bir bloklayan I/O çağrısına geldiğinde virtual thread askıya alınabilir ve carrier serbest bırakılabilir. Aynı carrier bu sırada başka bir virtual thread’i çalıştırır.</p>



<p class="wp-block-paragraph">I/O tamamlandığında virtual thread yeniden çalıştırılabilir; üstelik daha önce kullandığı carrier’a dönmek zorunda değildir. Uygulama açısından ise bu geçiş görünmezdir:</p>



<pre class="wp-block-code"><code>var response = httpClient.send(request, BodyHandlers.ofString());</code></pre>



<p class="wp-block-paragraph">Kod hâlâ senkron görünür. Metot sonucu bekler, normal <code>try/catch</code> kullanılabilir ve stack trace istek akışını korur. Fakat bekleyen virtual thread uygun durumda carrier’ı işgal etmez.</p>



<p class="wp-block-paragraph">Platform ve virtual thread’leri şöyle karşılaştırabiliriz:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><th>Özellik</th><th>Platform Thread</th><th>Virtual Thread</th></tr><tr><td>OS thread ilişkisi</td><td>Yaklaşık 1:1</td><td>M</td></tr><tr><td>Oluşturma maliyeti</td><td>Görece yüksek</td><td>Çok düşük</td></tr><tr><td>Tipik kullanım</td><td>Uzun ömürlü worker pool</td><td>Her görev için yeni thread</td></tr><tr><td>Bloklayan I/O</td><td>OS thread’i işgal eder</td><td>Çoğu durumda carrier’dan ayrılır</td></tr><tr><td>Stack</td><td>Native bellekte, görece büyük</td><td>Heap üzerinde büyüyüp küçülen stack chunk’ları</td></tr><tr><td>Uygun iş yükü</td><td>CPU ve sınırlı eşzamanlılık</td><td>Çok yüksek I/O eşzamanlılığı</td></tr><tr><td>Havuzlama</td><td>Genellikle gerekir</td><td>Yapılmamalıdır</td></tr><tr><td>Programlama modeli</td><td>Senkron</td><td>Senkron</td></tr><tr><td>Temel kazanç</td><td>Kontrollü kaynak kullanımı</td><td>Yüksek throughput ve basit kontrol akışı</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Virtual Threads Java 21’de JEP 444 ile kalıcı özellik hâline gelmiştir. OpenJDK’nın temel hedefi de thread-per-request biçimindeki sunucu uygulamalarının daha verimli ölçeklenmesini sağlamaktır. <a href="https://openjdk.org/jeps/444">JEP 444: Virtual Threads</a></p>



<h2 class="wp-block-heading">Thread-Per-Request modeline neden geri dönüyoruz?</h2>



<p class="wp-block-paragraph">Thread-per-request yaklaşımında her istek başından sonuna kadar tek bir mantıksal thread üzerinde ilerler:</p>



<pre class="wp-block-code"><code>HTTP isteği
   → kimlik doğrulama
   → veritabanı sorgusu
   → uzak servis çağrısı
   → yanıt üretimi</code></pre>



<p class="wp-block-paragraph">Bu modelin güçlü tarafı yapısal sadeliğidir:</p>



<ul class="wp-block-list">
<li>Kontrol akışı yukarıdan aşağı okunur.</li>



<li>Yerel değişkenler istek durumunu doğal biçimde taşır.</li>



<li>Hatalar <code>try/catch/finally</code> ile yönetilir.</li>



<li>Transaction ve güvenlik sınırları daha kolay görülür.</li>



<li>Debugger ile adım adım ilerlenebilir.</li>



<li>Stack trace, iş çağrı zincirini doğrudan gösterir.</li>
</ul>



<p class="wp-block-paragraph">Sorun modelin kendisi değil, modelin platform thread’leriyle uygulanmasıydı.</p>



<p class="wp-block-paragraph">Little Yasası’na göre sistemde aynı anda bulunan ortalama iş sayısı yaklaşık olarak throughput ile ortalama gecikmenin çarpımıdır:</p>



<pre class="wp-block-code"><code>Concurrency ≈ Throughput × Latency</code></pre>



<p class="wp-block-paragraph">Örneğin ortalama 200 ms süren bir işlemde saniyede 5.000 istek karşılamak istiyorsak sistemin yaklaşık 1.000 isteği eşzamanlı taşıması gerekir. Her istek bir platform thread tüketiyorsa thread sayısı kısa sürede temel darboğaza dönüşebilir.</p>



<p class="wp-block-paragraph">Reactive ve callback tabanlı sistemler bu problemi, bekleme sırasında thread’i serbest bırakarak çözdü. Bunun karşılığında uygulamanın mantıksal çağrı zinciri callback, <code>Mono</code>, <code>Flux</code> veya <code>CompletableFuture</code> aşamalarına bölündü.</p>



<p class="wp-block-paragraph">Virtual Threads aynı kaynak sorununa farklı bir katmanda çözüm getirir:</p>



<ul class="wp-block-list">
<li>Reactive yaklaşım, thread paylaşımını uygulama programlama modeline taşır.</li>



<li>Virtual Threads ise thread paylaşımını JVM zamanlayıcısının sorumluluğuna bırakır.</li>
</ul>



<p class="wp-block-paragraph">Böylece “bir istek, bir thread” modeli yeniden ekonomik hâle gelir. Buradaki thread artık pahalı bir OS kaynağı değil, uygulamadaki bir görevi temsil eden hafif bir Java nesnesidir.</p>



<p class="wp-block-paragraph">Önemli bir sonuç da şudur: Virtual Threads tek bir isteği kendiliğinden hızlandırmaz. Esas fayda, çok sayıda bekleyen işi daha az platform thread ile taşıyarak toplam throughput’u artırmaktır. I/O gecikmesi yine aynı gecikmedir.</p>



<h2 class="wp-block-heading">Reactive programming artık gereksiz mi?</h2>



<p class="wp-block-paragraph">Kısa cevap: Hayır. Fakat reactive programlamanın seçim gerekçesi değişmiştir.</p>



<p class="wp-block-paragraph">Reactive stack’lerin yaygınlaşmasındaki önemli nedenlerden biri, çok sayıda bloklayan isteği sınırlı platform thread havuzuyla taşımanın zor olmasıydı. Virtual Threads bu teknik zorunluluğu önemli ölçüde azaltır.</p>



<p class="wp-block-paragraph">Tipik bir servis aşağıdaki özelliklere sahipse Spring MVC ve Virtual Threads güçlü bir seçenek olabilir:</p>



<ul class="wp-block-list">
<li>Request/response ağırlıklı çalışma</li>



<li>JDBC gibi bloklayan istemciler</li>



<li>Senkron HTTP çağrıları</li>



<li>Kısa veya orta uzunlukta iş akışları</li>



<li>Ekibin imperative Java konusunda daha deneyimli olması</li>



<li>Kolay debugging ve okunabilir stack trace beklentisi</li>
</ul>



<p class="wp-block-paragraph">Spring Boot 3.2’den itibaren Java 21 üzerinde virtual thread desteği şu ayarla etkinleştirilebilir:</p>



<pre class="wp-block-code"><code>spring.threads.virtual.enabled=true</code></pre>



<p class="wp-block-paragraph">Spring ekibi de Spring MVC ile Virtual Threads kombinasyonunu geleneksel web uygulamaları için yalın bir seçenek olarak konumlandırmaktadır. Kaynak: <a href="https://spring.io/blog/2023/10/16/runtime-efficiency-with-spring/" target="_blank" rel="noreferrer noopener">Spring: Runtime Efficiency with Spring</a></p>



<p class="wp-block-paragraph">Bununla birlikte WebFlux ve Reactive Streams hâlâ belirgin avantajlara sahiptir:</p>



<ul class="wp-block-list">
<li>Sonsuz veya uzun süreli veri akışları</li>



<li>Backpressure’ın uçtan uca ifade edilmesi</li>



<li>Server-Sent Events ve streaming response’lar</li>



<li>Çok aşamalı event pipeline’ları</li>



<li>Deklaratif dönüşüm ve birleştirme operasyonları</li>



<li>Reactive sürücüler üzerine kurulmuş mevcut sistemler</li>



<li>Akışın talep, iptal ve hata sinyallerinin bir protokol olarak modellenmesi</li>
</ul>



<p class="wp-block-paragraph">Virtual Threads bir zamanlama mekanizmasıdır; backpressure protokolü değildir. Tüketici üreticiden yavaşsa virtual thread kullanmak tek başına kuyruk büyümesini veya bellek baskısını önlemez.</p>



<p class="wp-block-paragraph">Dolayısıyla doğru soru “WebFlux mü, Virtual Threads mü?” değildir. Daha doğru karar çerçevesi şöyledir:</p>



<ul class="wp-block-list">
<li>Yüksek sayıda bağımsız, bloklayan request/response işi için imperative kod ve Virtual Threads genellikle daha basittir.</li>



<li>Akış semantiği, backpressure veya yoğun reaktif kompozisyon gerekiyorsa WebFlux değerini korur.</li>



<li>Mevcut reactive sistem yalnızca virtual thread geldi diye yeniden yazılmamalıdır.</li>



<li>Yeni bir CRUD servisi de sırf yüksek eşzamanlılık ihtimali var diye otomatik olarak reactive tasarlanmamalıdır.</li>
</ul>



<h2 class="wp-block-heading">I/O yoğunluklu bir örnek</h2>



<p class="wp-block-paragraph">Aşağıdaki servis, çok sayıda sipariş için iki uzak sistemden bloklayan HTTP çağrıları yapıyor. Her sipariş ayrı bir virtual thread üzerinde işleniyor.</p>



<pre class="wp-block-code language-java"><code>import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public final class OrderAggregator {

    private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(2))
            .build();

    /*
     * Virtual thread sayısını değil, kapasitesi sınırlı olan uzak servise
     * yapılan eşzamanlı çağrı sayısını sınırlandırıyoruz.
     */
    private static final Semaphore REMOTE_SERVICE_LIMIT =
            new Semaphore(100);

    public static void main(String&#91;] args) throws Exception {
        List&lt;Long&gt; orderIds = java.util.stream.LongStream
                .rangeClosed(1, 1_000)
                .boxed()
                .toList();

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List&lt;Callable&lt;OrderView&gt;&gt; tasks = orderIds.stream()
                    .&lt;Callable&lt;OrderView&gt;&gt;map(orderId -&gt;
                            () -&gt; loadOrderView(orderId))
                    .toList();

            var futures = executor.invokeAll(tasks);

            for (var future : futures) {
                OrderView view = future.get();
                System.out.println(view);
            }
        }
    }

    private static OrderView loadOrderView(long orderId)
            throws IOException, InterruptedException {

        String orderJson = get(
                "https://orders.example.com/orders/" + orderId);

        String shipmentJson = get(
                "https://shipping.example.com/shipments/" + orderId);

        return new OrderView(orderId, orderJson, shipmentJson);
    }

    private static String get(String url)
            throws IOException, InterruptedException {

        REMOTE_SERVICE_LIMIT.acquire();
        try {
            HttpRequest request = HttpRequest.newBuilder(URI.create(url))
                    .timeout(Duration.ofSeconds(3))
                    .GET()
                    .build();

            return HTTP_CLIENT.send(
                    request,
                    HttpResponse.BodyHandlers.ofString()
            ).body();
        } finally {
            REMOTE_SERVICE_LIMIT.release();
        }
    }

    record OrderView(
            long orderId,
            String orderJson,
            String shipmentJson
    ) {}
}</code></pre>



<p class="wp-block-paragraph">Kodun önemli tarafı, çağrıların bloklayan <code>HttpClient.send()</code> kullanmasına rağmen her görev için ayrı bir virtual thread oluşturulabilmesidir. I/O bekleyen görevler uygun olduğunda carrier thread’i serbest bırakır.</p>



<p class="wp-block-paragraph">Burada executor bir virtual thread havuzu değildir. <code>newVirtualThreadPerTaskExecutor()</code> her görev için yeni bir virtual thread oluşturur. Virtual thread’ler ucuz olduğundan yeniden kullanılmaları veya sabit boyutlu bir havuzda tutulmaları amaçlanmaz.</p>



<p class="wp-block-paragraph">Buna karşılık uzak servis bağlantıları sınırsız değildir. Bu nedenle <code>Semaphore</code>, iş parçacığı sayısını değil kıt kaynağa erişimi sınırlar. OpenJDK da virtual thread’lerin havuzlanmamasını ve sınırlı kaynakların semaphore gibi mekanizmalarla korunmasını önerir. <a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html">Oracle Virtual Threads Adoption Guide</a></p>



<p class="wp-block-paragraph">Üretim kodunda ayrıca timeout, retry bütçesi, circuit breaker, rate limit ve iptal davranışı açık biçimde tasarlanmalıdır. Bir milyon virtual thread oluşturabilmek, veritabanının bir milyon bağlantıyı veya uzak servisin bir milyon eşzamanlı isteği kaldırabileceği anlamına gelmez.</p>



<h2 class="wp-block-heading">Kaçınılması gereken antipattern’ler</h2>



<h3 class="wp-block-heading">1. Virtual thread’leri sabit boyutlu havuzda toplamak</h3>



<p class="wp-block-paragraph">Yanlış yaklaşım:</p>



<pre class="wp-block-code"><code>var executor = Executors.newFixedThreadPool(
        100,
        Thread.ofVirtual().factory()
);</code></pre>



<p class="wp-block-paragraph">Bu tasarım virtual thread’in temel avantajını yapay olarak sınırlar. Bir göreve bir virtual thread oluşturulmalıdır:</p>



<pre class="wp-block-code"><code>var executor = Executors.newVirtualThreadPerTaskExecutor();</code></pre>



<p class="wp-block-paragraph">Eşzamanlılık sınırı gerekiyorsa thread sayısı yerine veritabanı, haricî API veya başka bir kıt kaynak doğrudan sınırlandırılmalıdır.</p>



<h3 class="wp-block-heading">2. Uzun süren CPU işlerini sınırsız virtual thread’e dağıtmak</h3>



<p class="wp-block-paragraph">Virtual Threads CPU kapasitesini artırmaz. Binlerce CPU-bound görevi virtual thread üzerinde başlatmak çekirdek sayısını değiştirmez; yalnızca zamanlama baskısı ve kuyruk oluşturur.</p>



<p class="wp-block-paragraph">CPU-bound işlerde çekirdek sayısına göre boyutlandırılmış executor, <code>ForkJoinPool</code> veya uygun paralel algoritma çoğu zaman daha doğrudur.</p>



<pre class="wp-block-code"><code>I/O-bound görev  → Virtual Threads için güçlü aday
CPU-bound görev  → Sınırlı paralellik gerekir</code></pre>



<h3 class="wp-block-heading">3. JDK 21–23 üzerinde bloklayan I/O’yu <code>synchronized</code> içinde yapmak</h3>



<p class="wp-block-paragraph">Java 21–23’te virtual thread, <code>synchronized</code> bir bölüm içinde bloklandığında carrier thread’e pinlenebilir:</p>



<pre class="wp-block-code"><code>synchronized (lock) {
    callSlowRemoteService();
}</code></pre>



<p class="wp-block-paragraph">Bu durum programı hatalı yapmaz; fakat uzun ve sık gerçekleşiyorsa ölçeklenebilirliği düşürür. Bu JDK sürümlerinde gerekli noktalarda <code>ReentrantLock</code> kullanılabilir veya I/O kritik bölümün dışına taşınabilir:</p>



<pre class="wp-block-code"><code>lock.lock();
try {
    updateSharedInMemoryState();
} finally {
    lock.unlock();
}

callSlowRemoteService();</code></pre>



<p class="wp-block-paragraph">Ancak bu tavsiye sürüme bağlıdır. JDK 24’te gelen JEP 491 ile <code>synchronized</code> altında bekleyen virtual thread’lerin carrier’ı serbest bırakabilmesi sağlandı. Dolayısıyla JDK 24+ üzerinde sırf virtual thread kullanılıyor diye bütün <code>synchronized</code> bloklarını mekanik olarak <code>ReentrantLock</code> ile değiştirmek doğru değildir. <a href="https://docs.oracle.com/en/java/javase/24/migrate/significant-changes-jdk-24.html">Oracle JDK 24 değişiklikleri</a></p>



<p class="wp-block-paragraph">Yine de sürümden bağımsız tasarım ilkesi geçerlidir: Ağ veya veritabanı çağrısı boyunca lock tutmak contention, uzun kuyruklar ve deadlock riski yaratabilir. Ayrıca native veya foreign-function çağrıları güncel JDK’larda hâlâ pinning oluşturabilir.</p>



<h3 class="wp-block-heading">4. <code>ThreadLocal</code> içinde pahalı nesne cache’lemek</h3>



<p class="wp-block-paragraph">Virtual thread’ler <code>ThreadLocal</code> destekler. Sorun API’nin çalışmaması değil, thread sayısının artık çok büyük olabilmesidir:</p>



<pre class="wp-block-code"><code>private static final ThreadLocal&lt;ExpensiveParser&gt; PARSER =
        ThreadLocal.withInitial(ExpensiveParser::new);</code></pre>



<p class="wp-block-paragraph">On binlerce veya yüz binlerce virtual thread, aynı sayıda pahalı nesne örneği oluşturabilir. Platform thread havuzunda makul görünen “thread başına cache” yaklaşımı virtual thread mimarisinde ciddi bellek tüketimine dönüşebilir.</p>



<p class="wp-block-paragraph">Daha güvenli seçenekler şunlardır:</p>



<ul class="wp-block-list">
<li>Immutable ve thread-safe nesneleri paylaşmak</li>



<li>Gerçekten sınırlı kaynaklar için açık bir pool kullanmak</li>



<li>Request context’i mümkünse metot parametreleriyle taşımak</li>



<li>Kullanılan JDK sürümünün durumuna göre <code>ScopedValue</code> değerlendirmek</li>



<li>Framework’lerin context propagation davranışını test etmek</li>
</ul>



<p class="wp-block-paragraph">Correlation ID veya güvenlik bağlamı gibi küçük request-scoped veriler <code>ThreadLocal</code> ile çalışabilir. Ancak bellek maliyeti, yaşam süresi, temizleme davranışı ve <code>InheritableThreadLocal</code> ile istemsiz veri aktarımı özellikle incelenmelidir.</p>



<h3 class="wp-block-heading">5. Sınırsız dış kaynak tüketimini ölçeklenebilirlik sanmak</h3>



<p class="wp-block-paragraph">Virtual thread sayısı arttığında darboğaz ortadan kalkmaz; başka bir katmana taşınır:</p>



<ul class="wp-block-list">
<li>JDBC connection pool</li>



<li>HTTP connection pool</li>



<li>Uzak servis kapasitesi</li>



<li>Rate limit</li>



<li>Heap</li>



<li>Dosya tanıtıcıları</li>



<li>CPU</li>



<li>Kuyruklar</li>
</ul>



<p class="wp-block-paragraph">Bu nedenle virtual thread geçişi mutlaka bulkhead, timeout ve yük testi stratejisiyle birlikte ele alınmalıdır.</p>



<h3 class="wp-block-heading">6. Yalnızca ortalama latency’ye bakmak</h3>



<p class="wp-block-paragraph">Virtual Threads’e geçiş değerlendirilirken şu metrikler birlikte izlenmelidir:</p>



<ul class="wp-block-list">
<li>Throughput</li>



<li>p95 ve p99 latency</li>



<li>Carrier thread kullanımı</li>



<li>CPU ve heap tüketimi</li>



<li>GC davranışı</li>



<li>Bağlantı havuzu bekleme süreleri</li>



<li>Uzak servis hata oranları</li>



<li>Virtual thread pinning olayları</li>



<li>Kuyruk uzunlukları</li>
</ul>



<p class="wp-block-paragraph">Java Flight Recorder, özellikle pinning ve virtual thread yaşam döngüsüyle ilgili olayları gözlemlemek için kullanılabilir. Java 21 dokümantasyonu <code>jdk.VirtualThreadPinned</code> olayını ve <code>jdk.tracePinnedThreads</code> seçeneğini açıklar. JDK 24+ değerlendirmelerinde ise monitor pinning davranışının değiştiği unutulmamalıdır.</p>



<h2 class="wp-block-heading">Mimari karar özeti</h2>



<p class="wp-block-paragraph">Virtual Threads, reactive programlamanın yerini alan evrensel bir teknoloji değildir. Daha çok, reactive programlamanın zorunlu hâle gelmesine neden olan thread kıtlığını JVM seviyesinde azaltır.</p>



<p class="wp-block-paragraph">İyi bir başlangıç matrisi şöyledir:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Senaryo</td><td>Muhtemel tercih</td></tr><tr><td>JDBC ve senkron HTTP kullanan CRUD servisi</td><td>Spring MVC + Virtual Threads</td></tr><tr><td>Çok sayıda bağımsız I/O çağrısı</td><td>Virtual Threads</td></tr><tr><td>CPU yoğun hesaplama</td><td>Sınırlı platform thread paralelliği</td></tr><tr><td>Sürekli veya sonsuz event stream</td><td>WebFlux / Reactive Streams</td></tr><tr><td>Uçtan uca backpressure</td><td>Reactive yaklaşım</td></tr><tr><td>Mevcut, çalışan reactive mimari</td><td>Yeniden yazmak için tek başına gerekçe yok</td></tr><tr><td>Basit kontrol akışı ve kolay debugging önceliği</td><td>Virtual Threads</td></tr><tr><td>Karma model</td><td>Kontrollü biçimde MVC, virtual thread ve reactive istemcilerin birlikte kullanımı</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Sonuç</h2>



<p class="wp-block-paragraph">Project Loom’un en önemli katkısı “milyonlarca thread oluşturabilmek” değildir. Daha temel katkısı, Java’daki mantıksal görev ile işletim sistemi thread’i arasındaki zorunlu 1:1 ilişkiyi kaldırmasıdır.</p>



<p class="wp-block-paragraph">Bu değişiklik sayesinde:</p>



<ul class="wp-block-list">
<li>Thread-per-request modeli yeniden ölçeklenebilir hâle gelir.</li>



<li>Bloklayan ve okunabilir Java kodu yüksek I/O eşzamanlılığı taşıyabilir.</li>



<li>Stack trace, debugging ve hata yönetimi doğal yapısını korur.</li>



<li>Reactive programlama teknik bir zorunluluk olmaktan çıkarak bilinçli bir mimari seçeneğe dönüşür.</li>
</ul>



<p class="wp-block-paragraph">Fakat fiziksel kaynak sınırları ortadan kalkmaz. Virtual thread’ler sınırsız veritabanı bağlantısı, sınırsız CPU veya sınırsız uzak servis kapasitesi sağlamaz. Başarılı bir geçiş; thread sayısını büyütmekten çok, darboğazları doğru yerde sınırlandırmayı, kullanılan kütüphaneleri doğrulamayı ve sistemi gerçek yük altında ölçmeyi gerektirir.</p>



<p class="wp-block-paragraph">Doğru zihinsel model şudur:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Virtual Threads, bloklamayı ucuzlatır; kaynakları sınırsızlaştırmaz.</p>
</blockquote>
<p><a href="https://alikarahisar.com/blog/java-virtual-threads/">Java Virtual Threads: Daha Fazla Thread Değil, Daha Doğru Bir Eşzamanlılık Modeli</a> yazısı ilk önce <a href="https://alikarahisar.com/blog">Ali Karahisar</a> üzerinde ortaya çıktı.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://alikarahisar.com/blog/java-virtual-threads/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
