.NET Memory Management

C gibi Low level diller ile C#, Java gibi high level dilleri ayıran önemli özelliklerden biri low level dillerde belleğe direkt erişimimiz var ve bellek üzerinde manipülasyonlar yapma imkanımız var iken high level dillerde yazdığımız kodlar önce arada bulunan CLR yapısı ile bytelara çevrilip sonrasında makina koduna çevrilir. CLR ise bize bazı güvenlik, configurasyon ve memory management özellikerini hazır olarak verir. 

Direkt makina belleğine erişebildiğimiz dillere unmanaged code, arada CLR gibi bir katmanın daha bulunduğu dillere ise managed code denir. Cloud ile ilgilenenleriniz varsa bilir. Cloud’da bir sistem direkt olarak cloud provider tarafından yönetiliyorsa, buna fully managed denir. Yani sen best practicelere ve işlere kafa yorma cloud bunu senin için halledecek gibi. Burada da managed code için aynı şeyi söyleyebiliriz. Yazı memory management üzerine olduğu için memory management üzerinden ilerleyeceğim. Bildiğiniz gibi low level dillerde çoğu bellek manipülasyonunu kendimiz yaparken high level managed code‘lar bize bellek numaraları için vakit kaybetme onu ben hallediyorum diyor. Yani benim tarafımdan managed edilecek diyor.

Hepimiz .nette Garbage collector olduğunu ve belli aralıklarla kullanılmayan objeleri temizlediğini bir şekilde bir yerden duymuşuzdur. Ancak bu garbage collectionu sisteme dahil etmek için ne using ile bir namespace dahil ediyoruz ne de bir paket yüklüyoruz. Peki nereden geliyor bu garbage collector? CLR’dan. Peki nedir bu garbage collector. Tam olarak ne yapar ve nasıl yapar? Bu yazımda bunları açıklayacağım ancak garbage collectore geçmeden bir kaç tanım ve sonrasında alanlarını anlatmak yararlı olacaktır. 

Memory Leak : Bellekte ayrılmış fakat serbest bırakılması unutulmuş alan
Memory Corruption : Silinen objeyi kullanma çabası
Memory Error : Allocate edilmemiş objeyi silme çabası

STACK ve HEAP

  1. STACK

Stack alanı  değer tipli primitive local değişkenleri tutan alandır. Ancak tuttuğu yapılar sadece bununla sınırlı değil. Stack bir method call edildiğinde metodun içerisindeki az önce söylediğim şartlara uyan local değişkenleri, metodun return adresini ve metot parametrelerini tutar.

   – Local değişkenler
   – Return adresleri
   – Method parametreleri

Methodun belli parçalarından oluşan bu üçlü bir arada stack frame olarak adlandırılır. Yani siz bir fonksiyonu her call ettiğinizde stack’e yeni bir stack frame yerleşir. Return edip hayatını sonlandıran her metot için de bu stack frame stackten silinir. Tam da bu noktada ek bir bilgi vermek istiyorum. Belki kulağınıza recursion function kullanmanın performanssız olduğu ve olabildiğince kaçınılması gerektiği gibi bir bilgi gelmiş olabilir. Sebebi de tam olarak bu. Siz bir fonksiyon içerisinde aynı fonksiyonu tekrar tekrar call ettiğinizde, her seferinde stack’e bir stack frame daha eklenir ve bir süre sonra stack over flow exception hatası alma ihtimaliniz artar almasanız bile belleği müsrif kullanmış olursunuz.

2. HEAP

Bir nesne yaratmaya kalktığımızda bu işlem iki kısımdan oluşur. Birincisi decleration, ikincisi ise creation kısmı. Örnek üzerinden gitmek gerekirse, Person adında bir classınız olduğunu ve nesnesini yaratmak istediğinizi varsayalım.

Person p = new Person();

Buradaki Person p kısmı decleration, new Person kısmı ise creation kısmıdır ve Heap nesnelerin creation kısmını tutan bellek alanıdır. Peki decleration kısmı o nerede tutuluyor? O da stackte tutuluyor. Peki aralarındaki ilişki nasıl kuruluyor? Stackteki decleration kısmı heapteki creation kısmına refere eder.  Hepimiz kodumuzu yazarken çokca null reference exception hatası almışızdır. Bu hatayı, null bir objenin propertysine ulaşmak istediğimiz zaman alırız. Şimdi bu bilgiler ışığında arkadaki olayı biraz daha detaylandıralım.

Person p =  null;

gibi bir nesnemiz olsun. Bu nesnenin stack’e yerleşmiş bir decleration kısmı varken, heap’e yerleşmiş bir creation kısmı yoktur. Dolayısıyla stackte, heapi refere etmesi gereken kısım nulldır. Dolayısıyla siz p.Name gibi bir değişkene ulaşmak istediğinizde alacağınız hata Null Reference Exceptiondır. Yani benim stackte bir declerationım var ama bunun heapteki referensı null… Null reference exception…

Peki şimdi buradan farklı bir konuya atlayalım. Her fonksiyon call edildiğinde içerisindeki local değişkenler bellekte yaşamaya başlar ve fonksiyon işini tamamladığında scope’u bittiği için kullanılamaz hale gelir. Madem değişkenler bu şekilde kullanılamaz hale geliyor o zaman garbage collector neyi temizliyor? Tam olarak görevi ne?

Bir fonksiyonun işi tamamladığı zaman yani scope sonlandığında stackte tutulan ilgili veriler temizlenir. Person isimli nesnemiz üzerinden düşünelim. Bu nesnenin declerationı stackte ancak creationu heapte yer alıyordu ve birbirlerini refere ediyorlardı. Fonksiyon hayatını tamamladığında stackte bulunan decleration kısmı bellekten silinir. Peki ya heapteki creation kısmı? o dereferenced olarak başı boş bir şekilde kalır :).  Garbage collector de tam olarak heapte olan bu başı boş dereferenced nesneleri temizler.

VALUE ve REFERENCE TYPE

1. VALUE TYPE

 Value type yani değer tipli değişkenler. Nesnelerin aksine tipi de değeri de aynı yerde depolanan değişkenlerdir. Genelde value typelar için belleğin stack alanında tutulur derler ancak bu her zaman doğru bir tanım olmaz. Value type’lar hem stackte hem de heapte bulunabilir. Hemen bununla ilgili basit bir örnek düşünelim. 

Aşağıdaki age değişkeni local bir değişken olduğu için evet stackte tanımlanacaktır.

public void Fonksiyon()
{
 int age=28;
}

Aşağıdaki Age value tipli değişkeni ise yaşamını değer tipli bir object içerisinde sürdürdüğü için heapte tutulacaktır.

public class Person
{
   public int Age{get; set;}
}

Person p = new Person();
p.Age= 28;

Değer tipli değişkenleri birbirine atadığımız zaman referansı değil değeri üzerinden atama yapılır ve birbirine atanan değişkenler tamamen birbirinden bağımsızdır.

int a =5;    // a=5
int b = 6;  // b=6

b=a;         // a=5, b=5
a++ ;       // a=6, b =5

Değer tipli değişkenlerin eşitliğini kontrol ederken de sadece değeri baz alınır.

int a=5,b=5;

if(a==b)  //TRUE
{...}

2. REFERENCE TYPE

Reference type ise yukarıda da bahsettiğimiz gibi referensı üzerinden atama yapılan tiplerdir. Genelde decleration kısmının stackte creation kısmının da  heap bölgesinde olduğu söylenir ancak bu da tam anlamıyla doğru değildir. Bu genellemenin yanlışlığını da kısa bir şekilde örneklememiz gerekirse.

public class Person
{
   public Adress Adress{get; set;}
}

Aşağıdaki şekilde bir nesne tanımı yaptığımızda decleration kısmı olan Person p stackte, new Person() kısmı heapte tutulacak. Buraya kadar herşey anlattığım gibi.

Person p = new Person();

Peki ya aşağıdaki tanımlamada durum ne olacak? Bu durumda decleration kısmı da creation kısmı da heapte yer alacak.

p.Adress = new Adress();

Reference tipli değişkenler null atanabilir değişkenlerdir. Bu noktada dolayısıyla string’in de bir reference tip olduğunu söylemiş olalım ve reference tipli değişkenler birbirlerine atandıkları noktada değerler  değil referansları atanır.  Aşağıdaki örnekte person2 değişkeni person1 değişkenine atandığı zaman iki değişken de artık bellekte aynı heap bölgesini refere eder duruma gelir. Dolayısıyla herhangi bir person nesnesinde yapılan değişiklik diğerini de etkiler. Çünkü bu nesneler aslında aynı creation part’ına sahipler.

Person person1 = new Person();

Person person2 = person1;

Reference tipli değişkenler üzerinde eşitlik sorgusu yapılırken de kontrol edilen, bellekteki adresleridir. Dolayısıyla, birebir aynı property ve değerleri taşıyan iki Person nesnesi heapte farklı bölgeleri refere ediyorsa yani creationları farklıysa farklı nesneler olarak ele alınır diyebiliriz.

BOXING ve UNBOXING

Konuya ilişkin bir diğer önemli konsept de boxing ve unboxingtir. Aslında boxing ve unboxing’e dair bütün terminolojiyi detaylarıyla beraber yukarıdaki başlıklarda açıkladığım için burada direkt olarak boxing ve unboxingi tanımlayabilrim.

Değer tipli bir değişkenin referans tipli bir değişkene dönüştürülmesi işlemi boxing, referans tipli bir değişkenin de değer tipli bir değişkene dönüştürülmesi unboxingtir.

int a =1234;

object b = a; //Boxing

int c = (int)b; //Unboxing

Ancak yine yukarıdaki bilgiler ışığında bu tiplerin bellekte farklı alanlarda tutulduğunu biliyoruz. O zaman bu dönüşüm nasıl gerçekleşiyor sorusu akıllara geliyor. Her bir boxing bir değişkenin stackten heap’e taşınması her unboxing ise heapten stack’e taşınması demektir ki bu da costu yüksek bir işlemdir. Dolayısıyla boxing-unboxing yapmadan önce bu  costun varlığının farkında olmakta fayda var.

You may also like...

4 Responses

  1. 22 Ekim 2023

    […] detayları için .Net Memory Management yazımı okumanızı tavsiye […]

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir