← Kursa Dön
📄 Text · 15 min

JVM ve Bellek Yönetimi

Giriş

Java'nın en büyük vaatlerinden biri şuydu: "Bellek yönetimiyle uğraşma, ben hallederim." C/C++ dünyasından gelen insanlar için bu bir devrimdi. Ama "ben hallederim" demek "hiç düşünme" demek değil. JVM'in belleği nasıl yönettiğini anlamak, performans sorunlarını çözmek ve production'da gece 3'te çalan alarmları önlemek için kritik.

Bu derste JVM'in iç dünyasına dalacağız. Stack ve Heap nedir, Garbage Collector nasıl çalışır, hangi GC algoritması ne zaman seçilir — hepsini konuşacağız.

JVM Mimarisi — Büyük Resim

JVM bir sanal makine. Yazdığın Java kodu önce bytecode'a derlenir (.class dosyaları), sonra JVM bu bytecode'u çalıştırır. Ama çalıştırırken bellekte ciddi bir organizasyon var.

JVM belleği kabaca şu bölgelere ayrılır:

  • Stack — Method çağrıları ve yerel değişkenler

  • Heap — Nesneler (objects)

  • Metaspace — Sınıf bilgileri (Java 8 sonrası)

  • Code Cache — JIT derlenmiş native kod

Şimdilik Stack ve Heap'e odaklanalım çünkü günlük hayatında en çok bunlarla muhatap olacaksın.

Stack vs Heap

Analoji: Mutfak Tezgahı vs Buzdolabı

Stack'i mutfak tezgahı gibi düşün. Yemek yaparken aktif olarak kullandığın malzemeler tezgahta durur. Yemek bitince tezgahı temizlersin. Her şey düzenli, hızlı erişilir, ama alan sınırlı.

Heap ise buzdolabı. Büyük, kapasiteli, her şeyi atabilirsin. Ama bir şey bulmak tezgaha göre daha yavaş. Ve arada bir temizlemen (Garbage Collection) gerekir, yoksa bozulan şeyler yer kaplamaya devam eder.

Stack Detaylı

Stack, her thread için ayrı oluşturulur. Yani 10 thread varsa 10 ayrı stack var. LIFO (Last In, First Out) prensibiyle çalışır.

Stack'te neler tutulur?

  • Primitive değişkenler (int, double, boolean...)

  • Object reference'ları (nesnenin kendisi değil, adresi)

  • Method çağrı bilgileri (stack frame)

public class StackOrnek {
    public static void main(String[] args) {
        int sayi = 42;           // Stack'te: sayi = 42
        String isim = "Ali";     // Stack'te: isim -> Heap'teki String nesnesinin adresi
        hesapla(sayi);           // Yeni stack frame oluşur
    }

    static int hesapla(int x) {
        int sonuc = x * 2;      // Stack'te: x = 42, sonuc = 84
        return sonuc;            // Method bitince bu frame stack'ten silinir
    }
}

Her method çağrıldığında stack'e bir frame eklenir. Method bitince frame otomatik silinir. Temizlik bedava — GC'ye gerek yok.

Stack'in boyutu sınırlıdır (varsayılan genelde 512KB-1MB). Sonsuz recursive çağrı yaparsan StackOverflowError alırsın:

public static void sonsuzDongu() {
    sonsuzDongu(); // Her çağrı yeni frame ekler
    // Sonunda: java.lang.StackOverflowError
}

Heap Detaylı

Heap, tüm thread'ler tarafından paylaşılır. new keyword'üyle oluşturduğun her nesne burada yaşar.

public class HeapOrnek {
    public static void main(String[] args) {
        // "Ali" nesnesi Heap'te oluşur
        // ogrenci reference'ı Stack'te tutulur
        Ogrenci ogrenci = new Ogrenci("Ali", 20);

        // Yeni bir nesne daha Heap'te
        List<String> liste = new ArrayList<>();
        liste.add("Java");
    }
}

Heap çok daha büyüktür (GB'larca olabilir) ama yönetimi karmaşıktır. İşte burada Garbage Collector devreye girer.

Karşılaştırma Tablosu

ÖzellikStackHeap
BoyutKüçük (MB)Büyük (GB)
HızÇok hızlıNispeten yavaş
ThreadHer thread'e özelTüm thread'ler paylaşır
TemizlikOtomatik (frame pop)Garbage Collector
İçerikPrimitive + referenceNesneler
HataStackOverflowErrorOutOfMemoryError

Heap'in İç Yapısı — Generational Model

JVM Heap'i tek düz bir alan değil. Nesne yaşına göre bölgelere ayrılmış.

Young Generation

Yeni oluşturulan nesneler burada başlar. Çoğu nesne kısa ömürlüdür — bir method içinde oluşur, method bitince artık kimse referans etmez. Bu gözleme "Weak Generational Hypothesis" denir: nesnelerin çoğu genç yaşta ölür.

Young Generation üç bölümden oluşur:

  • Eden Space — Yeni nesneler burada doğar

  • Survivor 0 (S0) — İlk GC'den sağ çıkanlar

  • Survivor 1 (S1) — İkinci GC'den sağ çıkanlar

Old Generation (Tenured)

Birkaç GC döngüsünden sağ çıkan nesneler buraya terfi eder. Buradaki nesneler genelde uzun ömürlüdür: cache'ler, connection pool'lar, singleton'lar gibi.

Akış Şeması

new Object() → Eden Space
                  ↓ (Minor GC)
              Survivor 0 ↔ Survivor 1
                  ↓ (Birkaç GC sağ kalırsa)
              Old Generation
                  ↓ (Major/Full GC ile temizlenir)

💡 Minor GC sadece Young Generation'ı temizler ve çok hızlıdır (milisaniye). Major GC (veya Full GC) tüm Heap'i tarar ve çok daha yavaştır.

Garbage Collector — Nasıl Çalışır?

Temel Prensip: Erişilebilirlik (Reachability)

GC, "Bu nesneye kimse erişebiliyor mu?" sorusunu sorar. Erişilemez nesneler çöp sayılır.

Erişilebilirlik GC Roots'tan başlayarak kontrol edilir. GC Roots şunlardır:

  • Aktif thread'lerdeki yerel değişkenler

  • Static değişkenler

  • JNI referansları

public class GCOrnek {
    public static void main(String[] args) {
        Object a = new Object(); // a -> Nesne1 (erişilebilir)
        Object b = new Object(); // b -> Nesne2 (erişilebilir)

        a = null;  // Nesne1'e artık kimse erişemiyor → GC temizleyebilir
        b = a;     // b artık null, Nesne2'ye erişim yok → GC temizleyebilir
    }
}

Mark and Sweep

En temel GC algoritması iki aşamadan oluşur:

  1. Mark — GC Roots'tan başla, erişilebilir tüm nesneleri işaretle

  2. Sweep — İşaretlenmemiş nesneleri sil

Bazı GC'ler üçüncü adım ekler:

  1. Compact — Hayatta kalan nesneleri yan yana diz (fragmentation'ı önle)

Stop-the-World (STW)

GC çalışırken uygulamanın durması gerekebilir. Buna Stop-the-World pause denir. Neden? Çünkü GC nesneleri tararken, uygulama aynı anda yeni nesne oluşturur veya referansları değiştirirse tutarsızlık olur.

Modern GC'lerin amacı bu duraklamaları minimuma indirmek.

GC Algoritmaları

Serial GC

En basit GC. Tek thread kullanır. Stop-the-World süreleri uzundur.

-XX:+UseSerialGC

Ne zaman? Küçük uygulamalar, düşük bellek ortamları, container'larda sınırlı kaynak varsa.

Parallel GC (Throughput Collector)

Serial GC'nin çok thread'li versiyonu. Birden fazla GC thread'i paralel çalışır. Java 8'in varsayılan GC'siydi.

-XX:+UseParallelGC

Ne zaman? Batch processing, throughput önemliyse ve kısa duraklamalar tolere edilebiliyorsa.

G1 GC (Garbage-First)

Java 9'dan itibaren varsayılan GC. Heap'i eşit boyutlu region'lara böler. Hangi region'da en çok çöp varsa önce onu toplar (adı buradan gelir: Garbage-First).

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200   // Hedef duraklama süresi

G1'in güzelliği: Bir hedef duraklama süresi veriyorsun, G1 bu süreye uymaya çalışıyor.

Ne zaman? Çoğu production uygulaması için iyi bir varsayılan. 4GB+ Heap boyutlarında özellikle etkili.

// G1 region yapısı konsepti
// Heap: [E][E][S][O][O][E][H][O][S][E]...
// E = Eden, S = Survivor, O = Old, H = Humongous (büyük nesne)
// Her region bağımsız toplanabilir

ZGC

Java 15'te production-ready oldu. Hedefi: 1ms altında duraklama süreleri, TB boyutunda Heap desteği.

-XX:+UseZGC

ZGC bunu concurrent çalışarak başarır — uygulama çalışırken GC de çalışır, neredeyse hiç durma yok.

Ne zaman? Düşük latency kritikse (trading sistemleri, real-time uygulamalar), çok büyük Heap boyutları.

Karşılaştırma

GCSTW SüresiThroughputHeap BoyutuJava
SerialUzunDüşükKüçükTüm versiyonlar
ParallelOrtaYüksekOrtaTüm versiyonlar
G1KontrollüİyiOrta-Büyük9+ varsayılan
ZGC<1msİyiÇok büyük15+

⚠️ Dikkat: GC seçimi uygulamanın doğasına göre yapılır. "En iyi GC" diye bir şey yok. Throughput mu önemli, latency mi? Heap ne kadar büyük? Bu soruları yanıtla, sonra seç.

Memory Leak — Java'da Olur mu?

"Java'da GC var, memory leak olmaz" diye düşünüyorsan yanılıyorsun. GC sadece erişilemeyen nesneleri temizler. Erişilebilir ama gereksiz nesneler temizlenmez. İşte Java'daki memory leak budur.

Yaygın Memory Leak Nedenleri

1. Koleksiyonlara Ekleme, Silmemeyi Unutma

public class LeakOrnek {
    private static List<byte[]> cache = new ArrayList<>();

    public static void islemYap() {
        // Her çağrıda 1MB ekleniyor ama hiç silinmiyor
        cache.add(new byte[1024 * 1024]);
    }
    // cache static olduğu için GC Roots'tan erişilebilir
    // → GC asla temizlemez → OutOfMemoryError
}

2. Listener/Callback Temizlememek

public class EventManager {
    private List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener);
    }

    // removeListener yok! Eklenen listener'lar sonsuza kadar yaşar
}

3. Kapatılmayan Kaynaklar

// KÖTÜ — Connection kapatılmıyor
public void veriOku() {
    Connection conn = DriverManager.getConnection(url);
    // ... bir şeyler yap
    // conn.close() yok! Connection nesnesi ve bağlı her şey Heap'te kalır
}

// İYİ — try-with-resources
public void veriOku() {
    try (Connection conn = DriverManager.getConnection(url)) {
        // ... bir şeyler yap
    } // Otomatik kapanır
}

4. Inner Class Referansları

public class Outer {
    private byte[] buyukVeri = new byte[10_000_000]; // 10MB

    // Non-static inner class, Outer'a implicit referans tutar
    class Inner {
        void birSeyYap() { }
    }

    public Inner getInner() {
        return new Inner(); // Inner yaşadığı sürece Outer da yaşar (10MB!)
    }
}

💡 Çözüm: Inner class'ı static yapabiliyorsan yap. Static inner class, outer class'a referans tutmaz.

Memory Leak Tespit Araçları

  • VisualVM — JDK ile birlikte gelir, Heap dump alıp analiz edebilirsin

  • JConsole — Basit monitoring

  • Eclipse MAT (Memory Analyzer Tool) — Heap dump analizi için güçlü

  • jmap — Komut satırından Heap dump

# Çalışan JVM'in Heap dump'ını al
jmap -dump:format=b,file=heapdump.hprof <pid>

# Heap kullanım özeti
jmap -heap <pid>

JVM Tuning Temelleri

Heap Boyutu Ayarlama

# Minimum ve maksimum Heap boyutu
java -Xms512m -Xmx2g MyApp

# -Xms: Başlangıç Heap boyutu
# -Xmx: Maksimum Heap boyutu

Genel kural: -Xms ve -Xmx aynı değer yapılırsa Heap'in runtime'da büyümesi/küçülmesi engellenir. Production'da bu genelde tercih edilir.

Stack Boyutu

# Thread başına Stack boyutu
java -Xss1m MyApp

Derin recursive çağrılar yapıyorsan Stack boyutunu artırabilirsin. Ama dikkat — her thread için ayrı Stack ayrılır, 1000 thread × 1MB = 1GB sadece Stack için.

GC Loglama

# Java 9+ GC log
java -Xlog:gc*:file=gc.log:time,uptime,level,tags MyApp

# Çıktı örneği:
# [0.015s][info][gc] Using G1
# [1.234s][info][gc] GC(0) Pause Young (Normal) 24M->8M(256M) 3.456ms

GC logları okumak önemli bir beceri. Pause süreleri, Heap kullanımı ve GC sıklığı buradan takip edilir.

Temel Tuning Stratejisi

  1. Ölç — GC loglarını aç, metrikleri topla

  2. Anla — Duraklama süreleri kabul edilebilir mi? OOM oluyor mu?

  3. Ayarla — Heap boyutu, GC algoritması, GC parametreleri

  4. Tekrar ölç — Değişiklik işe yaradı mı?

# Tipik bir production başlatma komutu
java \
  -Xms4g -Xmx4g \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/app/ \
  -Xlog:gc*:file=/var/log/app/gc.log:time \
  -jar myapp.jar

⚠️ Dikkat: Premature optimization yapma. Önce uygulamayı yaz, sonra performans sorunu varsa profiling yap. GC tuning son çare olmalı — genelde kod seviyesinde düzeltmeler daha etkili.

Metaspace

Java 8 öncesi sınıf bilgileri PermGen (Permanent Generation) adlı sabit boyutlu bir alanda tutulurdu. Boyutu dolunca OutOfMemoryError: PermGen space alırdın.

Java 8 ile PermGen kaldırıldı, yerine Metaspace geldi. Metaspace native bellekte yaşar ve varsayılan olarak sınırsız büyüyebilir (OS belleği bitene kadar).

# Metaspace sınırı koymak istersen
java -XX:MaxMetaspaceSize=256m MyApp

Metaspace genelde sorun olmaz. Ama dinamik olarak çok sayıda sınıf oluşturan uygulamalarda (proxy framework'ler, bytecode manipulation) dikkat etmek gerekir.

Pratik: Bellek Durumunu Koddan Okuma

public class BellekDurumu {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();

        long maxMemory = runtime.maxMemory();       // -Xmx değeri
        long totalMemory = runtime.totalMemory();   // Şu an ayrılmış Heap
        long freeMemory = runtime.freeMemory();     // Boş alan
        long usedMemory = totalMemory - freeMemory; // Kullanılan

        System.out.println("Max Heap:  " + mbFormat(maxMemory));
        System.out.println("Total:     " + mbFormat(totalMemory));
        System.out.println("Used:      " + mbFormat(usedMemory));
        System.out.println("Free:      " + mbFormat(freeMemory));
    }

    static String mbFormat(long bytes) {
        return (bytes / (1024 * 1024)) + " MB";
    }
}

String Pool — Özel Bir Heap Bölgesi

String'ler Java'da özel muamele görür. String literal'leri Heap'teki String Pool adlı özel bir alanda tutulur.

String s1 = "Merhaba";  // String Pool'da oluşturulur
String s2 = "Merhaba";  // Pool'daki aynı nesneyi işaret eder
String s3 = new String("Merhaba"); // Heap'te AYRI bir nesne

System.out.println(s1 == s2);      // true — aynı nesne
System.out.println(s1 == s3);      // false — farklı nesneler
System.out.println(s1.equals(s3)); // true — aynı içerik

String Pool bellek tasarrufu sağlar. Aynı literal'i 1000 yerde kullansen bile bellekte tek bir nesne vardır. new String() ile oluşturulanlar Pool'a girmez — ama intern() ile ekleyebilirsin:

String s3 = new String("Merhaba");
String s4 = s3.intern(); // Pool'daki versiyonu döndürür
System.out.println(s1 == s4); // true

💡 Not: Java 7'den itibaren String Pool Heap'te yaşar (eskiden PermGen'deydi). Bu sayede GC tarafından temizlenebilir.

Escape Analysis ve Stack Allocation

JVM bazı nesneleri Heap yerine Stack'te oluşturabilir. Buna Escape Analysis denir. JIT compiler bir nesnenin method dışına "kaçmadığını" tespit ederse, o nesneyi Stack'te oluşturur.

public int toplam() {
    // Bu Point nesnesi method dışına çıkmıyor (escape etmiyor)
    // JVM bunu Stack'te oluşturabilir → GC'ye iş düşmez
    Point p = new Point(3, 4);
    return p.x + p.y;
}

Escape Analysis otomatik çalışır, senin bir şey yapman gerekmez. Ama bilmek faydalı — kısa ömürlü, method-local nesneler performans açısından "bedava"ya yakın.

Daha da ileri giderse, JVM nesneyi hiç oluşturmayabilir — field'ları doğrudan register'lara koyar. Buna Scalar Replacement denir.

// JVM bunu optimize edebilir:
Point p = new Point(3, 4);
int sonuc = p.x + p.y;

// Buna:
int sonuc = 3 + 4; // Nesne hiç oluşturulmadı!

Weak, Soft ve Phantom Reference

Java'da normal referanslar dışında üç özel referans tipi var. Bunlar GC'nin davranışını etkilemene olanak tanır.

Strong Reference (Normal)

Object obj = new Object(); // Strong reference
// obj erişilebilir olduğu sürece GC temizlemez

WeakReference

GC çalıştığında, sadece weak reference'ı olan nesneler hemen temizlenir.

import java.lang.ref.WeakReference;

Object obj = new Object();
WeakReference<Object> weak = new WeakReference<>(obj);

obj = null; // Strong reference kaldırıldı
// Bir sonraki GC'de nesne temizlenir

System.gc(); // GC'yi öner (garanti değil)
System.out.println(weak.get()); // muhtemelen null

Kullanım alanı: Cache'ler. WeakHashMap weak reference kullanan bir Map implementasyonu — key'e başka referans kalmayınca entry otomatik silinir.

WeakHashMap<Object, String> cache = new WeakHashMap<>();
Object key = new Object();
cache.put(key, "değer");

key = null; // Key'e başka referans yok
System.gc();
// Bir süre sonra cache boşalır

SoftReference

WeakReference'a benzer ama GC daha "nazik" davranır. Bellek yeterliyse temizlemez, bellek azaldığında temizler. Cache için idealdir.

import java.lang.ref.SoftReference;

SoftReference<byte[]> cache = new SoftReference<>(new byte[10_000_000]);

byte[] data = cache.get();
if (data != null) {
    // Cache'ten oku
} else {
    // GC temizlemiş, yeniden yükle
    data = yukle();
    cache = new SoftReference<>(data);
}

PhantomReference

En zayıf referans tipi. get() her zaman null döner. Nesne temizlendikten sonra bildirim almak için kullanılır. ReferenceQueue ile birlikte çalışır.

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

ReferenceQueue<Object> queue = new ReferenceQueue<>();
Object obj = new Object();
PhantomReference<Object> phantom = new PhantomReference<>(obj, queue);

obj = null;
System.gc();

// Queue'dan referans alınca nesnenin temizlendiğini bilirsin
Reference<?> ref = queue.poll();
if (ref != null) {
    System.out.println("Nesne temizlendi!");
    // Kaynak temizleme yapabilirsin
}

Phantom reference'lar finalize() yerine kullanılır. Cleaner API de arka planda bu mekanizmayı kullanır.

Referans Gücü Karşılaştırma

ReferansGC DavranışıKullanım
StrongErişilebilirse asla temizlenmezNormal kullanım
SoftBellek azaldığında temizlenirMemory-sensitive cache
Weakİlk GC'de temizlenirWeakHashMap, listener'lar
PhantomTemizlendikten sonra bildirimKaynak temizleme

JIT Compiler ve Performans

JVM bytecode'u doğrudan yorumlamaz (interpreter). Sık çalışan kod parçalarını native makine koduna derler. Buna Just-In-Time (JIT) Compilation denir.

Nasıl Çalışır?

  1. Uygulama başlangıçta interpreter ile çalışır

  2. JVM her method'un kaç kez çağrıldığını sayar

  3. Bir eşik aşılınca (varsayılan ~10.000) method hot kabul edilir

  4. JIT compiler bu method'u native koda derler

  5. Sonraki çağrılarda native kod çalışır — çok daha hızlı

// Bu method 10.000 kez çağrıldıktan sonra JIT tarafından derlenir
public int topla(int a, int b) {
    return a + b;
}

C1 ve C2 Compiler

JVM'de iki JIT compiler var:

  • C1 (Client) — Hızlı derleme, temel optimizasyonlar. Uygulama hızlı başlasın.

  • C2 (Server) — Yavaş derleme, agresif optimizasyonlar. Uzun süre çalışan uygulamalarda yüksek performans.

Varsayılan modda Tiered Compilation aktif: Önce C1 ile hızlıca derle, sonra çok sıcak method'ları C2 ile daha agresif optimize et.

# JIT derleme bilgisini görmek için
java -XX:+PrintCompilation MyApp

# Çıktı örneği:
#    123   1    java.lang.String::charAt (33 bytes)
#    456   2    com.app.Main::topla (4 bytes)

Container Ortamında JVM

Docker ve Kubernetes ile çalışırken JVM'in container kaynaklarını doğru algılaması önemli.

Sorun

Eski JVM versiyonları container'ın bellek limitini görmezdi. Host makinenin 64GB RAM'ini görür, ona göre Heap ayarlardı — sonra container limiti 512MB olduğu için OOM Killer tarafından öldürülürdü.

Çözüm

Java 10+ container limitlerine otomatik uyum sağlar:

# Container'da önerilen ayarlar
docker run -m 512m myapp:latest \
  java \
  -XX:MaxRAMPercentage=75.0 \
  -XX:InitialRAMPercentage=50.0 \
  -jar myapp.jar

-XX:MaxRAMPercentage=75.0 demek: container'ın bellek limitinin %75'ini Heap için kullan. Kalan %25 Metaspace, Stack, native bellek ve OS için ayrılır.

# Eski -Xmx yerine yüzde bazlı ayar
# Container 1GB ise → Heap max 768MB
# Container 2GB ise → Heap max 1536MB
# Otomatik ölçeklenir!

⚠️ Dikkat: Container'da -Xmx ile sabit değer vermek yerine -XX:MaxRAMPercentage kullanmak daha esnektir. Container limiti değiştiğinde JVM otomatik uyum sağlar.

Pratik: OutOfMemoryError Simülasyonu

public class OOMSimulasyon {
    public static void main(String[] args) {
        List<byte[]> liste = new ArrayList<>();
        int sayac = 0;

        try {
            while (true) {
                liste.add(new byte[1024 * 1024]); // Her iterasyonda 1MB
                sayac++;
                System.out.println(sayac + " MB ayrıldı");
            }
        } catch (OutOfMemoryError e) {
            System.out.println("OOM! " + sayac + " MB'da patladı.");
            System.out.println("Hata: " + e.getMessage());
        }
    }
}
# Küçük Heap ile çalıştır
java -Xmx50m OOMSimulasyon
# Çıktı: ~47-48 MB'da OOM alırsın

Pratik: GC Davranışını Gözlemleme

public class GCGozlem {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("Başlangıç bellek: " + usedMB() + " MB");

        // Çok sayıda geçici nesne oluştur
        for (int i = 0; i < 100_000; i++) {
            String s = new String("Geçici nesne #" + i);
        }

        System.out.println("Nesneler oluşturuldu: " + usedMB() + " MB");

        // GC'yi öner
        System.gc();
        Thread.sleep(1000);

        System.out.println("GC sonrası: " + usedMB() + " MB");
    }

    static long usedMB() {
        Runtime rt = Runtime.getRuntime();
        return (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
    }
}
# GC logları ile çalıştır
java -Xlog:gc* GCGozlem

finalize() — Neden Kullanılmamalı

Java'da finalize() method'u, nesne GC tarafından temizlenmeden önce çağrılırdı. Ama bu method ciddi sorunlara yol açar:

  • Ne zaman çağrılacağı garanti değil

  • GC'yi yavaşlatır

  • Nesneyi yanlışlıkla tekrar erişilebilir yapabilir (resurrection)

  • Java 9'da deprecated, Java 18'de kaldırılmak üzere işaretlendi

// YAPMA
@Override
protected void finalize() throws Throwable {
    // Kaynak temizleme kodu buraya konmamalı
    connection.close();
}

// BUNUN YERİNE: try-with-resources veya Cleaner API kullan

Özet

  • Stack thread'e özel, küçük, hızlı — primitive ve reference tutar. Heap paylaşımlı, büyük — nesneler burada yaşar

  • Heap, Young (Eden + Survivor) ve Old generation'a ayrılır. Çoğu nesne genç yaşta ölür

  • GC, erişilemeyen nesneleri temizler. Mark → Sweep → (Compact) adımlarıyla çalışır

  • G1 GC çoğu uygulama için iyi varsayılan. ZGC düşük latency istiyorsan tercih et

  • Java'da memory leak olur — koleksiyonlarda biriken nesneler, kapatılmayan kaynaklar, static referanslar ana nedenler

  • JVM tuning'de önce ölç, sonra ayarla. -Xms, -Xmx, GC logları temel araçların