Berk Akademi
Ana Sayfa
ÖZEL KODLAMA DERSLERİ
Yazılım Özel Ders
Tüm birebir programlara genel bakış
Python Yazılım Kursu
Sıfırdan ileri seviyeye birebir Python
Java Yazılım Kursu
OOP odaklı birebir Java eğitimi
AP Computer Science Principles
AP CSP sınav hazırlığı
GRUP DERSLERİ
Python & Django Masterclass
SINIRLI KONTENJAN
Java & Spring Boot Masterclass
SINIRLI KONTENJAN
C# .NET Masterclass
SINIRLI KONTENJAN
VİDEO DERSLER
Sıfırdan Temel Python Kursu
Kendi Hızında Öğren
ÜCRETSİZ
Seviye Testi
Ücretsiz — Python, Java, algoritma seviye testleri
Kariyerini Keşfet
Sertifika Doğrula
Belge numarası ve soyad ile doğrulama

Stack ve Heap Bellek Modeli: Değişkenler Nerede Tutulur?

stack-heap-bellek-modeli-degiskenler-nerede-tutulur
Bu yazıda neler var?
  1. Stack ve Heap Nedir? Bellekteki Temel Ayrım
  2. Fonksiyon Çağrılınca Stack Nasıl Büyür ve Küçülür?
  3. Heap, Nesneler ve Referanslar Nasıl İlişkilidir?
  4. Python ve Java’da Aynı Temel Fikri Kodla Görelim
  5. Stack ve Heap Farkları: Karşılaştırma ve Çöp Toplama
  6. Yaygın Yanlışlar ve Bellek Modelini Anlama Kontrol Listesi
  7. Sık Sorulan Sorular

Stack ve heap, program çalışırken verilerin nasıl organize edildiğini anlamak için kullanılan temel bellek modeli kavramlarıdır. Stack genellikle fonksiyon çağrılarını, çağrı çerçevelerini ve yerel çalışma bilgilerini yönetirken heap, çalışma sırasında oluşturulan ve ömrü fonksiyon sınırlarını aşabilen nesneler için kullanılan alanı anlatır.

Ancak “Değişken nerede tutulur?” sorusunun tek kelimelik bir yanıtı yoktur. Sonuç; kullanılan programlama diline, çalışma zamanına, değişkenin türüne, nesnenin ömrüne ve uygulamanın bellek yönetimine göre değişebilir. Bu nedenle stack ve heap’i gerçek bellek adresleriyle gösterilen, her dilde aynı davranan iki kesin fiziksel bölge gibi değil; programın çalışma mantığını açıklayan öğretici bir model gibi düşünmek gerekir.

Stack ve Heap Nedir? Bellekteki Temel Ayrım

Bir program çalışırken bütün veriler tek bir yerde tutulmaz. Fonksiyonların hangi sırayla çağrıldığı, hangi fonksiyona geri dönüleceği ve o anda kullanılan yerel adların yönetilmesi için stack adı verilen düzenli bir çalışma alanı düşünülür. Bunun yanında, programın çalışma sırasında oluşturduğu nesneler ve dinamik ömürlü veriler için heap adı verilen daha esnek bir alan kullanılır.

Stack’i üst üste konmuş tepsilere benzetebilirsiniz. Her fonksiyon çağrıldığında yeni bir tepsi eklenir. Fonksiyon çalışmasını tamamladığında en üstteki tepsi kaldırılır. Bu nedenle stack, “son eklenen önce çıkar” mantığıyla çalışır. Bir fonksiyon başka bir fonksiyonu çağırdığında çağıran fonksiyonun bilgileri stack’te kalır; çağrılan fonksiyonun bilgileri ise onun üstüne eklenir.

Heap ise tek bir çağrı sırasına göre kullanılan tepsilerden çok, nesnelerin bulunduğu geniş bir çalışma alanı gibi düşünülebilir. Program yeni bir nesne oluşturduğunda bu nesne heap üzerinde yer alabilir. Bir değişken doğrudan nesnenin kendisini değil, o nesneye ulaşmayı sağlayan bir referansı taşıyabilir. Böylece birden fazla ad aynı nesneye işaret edebilir veya bir nesne, onu oluşturan fonksiyon tamamlandıktan sonra da kullanılmaya devam edebilir.

Bu anlatımda “yer alabilir” ifadesi önemlidir. Stack ve heap, programlama dillerinin çalışma mantığını açıklamak için yaygın biçimde kullanılan kavramsal ayrımlardır. Gerçek bir uygulamada derleyici, yorumlayıcı veya çalışma zamanı sistemi farklı optimizasyonlar uygulayabilir. Bazı veriler doğrudan tutulabilir, bazıları başka bir nesneye referans verebilir ve bazı çağrı bilgileri beklenenden farklı bir biçimde yönetilebilir.

Stack hangi tür bilgileri yönetir?

Stack’in temel görevi fonksiyon çağrılarını düzenlemektir. Bir fonksiyon çağrıldığında onun için bir çağrı çerçevesi oluşturulduğu düşünülür. Bu çerçevede genellikle şu bilgiler bulunur:

  • Fonksiyona gönderilen parametreler,
  • Fonksiyon içinde tanımlanan yerel adlar ve değişkenler,
  • Fonksiyonun çalışması için gereken geçici bilgiler,
  • İşlem tamamlandığında hangi noktaya dönüleceği.

Örneğin bir ana fonksiyonun önce bir sayıyı hazırlayıp ardından yardımcı bir fonksiyonu çağırdığını düşünelim. Yardımcı fonksiyon çalışırken ana fonksiyonun çağrı çerçevesi stack’te kalır. Yardımcı fonksiyon tamamlandığında en üstteki çerçeve çıkarılır ve program ana fonksiyondaki kaldığı noktaya döner.

Heap hangi tür bilgileri yönetir?

Heap, özellikle çalışma sırasında oluşturulan nesneler için kullanılan kavramsal alandır. Bir listenin, metin nesnesinin, sınıf örneğinin veya daha büyük bir veri yapısının oluşturulması buna örnek olabilir. Bu nesnelerin ömrü her zaman tek bir fonksiyon çağrısıyla sınırlı değildir.

Bir heap nesnesine ulaşmak için kullanılan referans ise başka bir yerde tutulabilir. Örneğin bir fonksiyon içinde oluşturulan liste, fonksiyonun yerel adı ortadan kalksa bile başka bir değişken veya veri yapısı tarafından işaret ediliyorsa kullanılmaya devam edebilir. Bu durumda yerel adın yaşamı ile nesnenin yaşamı aynı şey değildir.

Programlama dillerinde bu ayrımın nasıl uygulandığı değişir. Python’da sayılar, metinler, listeler ve fonksiyonlar da nesne olarak ele alınır. Java’da ise ilkel tipler ile referans tipleri arasında dil düzeyinde belirgin bir ayrım bulunur. Fakat bu ayrımı “Python’daki her şey kesin olarak heap’tedir” veya “Java’daki her ilkel değer kesin olarak stack’tedir” şeklinde yorumlamak doğru değildir. Dil kuralları ile çalışma zamanının gerçek uygulaması aynı düzeyde değerlendirilmemelidir.

Bu nedenle bellek modelini öğrenirken önce şu soruya odaklanmak daha sağlıklıdır: “Bu bilgi bir fonksiyon çağrısının geçici çalışma bağlamına mı ait, yoksa başka adlar tarafından erişilebilen daha kalıcı bir nesne mi?” uygulamalı video yazılım eğitimleri gibi kaynaklarda bu zihinsel ayrımı küçük örneklerle tekrar etmek, yalnızca terimleri ezberlemekten daha kalıcı bir anlayış sağlar.

“Değişken nerede tutulur?” sorusu neden tek yanıtlı değildir?

Bir değişken adı ile değişkenin işaret ettiği değer veya nesne aynı şey olmayabilir. Basit bir sayı için dil, çalışma zamanı ve optimizasyonlar farklı davranışlar sergileyebilir. Bir liste veya sınıf örneği içinse adın tuttuğu şey çoğu zaman nesnenin kendisinden çok nesneye ulaşmayı sağlayan bir referans olarak düşünülür.

Ayrıca “yerel değişken” ifadesi de otomatik olarak “stack üzerinde duran bağımsız değer” anlamına gelmez. Yerellik, öncelikle değişkene hangi kapsamdan erişilebildiğini anlatır. O değişkenin temsil ettiği değer veya nesnenin bellekte nasıl yönetileceği ise programlama dilinin ve çalışma zamanının kurallarına bağlıdır.

Bu bölümde kullanılacak zihinsel modeli şöyle özetleyebiliriz:

  • Stack: Fonksiyon çağrılarının sırasını ve her çağrının geçici çalışma çerçevesini izlemeye yarar.
  • Heap: Çalışma sırasında oluşturulan, referanslarla erişilebilen nesneleri düşünmek için kullanılır.
  • Referans: Bir adın veya değişkenin nesnenin kendisi yerine o nesneye ulaşma bağlantısını temsil edebilir.
  • Gerçek uygulama: Bu model, her dilde ve her çalışma zamanında fiziksel belleği birebir gösteren evrensel bir harita değildir.

Dolayısıyla stack ve heap’i iki değişmez fiziksel kutu gibi düşünmek yerine, fonksiyon çağrıları ile nesne yaşamını anlamaya yarayan soyutlama olarak kullanmak gerekir. Şimdi bu modelin en görünür kısmını, bir ana fonksiyonun yardımcı bir fonksiyonu çağırması üzerinden adım adım izleyelim.

Fonksiyon Çağrılınca Stack Nasıl Büyür ve Küçülür?

Fonksiyon Çağrılınca Stack Nasıl Büyür ve Küçülür?

Bir fonksiyon çağrıldığında stack üzerinde yeni bir çağrı çerçevesi oluştuğu varsayılır. Bu çerçeve, fonksiyonun hangi parametrelerle çalıştığını, hangi yerel adlara sahip olduğunu ve işini bitirdiğinde nereye döneceğini takip eder. Fonksiyon başka bir fonksiyon çağırırsa yeni çerçeve mevcut çerçevenin üzerine eklenir.

Aşağıdaki senaryoda ana() fonksiyonu, hesapla() adlı yardımcı fonksiyonu çağırıyor. hesapla() içinde kullanılan sonuc adlı değer için heap üzerinde bir nesne bulunduğunu ve stack çerçevesindeki sonuc adının bu nesneye referans verdiğini düşünelim. Şemalar, gerçek bellek adreslerini değil, kavramsal ilişkiyi gösterir.

1. Çağrı öncesi: ana fonksiyonun çerçevesi eklenir

Program başladığında önce ana() çağrılır. Bu çağrı için stack’e bir çerçeve eklenir. Henüz hesapla() çağrılmadığı için onun çerçevesi stack’te bulunmaz.

STACK
┌──────────────────────────────┐
│ ana() çerçevesi              │
│ sayı = 6                     │
│ dönüş noktası: program sonu  │
└──────────────────────────────┘

HEAP
┌──────────────────────────────┐
│ henüz hesapla() nesnesi yok  │
└──────────────────────────────┘

Bu aşamada eklenen çerçeve ana() çerçevesidir. Programın başlangıç akışı bu çerçeve üzerinden ilerler. ana() içinde bir yardımcı fonksiyon çağrısı görüldüğünde mevcut çerçeve silinmez; çağrılan fonksiyonun çalışabilmesi için onun üstüne yeni bir çerçeve eklenir.

2. Yardımcı fonksiyon çalışırken: yeni çerçeve üstte yer alır

ana(), hesapla(6) çağrısını yaptığında stack’e hesapla() çerçevesi eklenir. Bu çerçevede değer parametresi ve fonksiyon içinde tanımlanan sonuc adı bulunur. sonuc adı, örneğin 12 değerini temsil eden bir heap nesnesine referans verir.

STACK
┌──────────────────────────────┐
│ hesapla() çerçevesi          │
│ değer = 6                    │
│ sonuc ───────────────────────┼────┐
│ dönüş noktası: ana()         │    │
└──────────────────────────────┘    │
┌──────────────────────────────┐    │
│ ana() çerçevesi              │    │
│ sayı = 6                     │    │
│ dönüş noktası: program sonu  │    │
└──────────────────────────────┘    │
                                      ▼
HEAP
┌──────────────────────────────┐
│ Nesne A: 12                  │
└──────────────────────────────┘

Burada hesapla() çerçevesi en son eklenen çerçevedir ve stack’in üstünde bulunur. ana() çerçevesi ise altta kalır; çünkü çağıran fonksiyon, çağrılan fonksiyon tamamlanana kadar beklemektedir. sonuc adı ile heap’teki 12 nesnesi arasındaki ok, değişken adının nesneye referans verdiğini anlatır.

Bu şema, parametrelerin ve yerel adların her zaman doğrudan heap nesnesiyle aynı şey olmadığını da gösterir. sonuc stack çerçevesinin parçası olarak düşünülürken, onun işaret ettiği nesne heap üzerinde kavramsallaştırılmıştır. Bu ayrım, özellikle nesneler birden fazla ad tarafından paylaşılabildiğinde önem kazanır.

3. Dönüş anı: en son eklenen çerçeve önce çıkarılır

hesapla() işlemini tamamlayıp 12 sonucunu döndürdüğünde, onun çağrı çerçevesi stack’ten çıkarılır. Çıkarılan çerçeve hesapla() çerçevesidir; çünkü stack mantığında en son eklenen çerçeve önce kaldırılır. Program daha sonra ana() çerçevesine geri döner.

STACK
┌──────────────────────────────┐
│ ana() çerçevesi              │
│ sayı = 6                     │
│ sonuç = 12                   │
│ dönüş noktası: program sonu  │
└──────────────────────────────┘

HEAP
┌──────────────────────────────┐
│ Nesne A: 12                  │
│ ana() içindeki sonuç adı     │
│ bu nesneye başvurabilir      │
└──────────────────────────────┘

Bu aşamada kaldırılan çerçeve hesapla() çerçevesidir. ana() çerçevesi ise çağrı öncesinde eklendiği için hâlâ stack’tedir. Döndürülen değer, çalışma zamanının kurallarına göre çağıran fonksiyonun kullanabileceği biçimde aktarılır. Heap’teki 12 nesnesinin yaşamı ise yalnızca hesapla() çerçevesinin kaldırılmasıyla açıklanmaz; bu nesneye hâlâ başka bir referans olup olmadığı da önemlidir.

Çağrı sırasının tersine yerleşim nasıl çalışır?

Fonksiyonlar iç içe çağrıldığında stack’teki düzen çağrı sırasının tersine görünür. Örneğin ana(), hesapla() fonksiyonunu; hesapla() da dogrula() fonksiyonunu çağırırsa çerçeveler şu sıraya gelir:

STACK'İN ÜSTÜ
┌──────────────────────────────┐
│ dogrula() çerçevesi          │  ← en son eklendi, ilk çıkar
├──────────────────────────────┤
│ hesapla() çerçevesi          │
├──────────────────────────────┤
│ ana() çerçevesi              │  ← ilk eklendi, en son çıkar
└──────────────────────────────┘
STACK'İN ALTI

Önce dogrula() tamamlanır ve onun çerçevesi çıkarılır. Ardından akış hesapla() içine döner. hesapla() de tamamlandığında kendi çerçevesi çıkarılır ve program ana() içindeki dönüş noktasına gelir. Son olarak ana() tamamlandığında onun çerçevesi de kaldırılır.

Bu nedenle stack’in büyüyüp küçülmesini anlamanın en kolay yolu çağrı zincirini takip etmektir: çağrı geldikçe çerçeve eklenir, dönüş oldukça en üstteki çerçeve çıkarılır. Heap nesneleri ise bu çağrı sırasından bağımsız olarak, onlara ulaşan referansların durumuna göre yaşamaya devam edebilir veya artık erişilemez hâle gelebilir.

Heap, Nesneler ve Referanslar Nasıl İlişkilidir?

Heap’i, program çalışırken dinamik olarak oluşturulan nesnelerin yaşam alanını anlamak için kullanılan kavramsal bir bölge gibi düşünebilirsiniz. Bir liste, dizi, sınıf örneği veya başka bir karmaşık veri yapısı oluşturulduğunda, programın bu nesneye daha sonra ulaşabilmesi gerekir. Değişken adı ise çoğu zaman nesnenin kendisi değil, o nesneye ulaşmayı sağlayan bir adlandırma veya referans eşleşmesidir.

Örneğin Python’daki liste = [10, 20] ifadesini iki parçaya ayırarak düşünmek daha doğrudur:

  • [10, 20], oluşturulan liste nesnesidir.
  • liste, bu nesneye ulaşmak için kullanılan isimdir.
  • =, ismi nesneyle ilişkilendiren atama işlemidir.

Bu nedenle “liste değişkeninin içinde doğrudan iki sayı var” demek başlangıç seviyesinde pratik bir anlatım olsa da bellek modelini anlamak için eksiktir. Daha doğru zihinsel model şudur: liste adı, liste nesnesine ulaşır. Python dokümantasyonunda da isimlerin nesnelere bağlandığı ve atamanın nesneyi kopyalamak yerine ismi mevcut nesneyle ilişkilendirdiği açıklanır. Ayrıntılı teknik tanım için Python dokümantasyonu incelenebilir.

Liste adı ile liste nesnesi aynı şey değildir

Aşağıdaki şema, liste = [10, 20] ifadesini öğretim amaçlı sadeleştirir:

İsimler / referanslar                 Heap'teki nesne

liste  --------------------------->  Liste nesnesi
                                      [10, 20]

Burada liste adı, listenin içeriğinin kendisi gibi düşünülmemelidir. İsim, nesneye ulaşma yoludur. Bu ayrım özellikle nesneler değiştirilebilir olduğunda önem kazanır. Listeye yeni bir eleman eklendiğinde çoğu durumda aynı liste nesnesinin içeriği değiştirilir; liste adı farklı bir listeye bağlanmış olmaz.

Bir başka değişkeni aynı nesneye bağlarsak iki farklı isim, tek bir nesneyi gösterebilir:

İsimler / referanslar                 Heap'teki nesne

liste  --------------------------
                                    >  Liste nesnesi
diger  --------------------------/    [10, 20]

Python’da bunun karşılığı şu şekildedir:

liste = [10, 20]
diger = liste

diger.append(30)

print(liste)
print(diger)

Beklenen çıktı:

[10, 20, 30]
[10, 20, 30]

Çıktının iki satırda da aynı olmasının nedeni, liste ve diger isimlerinin iki ayrı liste oluşturmaması, aynı liste nesnesine ulaşmasıdır. diger.append(30) işlemi nesnenin içeriğini değiştirir. Değişikliği yapan isim diger olsa da değişen nesneye liste üzerinden de ulaşılabilir.

Bir referansı başka nesneye bağlamak

Bir ismi başka bir nesneye yeniden bağlamak, ilk nesnenin içeriğini değiştirmekten farklıdır:

liste = [10, 20]
diger = liste

diger = [50, 60]

print(liste)
print(diger)

Beklenen çıktı:

[10, 20]
[50, 60]

Bu durumda başlangıçta iki isim aynı listeye bağlıdır. Ancak diger = [50, 60] satırından sonra diger yeni oluşturulan ikinci listeye bağlanır. liste ise ilk listeyi göstermeye devam eder.

İlk durum:

liste  --------------------------
                                    >  [10, 20]
diger  --------------------------/


İkinci durum:

liste  ------------------------------>  [10, 20]

diger  ------------------------------>  [50, 60]

Bu iki işlem arasındaki farkı şöyle özetleyebiliriz:

  • diger.append(30): Aynı listenin içeriğini değiştirir.
  • diger = [50, 60]: diger adını yeni bir liste nesnesine bağlar.

Bu ayrım; fonksiyonlara liste gönderirken, sınıf örneklerini paylaşırken ve birden fazla bölümün aynı veri üzerinde çalıştığı programlarda sık sık karşınıza çıkar.

Python’da “Her değişken stack’te, her nesne heap’te” demek neden fazla katıdır?

Stack ve heap şemaları, programın çalışma mantığını kavramak için oldukça kullanışlıdır. Ancak bu şemalar Python’un dil kurallarının bütün fiziksel bellek yerleşimini kesin biçimde tanımladığı anlamına gelmez. Python’da sayılar, dizeler, listeler, fonksiyonlar ve sınıf örnekleri nesne olarak modellenir. İsimler bu nesnelere bağlanır; nesnelerin nerede ve hangi iç düzenle tutulduğu ise kullanılan Python uygulamasının çalışma biçimine bağlı olabilir.

Bu nedenle “Python’da her değişken kesinlikle stack’tedir, her nesne kesinlikle heap’tedir” cümlesi güvenilir bir dil kuralı gibi kullanılmamalıdır. Başlangıç seviyesinde daha güvenli ifade şudur: Fonksiyon çağrıları ve yerel bağlamlar stack modeliyle, dinamik nesneler ve onlara ulaşan referanslar heap modeliyle açıklanabilir. Bu, davranışı anlamaya yarayan bir modeldir; her Python uygulamasının fiziksel belleğini gösteren garanti edilmiş bir harita değildir.

Java’da ilkel değer ve referans türü ayrımı

Java’da öğretim amacıyla iki temel değer grubu arasında ayrım yapılır: ilkel türler ve referans türleri. int, double ve boolean gibi ilkel türler doğrudan ilkel değerlerle ilişkilendirilir. Sınıf örnekleri, diziler ve benzeri nesneler için kullanılan türlerde ise değişken, nesnenin kendisine yönelik bir referans değeri taşır.

int sayi = 10;
int[] dizi = {10, 20};

Bu kodu sadeleştirilmiş bir şemayla şöyle gösterebiliriz:

Yerel değişkenler                    Heap'teki nesne

sayi  = 10

dizi  ---------------------------->  int[] nesnesi
                                      [10, 20]

Java Dil Spesifikasyonu, ilkel türlerle referans türlerini ayrı değer kategorileri olarak tanımlar; referans değerleri sınıf örneklerine veya dizilere ulaşmayı sağlar. Bununla birlikte, Java Sanal Makinesi’nin gerçek çalışma zamanındaki yerleşimi için tek bir fiziksel düzen zorunlu değildir. Yerel değişkenlerin, çağrı çerçevelerinin ve nesnelerin nasıl temsil edildiği sanal makine uygulamasına ve çalışma zamanı optimizasyonlarına göre soyutlanabilir. Bu ayrım için Java Dil Spesifikasyonu temel başvuru kaynağıdır.

Dolayısıyla Java için de stack ve heap şeması bir öğretim aracıdır. “Dizi nesnesi heap’tedir, dizi değişkeni ise ona ulaşan referanstır” anlatımı kavramsal olarak yararlıdır; ancak her uygulamanın iç yerleşimini bu basit şemaya birebir eşitlememek gerekir.

Python ve Java’da Aynı Temel Fikri Kodla Görelim

Python ve Java’da Aynı Temel Fikri Kodla Görelim

Stack ve heap ayrımını yalnızca tanımlarla öğrenmek yerine, gözlemlenebilir program davranışı üzerinden incelemek daha sağlamdır. Aşağıdaki örneklerde fiziksel bellek adresi yazdırılmıyor. Bunun yerine yerel değişkenin fonksiyon içinde oluşturulması, listenin fonksiyona aktarılması, aynı nesnenin değiştirilmesi ve dönüş değerinin dışarı taşınması gösteriliyor.

Python örneği: Liste nesnesi fonksiyonda değiştiriliyor

def ekle_ve_dondur(veriler):
    gecici = 5
    veriler.append(gecici)
    return veriler

sayilar = [10, 20]
sonuc = ekle_ve_dondur(sayilar)

print(sayilar)
print(sonuc)

Beklenen çıktı:

[10, 20, 5]
[10, 20, 5]

Bu örnekte programın akışını adım adım izleyelim:

  1. sayilar = [10, 20] ifadesi bir liste nesnesi oluşturur ve sayilar adını bu nesneyle ilişkilendirir.
  2. ekle_ve_dondur(sayilar) çağrısı yapılırken fonksiyonun veriler parametresi aynı liste nesnesine bağlanır.
  3. gecici = 5, fonksiyonun yerel bağlamında kullanılan yerel bir isimdir.
  4. veriler.append(gecici), fonksiyona gelen liste nesnesinin içeriğini değiştirir.
  5. Fonksiyon aynı liste nesnesini döndürür ve bu nesne sonuc adıyla da ilişkilendirilir.

Öğretim amaçlı stack/heap şeması, fonksiyon çalışırken şu şekilde düşünülebilir:

Fonksiyon çağrısı sırasında stack çerçevesi

veriler  ---------------------------
gecici = 5                           
                                       >  Liste nesnesi
sayilar ----------------------------/    [10, 20, 5]

Fonksiyon döndükten sonra

sayilar ----------------------------
                                       >  [10, 20, 5]
sonuc  -----------------------------/

Fonksiyon sona erdiğinde, fonksiyona ait çağrı çerçevesi ve veriler ile gecici gibi yerel isimler artık o çağrının parçası olmaktan çıkar. Ancak liste nesnesine sayilar ve daha sonra sonuc üzerinden ulaşılabildiği için nesnenin yaşamı devam eder.

Buradaki önemli nokta, Python’un “listeyi kopyalayıp fonksiyona gönderdiği” varsayımının doğru olmamasıdır. Fonksiyon parametresi, mevcut nesneye bağlanan yeni bir isim gibi davranır. Bu nedenle liste gibi değiştirilebilir nesnelerde yapılan içerik değişikliği çağıran kod tarafından da görülebilir. Python temellerini bu tür davranışlar üzerinden yapılandırılmış biçimde geliştirmek isteyenler için birebir Python dersleri kapsamında değişken, nesne ve fonksiyon ilişkisi uygulamalı olarak çalışılabilir.

Java örneği: İlkel değer ile dizi referansı

public class BellekOrnegi {
    static void degistir(int sayi, int[] dizi) {
        sayi = 99;
        dizi[0] = 99;
    }

    public static void main(String[] args) {
        int sayi = 10;
        int[] dizi = {10, 20};
        degistir(sayi, dizi);
        System.out.println(sayi);
        System.out.println(dizi[0]);
    }
}

Beklenen çıktı:

10
99

Çıktının bu şekilde oluşmasının nedeni, Java’da ilkel değer ile dizi referansının farklı davranmasıdır. degistir metoduna sayi gönderildiğinde, 10 değerinin bir kopyası metodun parametresine aktarılır. Metot içinde sayi = 99 yapılması yalnızca metodun kendi yerel parametresini değiştirir. main içindeki sayi değişkeni bu yüzden 10 olarak kalır.

dizi için ise metoda, aynı dizi nesnesine ulaşmayı sağlayan referans değeri aktarılır. Metot içindeki dizi[0] = 99 işlemi, bu referansın gösterdiği ortak dizi nesnesinin ilk elemanını değiştirir. Metot tamamlandıktan sonra main içindeki dizi hâlâ aynı nesneye ulaştığı için ilk elemanın yeni değeri 99 olarak görülür.

degistir metodu çalışırken kavramsal görünüm

sayi  = 10  --değerin ayrı kopyası-->  yerel sayi = 99

dizi  ------------------------------
                                      >  Dizi nesnesi
yerel dizi -------------------------/   [99, 20]

Bu örnek “Java’da her şey değer olarak mı, referans olarak mı aktarılır?” sorusuna da dikkatli bir cevap verir: Java’da metot parametreleri değer olarak aktarılır. İlkel türlerde aktarılan değer, ilkel değerin kendisidir. Dizi veya sınıf örneği gibi nesnelerde aktarılan değer ise nesneye ulaşmayı sağlayan referans değeridir. Bu nedenle nesnenin içeriği değişebilir; fakat parametreyi başka bir nesneye bağlamak, çağıran taraftaki değişkeni otomatik olarak başka nesneye bağlamaz.

Python ve Java örneklerini birlikte nasıl okumalı?

İki dilin sözdizimi ve tür sistemi farklı olsa da temel düşünce benzerdir: Bir isim veya değişken, programın daha sonra kullanacağı bir değere ya da nesneye ulaşmayı sağlar. Fark, bu ilişkinin dil tarafından nasıl modellenip sunulduğundadır.

Durum Python Java
Liste veya dizi oluşturma Liste nesnesi oluşturulur ve bir isim ona bağlanır. Dizi nesnesi oluşturulur ve referans türündeki değişken ona bağlanır.
Fonksiyona/metoda gönderme Parametre, mevcut nesneye bağlanan yeni bir isim gibi düşünülebilir. Parametreye referans değerinin kopyası aktarılır.
Nesnenin içeriğini değiştirme Değiştirilebilir nesneye yapılan değişiklik diğer isimlerden görülebilir. Aynı nesneye ulaşan referanslardan değişiklik görülebilir.
Değişkeni yeniden bağlama İsim başka bir nesneyle ilişkilendirilebilir. Referans değişkeni başka bir nesneye yönlendirilebilir.
İlkel türler Python’daki değerler nesne modeli içinde ele alınır. int ve boolean gibi ilkel türler ayrı bir kategori oluşturur.

Bu karşılaştırmayı ezberlenecek fiziksel adres kuralları olarak değil, program davranışını tahmin etmeyi sağlayan bir karar modeli olarak kullanın. Bir kod parçasını incelerken önce şu soruları sormak genellikle yeterlidir:

  • Burada yeni bir nesne mi oluşturuluyor, yoksa mevcut nesneye başka bir isimle mi ulaşılıyor?
  • Yapılan işlem nesnenin içeriğini mi değiştiriyor, yoksa değişkeni başka nesneye mi bağlıyor?
  • Fonksiyon veya metot sona erdiğinde nesneye ulaşan başka bir isim ya da referans kalıyor mu?
  • Java’da ilgili değişken ilkel tür mü, yoksa referans türü mü?
  • Python’da kullanılan nesne değiştirilebilir mi, yoksa değiştirilemez bir nesne mi?

Bu sorular, stack ve heap kavramlarını yalnızca “değişkenler burada, nesneler orada” şeklinde yüzeysel bir şemaya indirgemeden anlamayı sağlar.

Stack ve Heap Farkları: Karşılaştırma ve Çöp Toplama

Stack ve heap ayrımı, program çalışırken verilerin nasıl organize edildiğini anlamak için kullanılan yararlı bir modeldir. Ancak bu modeli, her dilde ve her çalışma zamanında fiziksel belleğin değişmez yerleşim planı gibi düşünmemek gerekir. Başlangıç seviyesinde en güvenli yaklaşım; stack’i fonksiyon çağrıları ve çağrı çerçeveleriyle, heap’i ise çalışma sırasında oluşturulan nesneler ve bu nesnelere ulaşmayı sağlayan referanslarla ilişkilendirmektir.

Özellik Stack Heap
Temel amaç Fonksiyon çağrılarının ve çağrıya ait geçici bilgilerin düzenlenmesine yardımcı olur. Çalışma sırasında oluşturulan nesnelerin ve dinamik verilerin yönetilmesine yardımcı olur.
Fonksiyon çağrılarıyla ilişki Bir fonksiyon çağrıldığında genellikle o çağrıya ait bir çerçeve oluşur; dönüşte bu çerçevenin yaşamı sona erer. Fonksiyon içinde oluşturulan bir nesne, fonksiyon döndükten sonra da başka bir referans kaldığı sürece yaşamaya devam edebilir.
Verilerin yaşam süresi Çağrı çerçevesinin yaşam süresiyle yakından ilişkili olabilir. Belirli bir fonksiyona bağlı olmak zorunda değildir; nesneye ulaşılabildiği sürece devam edebilir.
Erişim mantığı Çalışan fonksiyonun yerel bağlamı ve çağrı zinciri üzerinden düşünülür. Değişken adlarının, alanların veya diğer yapıların tuttuğu referanslar üzerinden düşünülür.
Başlangıç seviyesinde dikkat edilmesi gereken nokta Her yerel değişkenin fiziksel olarak kesinlikle stack üzerinde bulunduğu varsayılmamalıdır. Heap’in her zaman yavaş, sınırsız veya tamamen geliştiricinin doğrudan yönettiği bir alan olduğu düşünülmemelidir.

Bu tablo, dillerin ve çalışma zamanlarının ortak kavramsal ayrımını gösterir. Gerçek uygulamada derleyici, yorumlayıcı veya sanal makine; bazı değerleri farklı biçimlerde optimize edebilir. Bu nedenle “stack kesinlikle şu tür verileri tutar, heap kesinlikle yalnızca şu tür verileri tutar” şeklindeki katı cümleler yerine, verinin yaşam süresi ve erişim ilişkisi üzerinden düşünmek daha doğrudur.

Çöp toplama neyi temizler?

Heap üzerinde oluşturulan bir nesneye programın herhangi bir aktif referans zinciri üzerinden ulaşılamıyorsa, o nesne artık programın normal akışı içinde kullanılamaz kabul edilir. Örneğin bir fonksiyon içinde oluşturulan listeyi dışarıya döndürmez ve başka bir değişken de bu listeyi göstermezse, fonksiyon sona erdikten sonra listeye ulaşan bir yol kalmayabilir.

Bu noktada nesne mutlaka aynı anda bellekten silinmiş demek değildir. Daha doğru ifade, nesnenin çöp toplama için uygun hâle gelmiş olmasıdır. Çöp toplama mekanizması, çalışma zamanının kendi kurallarına göre erişilemeyen nesneleri belirli bir zamanda inceleyebilir ve kullandıkları alanı yeniden kullanılabilir hâle getirebilir.

Python’da nesnelerin erişilebilirliği ve referans ilişkileri önemlidir. Python’un belirli uygulamalarında referans sayımı, erişilemeyen nesnelerin önemli bir bölümünün hızlı biçimde ele alınmasına yardımcı olur; döngüsel referanslar için ayrıca çöp toplama mekanizması kullanılabilir. Python belgeleri, nesnelerin erişilemez hâle geldiğinde çöp toplanabileceğini ancak uygulamanın nesnelerin hemen sonlandırılmasına güvenmemesi gerektiğini belirtir. Ayrıntılar için Python Data Model belgeleri incelenebilir.

Java’da ise nesneler, sanal makinenin erişilebilirlik kurallarına göre değerlendirilir. Bir nesneye programın erişebildiği aktif bir yol kalmadığında nesne çöp toplama açısından uygun aday hâle gelebilir. Bunun ne zaman gerçekleşeceği, kullanılan Java çalışma zamanının kararlarına bağlıdır; bir referansın kopması, nesnenin aynı satırda anında silindiği anlamına gelmez.

Bu yüzden çöp toplama ile açık kaynak kapatma işlemlerini birbirine karıştırmamak gerekir. Bir dosya, ağ bağlantısı veya veritabanı bağlantısı gibi dış kaynakların kapanması için programın açıkça close() çağırması ya da Java’daki try-with-resources veya Python’daki with yapısı gibi uygun kaynak yönetimi tekniklerini kullanması gerekebilir. Bellekteki nesnenin daha sonra temizlenmesi, dış kaynağın zamanında serbest bırakılacağını garanti etmez.

Yaygın Yanlışlar ve Bellek Modelini Anlama Kontrol Listesi

Bellek modeli, yalnızca teorik bir konu değildir. Algoritma adımlarını, fonksiyonların birbirini çağırmasını ve nesneler arasındaki ilişkileri takip ederken bu model doğrudan işe yarar. Temel programlama kavramlarını pekiştirmek isteyen okuyucular, önce algoritma bilgi testi ile değişken, koşul, döngü ve fonksiyon mantığındaki mevcut durumunu ölçebilir.

Aşağıdaki kontrol listesi, stack ve heap konusunda sık yapılan aşırı genellemeleri daha dikkatli ifadelerle düzeltir:

  • Yanlış: Her yerel değişken kesinlikle stack’tedir.
    Daha doğru ifade: Yerel değişkenler çoğu anlatımda çağrı çerçevesiyle ilişkilendirilir; ancak gerçek yerleşim, dilin ve çalışma zamanının uygulama ayrıntılarına veya optimizasyonlarına bağlı olabilir.
  • Yanlış: Her nesne doğrudan değişkenin içinde bulunur.
    Daha doğru ifade: Değişken çoğu zaman nesnenin kendisini değil, nesneye ulaşmayı sağlayan bir referansı veya değeri temsil eder. Özellikle listeler, diziler, sınıf örnekleri ve sözlükler gibi yapılarda bu ayrım önemlidir.
  • Yanlış: Referans nesnenin kendisidir.
    Daha doğru ifade: Referans, nesneye ulaşmayı sağlayan bağlantı veya yönlendirme olarak düşünülebilir. Aynı nesneye birden fazla ad veya alan referans verebilir.
  • Yanlış: Fonksiyondan dönünce tüm veriler silinir.
    Daha doğru ifade: Fonksiyonun çağrı çerçevesi sona erebilir; fakat fonksiyonun dışına döndürülen ya da başka bir yerde hâlâ referansı bulunan nesneler yaşamaya devam edebilir.
  • Yanlış: Heap her zaman daha yavaştır.
    Daha doğru ifade: Erişim maliyeti ve performans; dil, çalışma zamanı, veri yapısı, bellek düzeni, optimizasyonlar ve programın erişim biçimi gibi birçok etkene bağlıdır. Stack veya heap etiketinden tek başına kesin performans sonucu çıkarılmaz.
  • Yanlış: Çöp toplama, erişilemeyen nesneyi anında kaldırır.
    Daha doğru ifade: Erişilemeyen nesne temizlenmeye uygun hâle gelebilir; temizlemenin zamanı ve yöntemi kullanılan dil ile çalışma zamanına bağlıdır.

Bir bellek sorununu nasıl teşhis edebilirsiniz?

Bir fonksiyonun beklenenden fazla veri tuttuğunu, nesnelerin neden hâlâ bellekte kaldığını veya bir değerin neden değiştiğini anlamaya çalışırken aşağıdaki sırayı izlemek işe yarar:

  1. Hangi fonksiyon çalışıyor? Önce programın hangi fonksiyonda ve hangi satıra yakın olduğunu belirleyin.
  2. Hangi çağrı çerçevesi eklendi? Yeni bir fonksiyon çağrıldıysa, çağrı zincirine yeni bir çalışma bağlamının eklendiğini düşünün.
  3. Hangi ad hangi nesneye referans veriyor? Değişkenin nesnenin kendisi mi, yoksa nesneye giden bir referans mı olduğunu ayırın.
  4. Nesneye başka referans var mı? Fonksiyon sona erse bile listeyi, sözlüğü veya sınıf örneğini gösteren başka bir ad, alan ya da koleksiyon kalmış olabilir.
  5. Dönüşten sonra hangi referanslar kaldı? Fonksiyonun döndürdüğü değerleri, dış kapsamda atanan değişkenleri ve nesneler arasındaki iç bağlantıları kontrol edin.

Örneğin bir fonksiyon aşağıdaki gibi bir liste oluşturuyorsa, listeyi fonksiyon dışına döndürdüğünüz durumda çağrı sona erdikten sonra da erişilebilir olabilir:

def sayilari_olustur():
    sayilar = [10, 20, 30]
    return sayilar

sonuc = sayilari_olustur()
print(sonuc)

Beklenen çıktı şöyledir:

[10, 20, 30]

Burada sayilar adı fonksiyonun çağrı bağlamında kullanılır. Fonksiyon döndüğünde bu adın yerel bağlamdaki yaşamı sona erer; fakat listeyi döndüren değer sonuc adına bağlandığı için listeye ulaşılabilen bir referans kalır. Bu nedenle “fonksiyon bitti, listedeki veri kesinlikle silindi” demek doğru değildir.

Tersine, fonksiyon listeyi dışarıya döndürmezse ve başka hiçbir ad bu listeyi göstermiyorsa, nesne erişilemez hâle gelebilir. Bu durumda çalışma zamanı nesneyi temizlemeye uygun kabul edebilir; fakat temizliğin tam olarak hangi anda gerçekleşeceği hakkında kodun normal akışına dayanarak kesin söz vermek doğru olmaz.

Python ve Java’yı karşılaştırırken de aynı kontrol soruları kullanılabilir. Önce çağrı bağlamını, sonra ad ile nesne arasındaki ilişkiyi, son olarak da nesneye ulaşan referansların devam edip etmediğini inceleyin. Böylece “bu kesin stack’tedir” veya “bu kesin heap’tedir” demek yerine, programın o anda neye erişebildiğini daha güvenilir biçimde açıklayabilirsiniz.

Sık Sorulan Sorular

Bir değişken her zaman stack'te mi tutulur?

Hayır. Başlangıç seviyesinde yerel değişkenler çoğu zaman fonksiyonun çağrı çerçevesiyle ilişkilendirilerek anlatılır; ancak değişkenin fiziksel olarak her durumda stack üzerinde bulunduğu söylenemez. Dilin çalışma biçimi, derleyici veya çalışma zamanı optimizasyonları bu ayrımı etkileyebilir.

Stack ile heap arasındaki en temel fark nedir?

Stack, fonksiyon çağrıları ve bu çağrılara ait geçici çalışma bağlamlarıyla ilişkilendirilir. Heap ise çalışma sırasında oluşturulan ve yaşam süresi tek bir fonksiyon çağrısıyla sınırlı olmak zorunda olmayan nesnelerle ilişkilendirilir. Bu, her dil için değişmez fiziksel bellek kuralı değil; programı anlamayı kolaylaştıran temel bir modeldir.

Bir referans silinince heap'teki nesne hemen silinir mi?

Hayır. Bir referansın silinmesi yalnızca nesneye giden yollardan birini kaldırır. Başka referanslar varsa nesne erişilebilir kalır. Hiçbir erişilebilir referans kalmadığında nesne çöp toplama için uygun hâle gelebilir; temizleme işleminin hemen gerçekleşmesi gerekmez.

Python ve Java bellek modelleri neden birebir aynı anlatılamaz?

Çünkü Python ve Java; nesne oluşturma, referansları yönetme, çağrı bağlamlarını yürütme ve çöp toplama süreçlerini farklı kurallarla ve farklı çalışma zamanlarıyla uygular. Ortak kavramlar kullanılabilir; ancak Python’daki her şeyin nesne olması ile Java’daki ilkel tip ve referans tipi ayrımı aynı cümlelerle açıklanmamalıdır.

Stack ve heap’i anlamanın en sağlam yolu, her fonksiyon çağrısında hangi bağlamın oluştuğunu ve hangi referansların nesnelere ulaşmaya devam ettiğini adım adım izlemektir.

Bu içerik aradığın cevabı verdi mi?
Yanıtın, hangi yazıları geliştirmemiz gerektiğini anlamamıza yardımcı olur.
Bu içeriğin üretilmesinde yapay zeka araçlarından destek alınmıştır.

Bu konudan sonra ne okuyabilirsin?

Tüm yazılar

İlgili Eğitimler

Berk Keskin — Yazılım Geliştirici ve Eğitmen
Yazar

Berk Keskin Kimdir?

Yazılıma 12 yaşında başladı; bugün öğrencinin seviyesine ve hedefine göre şekillenen sürdürülebilir öğrenme sistemleri tasarlıyor. 500'den fazla kişiye ezber değil, düşünerek kod yazmayı öğretti — Berk Akademi'de izlemeye değil üretmeye dayalı öğrenme kültürünü o kuruyor.

WhatsApp Hemen Ara