Stack ve Heap: Bir Programın Belleği Nasıl Çalışır?
Stack ve heap, her programın görmezden geldiği iki oda — ta ki bir çökme, bir GC duraklaması ya da bir performans sorunu kapıyı çalana kadar. Bu yazıda ikisini önce dilden bağımsız açıyoruz, sonra Swift ve Dart'a bağlıyoruz; tahsis maliyetinden value/reference dengesine kadar.

Her programın görmezden geldiği iki oda
Hangi dilde yazarsan yaz, programın çalışırken belleğini iki temelde farklı bölgeye ayırır: stack (yığın) ve heap (öbek). Çoğu zaman bunları düşünmezsin — ta ki performans, beklenmedik bir çökme ya da çöp toplayıcının (garbage collector) davranışı seni düşünmeye zorlayana kadar. Oysa bu ikisini bir kez gerçekten anlamak çok şeyi açıklar: neden bazı tahsisler (allocation) "bedava"yken bazıları GC'yi tetikler, neden derin bir özyineleme (recursion) çöker, neden value/reference type ayrımı önemlidir. Konu dilden bağımsız; aynı RAM, aynı kurallar. Anlatırken Swift ve Dart'a da bağlayacağız.
Önce harita: bir program belleği nasıl bölüyor
Bir program çalışmaya başladığında işletim sistemi ona bir adres uzayı verir ve bunu bölgelere ayırır (yüksek adresten düşüğe):
- Stack — çağrı çerçevelerinin yaşadığı yer; aşağı doğru büyür.
- Heap — dinamik nesnelerin yaşadığı yer; yukarı doğru büyür. İkisi aradaki boşluğu paylaşır.
- Data / BSS — global ve static değişkenler; program boyunca sabit.
- Kod (text) — programın komutları; salt-okunur (değiştirilemez).
Kritik nokta: stack de heap de aynı fiziksel RAM'dedir. Aralarındaki fark donanım değil, yönetim biçimidir.
Stack: hızlı, otomatik, disiplinli
Stack, fonksiyon çağrılarının çalışma alanıdır. Bir fonksiyon çağrıldığında stack'e bir çerçeve (stack frame) eklenir (push): local değişkenler, "iş bitince nereye döneceğim" bilgisi (return address) ve bir önceki çerçeveye bağ. Fonksiyon dönünce bu çerçeve kaldırılır (pop) — ki bu çoğu zaman fiziksel silme değil, sadece stack pointer'ı geri kaydırmaktır. Yapı LIFO'dur (last-in-first-out — son giren ilk çıkar).
Özellikleri:
- Tahsis bedavaya yakındır: sadece bir işaretçiyi kaydırırsın.
- Ömür otomatiktir: fonksiyon dönünce çerçeve yok olur; sen yönetmezsin.
- Çok hızlıdır: küçük ve yerel olduğu için neredeyse her zaman işlemciye yakın hızlı bellekte (cache) durur.
- Thread'e özeldir: her thread'in kendi stack'i vardır.
- Sınırlıdır: birkaç MB'tır. Çok derin ya da sonsuz özyineleme stack'i taşırır — stack overflow tam budur.
Bir çökmede gördüğün stack trace, aslında bu stack'in o andaki fotoğrafıdır: çerçevelerin "beni kim çağırdı" zinciri.
Heap: büyük, esnek, ama bedeli var
Heap, boyutunu ya da ömrünü derleme anında bilemediğin veya kendisini yaratan fonksiyondan daha uzun yaşaması gereken veriler içindir.
Özellikleri:
- Tahsis daha pahalıdır: bir tahsisçiden (allocator) yer istersin; stack'teki tek işaretçi kaydırması kadar ucuz değildir.
- Ömrünü sen/runtime belirlersin: veri, biri onu serbest bırakana kadar yaşar.
- Daha yavaştır: veriler dağınık olduğu için işlemci sık sık RAM'e iner (cache miss).
- Paylaşılır: tüm thread'ler aynı heap'e erişebilir — bu yüzden veri yarışları (race condition) burada doğar.
- Temizlenmesi gerekir: manuel (C), referans sayımı (Swift'in ARC'si) ya da çöp toplama (Dart, Java). Bedeli: fragmentasyon, tahsis maliyeti ve ya sızıntı (leak) ya da GC duraklamaları (pause).
Stack vs Heap, tek bakışta

| Stack | Heap | |
|---|---|---|
| Tahsis | işaretçiyi kaydır (tek işlem) | tahsisçiden iste (pahalı) |
| Ömür | fonksiyon dönünce biter (otomatik) | sen/GC bırakana kadar |
| Hız | çok hızlı (cache-dostu) | daha yavaş (dağınık) |
| Boyut | küçük (~MB) | büyük |
| Sahiplik | thread'e özel | paylaşılır |
Kısaca: stack control flow'u (akışı) taşır; heap veriyi saklar.
Diller bunu nasıl kullanıyor: değer mi, referans mı?

Bir dilin sana verdiği asıl kaldıraç şu: veriyi inline (içine gömülü, çoğunlukla stack'te) mi tutabiliyorsun, yoksa yalnızca bir referans (pointer) arkasında (heap'te) mi?
Swift ikisini de verir. struct bir value type'tır: verisi kopyalanarak inline tutulur; küçük struct'lar heap tahsisi olmadan stack'te ya da başka bir nesnenin içine gömülü yaşayabilir. class ise reference type'tır: nesne heap'te yaşar, ona giden küçük bir referans stack'te/register'da durur. Heap nesnelerini ARC (otomatik referans sayımı) yönetir — deterministiktir ama her retain/release atomik bir işlemdir. Ufak bir tuzak: Array, String, Dictionary value type'tır ama altında heap buffer + referans sayımı kullanır (copy-on-write).
struct Point { var x, y: Int } // value → inline
class Node { var next: Node? } // reference → heap
let p = Point(x: 1, y: 2) // verisi doğrudan burada (stack)
let n = Node() // nesne heap'te, referansı stack'te
Dart'ta struct yoktur — her sınıf reference type'tır. Yani senin nesnelerinin gövdesi heap'te, referansları stack'te yaşar.
class Point { int x, y; Point(this.x, this.y); }
var p = Point(1, 2); // nesne heap'te, referansı stack'te
var n = 42; // küçük int → inline (Smi), heap'e gitmez
"Her şey heap'te" yanılgısı
Buradan yanlış bir zihinsel modele kaymamak için iki şeyi netleştirelim.
Birincisi, "her şey object/reference" ≠ "her şey heap'te." Dart küçük tam sayıları (Smi — small integer) işaretçi etiketleme (pointer tagging) ile doğrudan işaretçinin içinde tutar; heap'e hiç gitmez. Optimize kodda double'lar register'da (unboxed) tutulabilir, hatta escape analizi bazı tahsisleri tamamen eleyebilir.
İkincisi ve en önemlisi: call stack her dilde, value/reference farkından bağımsız olarak kullanılır. Dart'ta bile fonksiyon çağrıları, dönüş adresleri ve referansların kendisi stack'te yaşar. Derin bir widget ağacının build() özyinelemesi tamamen call stack üzerinde koşar (ve stack overflow verebilir). Reference type, nesnenin gövdesini heap'e taşır; stack'i ortadan kaldırmaz. Yani "Dart sadece heap kullanır" yanlıştır: nesnelerinin verisi heap'e yaslanır (çünkü struct yok), ama akış, referanslar ve küçük değerler stack'i tüm hızıyla kullanır.
Peki bu performansı belirler mi?
Burada abartmamak lazım. Value type'lar, sıcak yollarda (hot path) heap tahsisini ve GC baskısını azaltır ve sana kontrol verir — yoğun döngüler, parçacık sistemleri, ses/sinyal işleme gibi yerlerde gerçek bir avantaj. Ama "her zaman daha hızlı" değildir: büyük struct'lar pahalıya kopyalanır; copy-on-write gizli bir heap tahsisi + atomik referans sayımı saklar; ARC'nin atomik retain/release'i bedavaya gelmez; Dart'ın generational GC'si ise kısa ömürlü nesneleri çok ucuza tahsis eder (genç kuşakta neredeyse pointer-bump). Üstelik gerçek uygulamalarda performansı çoğu zaman dilin stack/heap eğilimi değil, mimari, I/O ve render disiplini belirler. "Daha fazla kontrol" ile "her zaman daha hızlı" aynı şey değildir.
Zihinsel model
Tek cümleye sığdırırsak: stack, çağrılar için hızlı, otomatik, thread'e özel bir çalışma alanıdır (akış + local'ler + referanslar); heap ise uzun ömürlü ve dinamik veriler için büyük, paylaşılan, yönetilen bir depodur. Diller, veriyi ne kadar inline (value) tutabildiğin ile referans arkasında (heap) tutmak zorunda olduğun konusunda ayrışır — Swift seçim sunar (struct/class), Dart referansa yaslanır. Ama ikisi de her iki bölgeyi kullanır; stack hiçbir zaman ortadan kalkmaz. Hangisinin ne olduğunu bilmek; tahsis maliyetini, çökmeleri, GC'yi ve value-vs-reference dengelerini tek bir modelde açıklar.