Stack ve heap farkı, metot çağrılarına bağlı çalışma bağlamı ile nesnelerin yaşamını yönetmeye yarayan alan arasındaki ayrımdır. Değer türündeki bir değişken verisini doğrudan, referans türündeki bir değişken ise nesneye erişmeyi sağlayan referansı tutar. Ancak değişkenin türü, onun fiziksel bellekteki yerini tek başına belirlemez.
Stack ve heap farkı nedir? Kısa cevap
Stack, bir metot çalışırken o çağrıya ait bağlamı izlemeyi sağlar: parametreler, yerel veriler ve metodun tamamlanıp çağıran yere dönmesi bu bağlamın parçalarıdır. Metot çağrıldığında yeni bir çağrı çerçevesi oluşur; çağrı tamamlandığında o çerçeve sona erer. Java sanal makinesinin tanımladığı çağrı çerçeveleri, bu işleyişin somut bir örneğidir.
Heap ise nesneleri, onları kullanan tek bir metot çağrısına bağlı kalmadan düşünmeye yardımcı olur. Bir metotta oluşturulan nesneye başka bir yerden erişilebiliyorsa, o metot bittikten sonra da nesne kullanılabilir. Nesnenin artık kullanılmıyor olması bile belleğinin tam o anda geri alınacağı anlamına gelmez; bu süreç çöp toplayıcının yönetimindedir.
Stack ve heap, ezberlenecek iki kutu değil, değişkenlerin yaşamını ve nesnelerin nasıl yönetildiğini anlamak için kullanılan bir modeldir. Diyagramlarda ayrı çizilmeleri, her çalışma ortamında fiziksel belleğin keskin sınırlarla ayrıldığı anlamına gelmez. Java sanal makinesi belirtimi, çağrı çerçevelerinin fiziksel yerleşimi konusunda uygulamaya esneklik tanır.
Değer ve referans kavramları nasıl ayrılır?

C# dilinde int bir değer türüdür; Java dilinde de int ilkel türdür. Bir int değişkeninin değeri başka bir değişkene atandığında sayı kopyalanır. Sonradan kopyalardan birini değiştirmek diğerinin değerini değiştirmez. Bir class nesnesine erişen değişken başka bir değişkene atandığında ise nesnenin tamamı değil, referans değeri kopyalanır. Böylece iki değişken aynı nesneye erişebilir.
Nesnenin değiştirilebilir bir alanı varsa bu alanı bir değişken üzerinden değiştirmek, diğer değişken üzerinden bakıldığında da görülebilir. Buna karşılık değişkenlerden birini başka bir nesneye yöneltmek, diğerini kendiliğinden yöneltmez. Değer türü stack'tedir, referans türü heap'tedir kuralı bu yüzden yanıltıcıdır: Bir sayı, bir nesnenin alanı olabilir; bir referans da yerel değişken veya nesne alanı olabilir. Tür, atama davranışını anlamana yardım eder, tek başına fiziksel adres vermez.
Yerel bir referansın bulunduğu metot sona erdiğinde nesnenin de hemen yok olduğunu varsayma. Başka bir erişim yolu kalmış olabilir; kalmamış olsa bile bellek geri alma anı metottan çıkış anıyla aynı olmak zorunda değildir. Ayrıca C# dilindeki ref ve out, bir argümanı değişkene başvurarak aktarmaya ilişkin mekanizmalardır; bir türün referans türü olmasıyla aynı kavram değildir.
C# kodunda bir metot çağrısını bellek modeliyle izleme
Bir metot çağrısını izlerken iki soruyu ayrı düşünmek gerekir: Değer doğrudan mı kopyalandı, yoksa bir nesneye giden referans mı kopyalandı? Aşağıdaki örnekte sayi bir int değeri, ogrenci ise Ogrenci nesnesine erişen bir referansı tutar.
using System;
public class Ogrenci
{
public string Ad;
public Ogrenci(string ad)
{
Ad = ad;
}
}
public class Program
{
public static void Main()
{
int sayi = 10;
Ogrenci ogrenci = new Ogrenci("Ada");
YazdirVeDegistir(sayi, ogrenci);
Console.WriteLine(sayi);
Console.WriteLine(ogrenci.Ad);
}
public static void YazdirVeDegistir(int kopyaSayi, Ogrenci kopyaOgrenci)
{
int yerel = kopyaSayi + 5;
kopyaOgrenci.Ad = "Bora";
Console.WriteLine(yerel);
}
}
Beklenen çıktı sırasıyla şöyledir:
15
10
Bora
Şema, fiziksel adresleri veya her çalışma ortamındaki kesin yerleşimi göstermez. Basitleştirilmiş bir modelde çağrı bağlamları stack ile, oluşturulan nesne ise heap ile ilişkilendirilen ayrı bir alanda düşünülebilir.
1. Çağrı öncesinde Main bağlamı
Main:
sayi = 10
ogrenci -> Ogrenci { Ad: "Ada" }
ogrenci değişkeninin içinde nesnenin tamamı yerine o nesneye ulaşmayı sağlayan referans bulunur. Bu nedenle referansı, nesneye yönelen bir ok gibi düşünebilirsin.
2. YazdirVeDegistir çağrısı sırasında
Main:
sayi = 10
ogrenci -> aynı Ogrenci nesnesi
YazdirVeDegistir:
kopyaSayi = 10
kopyaOgrenci -> aynı Ogrenci nesnesi
yerel = 15
sayi gönderilirken içindeki 10 değeri kopyalanır. Bu yüzden kopyaSayi üzerinde yapılan işlem, yalnızca yerel değişkenini 15 yapar; Main içindeki sayi değişkeni 10 olarak kalır.
ogrenci gönderilirken ise referansın kendisi kopyalanır, nesnenin tamamı kopyalanmaz. kopyaOgrenci ile ogrenci aynı nesneye eriştiği için Ad alanındaki değişiklik, metot sona erdikten sonra da görülür.
3. Metot bitince
Main:
sayi = 10
ogrenci -> Ogrenci { Ad: "Bora" }
YazdirVeDegistir:
çağrı bağlamı kaldırıldı
Metot tamamlandığında ona ait kopyaSayi, kopyaOgrenci ve yerel bağlamdan çıkar. Ancak Main içindeki referans aynı nesneye erişmeyi sürdürür. Değer ve referans ayrımını pekiştirmek için ücretsiz bilgi testleri merkezindeki testlerden yararlanabilirsin.
Recursion çağrı yığınıyla nasıl ilişkilidir?

Recursion, bir metodun kendisini yeniden çağırmasıdır. Örneğin Say(3) çağrısı Say(2) çağrısını, o da Say(1) çağrısını oluşturabilir. Taban durumuna ulaşıldığında yeni çağrı yapılmaz ve dönüş süreci başlar.
Say(3)
Say(2)
Say(1)
taban durumu
Her çağrının kendi parametre değeri ve yerel değişkenleri bulunan ayrı bir kavramsal çağrı bağlamı vardır. Say(1) tamamlandıktan sonra Say(2) bağlamı, ardından Say(3) bağlamı ele alınır. Yani çağrılar ters sırayla çözülür.
Taban durumu, recursion'ın ne zaman duracağını belirler. Bu koşul bulunmazsa veya çağrılar gereğinden fazla derinleşirse çağrı yığını için ayrılan kaynaklar tükenebilir. Bu nedenle recursive bir çözümde her çağrının taban durumuna yaklaşması ve sonlanma koşulunun açık olması gerekir.
Stack ve heap hakkında hangi yanlışlar düzeltilmeli?
Stack ve heap kavramları, belleği anlamak için yararlı bir modeldir; ancak fiziksel belleğin her programda değişmeyen iki kutusu gibi düşünülmemelidir. Aşağıdaki ayrımlar, bu konudaki yaygın yanlışları düzeltir.
- Her değer türü stack'te bulunmaz. Bir değerin nerede tutulduğu, değişkenin bulunduğu bağlama ve çalışma ortamının kararlarına bağlıdır. Örneğin bir değer türü, heap üzerinde bulunan bir nesnenin alanının parçası olabilir.
- Her referans türü nesne değişmez biçimde heap'te durmaz. Referans türü, nesneye doğrudan değer kopyalamak yerine bir erişim yolu kullanıldığını anlatır. Referans değişkeni ile referans verilen nesne aynı şey değildir.
- Heap her zaman yavaş, stack her zaman hızlı değildir. Performans; veri düzeni, önbellek davranışı, nesne oluşturma maliyeti ve çalışma ortamının bellek yönetimi gibi birçok etkene bağlıdır. Bu nedenle yalnızca belleğin adına bakarak hız sonucu çıkarılamaz.
- Metot bitince ilgili nesne hemen yok olmaz. Metoda ait yerel değişkenlerin kapsamı sona erebilir; fakat nesneye başka bir erişilebilir referans ulaşıyorsa nesne yaşamını sürdürür. Hiçbir erişilebilir referans kalmadığında nesne temizlenmeye uygun hâle gelir, ancak temizleme anı metot dönüşüyle aynı olmak zorunda değildir.
- Stack ve heap, programcının elle yönettiği iki sabit kutu değildir. Programcı kapsamı, değerleri ve referansları yönetir; bellek yerleşimi ile kullanılmayan nesnelerin temizlenmesi ise çalışma ortamının sorumluluğundadır.
Bu yüzden bir yerel değişkenin yaşamı ile o değişkenin eriştiği nesnenin yaşamını ayrı ayrı düşünmek gerekir. Değişken kapsam dışına çıkabilir, fakat nesne başka bir referans üzerinden kullanılmaya devam edebilir.
Bir metot çağrısını bellek açısından nasıl takip edebilirsin?
Bir çağrıyı incelemek için belleği tahmin etmeye çalışmak yerine şu beş soruyu sırayla sorabilirsin:
- Hangi metot çağrıldı ve hangi çağrı bağlamı oluştu? Parametreleri ve metoda ait yerel isimleri belirle.
- Hangi yerel değerler üretildi? Her değişkenin o anda hangi değeri taşıdığını yaz.
- Hangi değişkenler değer kopyası taşıyor? Hangilerinin aynı nesneye eriştiğini ayrıca ayır.
- Hangi nesne değiştirildi? Nesnenin içeriği değiştiyse, aynı nesneye erişen diğer referansların etkilenip etkilenmediğini kontrol et.
- Metot döndükten sonra hangi referanslar erişmeye devam ediyor? Bu soru, yerel bağlamın sona ermesiyle nesne yaşamının sona ermesini birbirinden ayırır.
C# örneğinde bir metot, bir tamsayı ile bir nesne referansını birlikte alıyorsa çağrıyı şöyle okuyabilirsin: tamsayı parametresi kendi değerini taşır, nesne parametresi ise aynı nesneye erişim sağlar. Metot nesnenin alanını değiştirirse çağıran kod bu değişikliği görebilir; yalnızca tamsayı parametresini değiştirmek, çağırandaki değeri kendiliğinden değiştirmez. Metot döndüğünde yerel çağrı bağlamı kapanır, ancak nesneye erişen başka bir referans varsa nesne kullanılabilir kalır.
Aynı beş soruluk yöntem, Java kodunu okurken de kullanılabilir. Algoritmik düşünme ile birlikte Java veya C# bilgini pratik etmek için ücretsiz bilgi testleri merkezini kullanabilirsin.
Sık Sorulan Sorular
Stack overflow ile heap belleğinin tükenmesi aynı hata mıdır?
Hayır. Stack overflow, çağrı yığınına çok fazla çağrı bağlamı sığmasıyla ilişkilidir ve kontrolsüz recursion bunun yaygın nedenlerinden biridir. Heap belleğinin tükenmesi ise yeni nesne veya veri için yeterli bellek ayrılamamasıyla ilgilidir.
Bir metot sona erdiğinde oluşturulan nesne hemen bellekten kaldırılır mı?
Hayır. Metot içindeki referans ortadan kalksa bile nesneye başka bir erişilebilir referans ulaşabiliyorsa nesne yaşamını sürdürür. Hiçbir erişilebilir referans kalmadığında temizlenmeye uygun hâle gelir, fakat bu işlem hemen gerçekleşmek zorunda değildir.
C# ve Java'da her değer türü stack üzerinde mi tutulur?
Hayır. C#'ta değer türü, Java'da ise ilkel değer veya referans ayrımı doğrudan stack konumu anlamına gelmez. Değişkenin bağlamı ve çalışma ortamının uygulama kararları, fiziksel yerleşimde rol oynar.
Stack ve heap'i iki değişmez kutu olarak değil, çağrı bağlamı, değer kopyası, referans erişimi ve nesne yaşamı arasındaki ilişkiyle düşünmek daha sağlıklı bir başlangıçtır.