Programlama dillerinde bellek alanı kaba tasvirle ikiye ayrılır; benim “İstif” (Stack) ve “yığın” (Heap)
İstif (yani Stack), işine sabah 9 akşam 5 arası çalışan memura özenen verilerin barındığı hafıza alanıdır. Burada her veri sırayla işleme girer ve sırayla da ortadan kaybolur. Buradaki verilerin hesaplanması kolaydır çünkü boyutları daha program yazılmadan önceden bellidir.
Mesela ki, C dilinde bir bildiğimiz sayıya tekabül eden “Int” tipinden bir verinin boyutu “4” bayt kadardır. Aynı şekilde, “double” tipli ondalıklı sayı ise ise “8” bayttır. Elbette, 16 bitlik işletim sistemlerinin kullanıldığı zamanlarda bir Int’in boyutu 4 değil 2 idi. Swift’te bir Int’in değeri 8 bayta terfi etmiş olsa da eski köye yeni adet getirmemek için C dilinde Int hâlen daha 4 bayttır.
Ancak, her veri İstif alanındaki veriler gibi düzenli ve huzurlu bir hayat yaşamazlar, çok daha spontane bir hayatları vardır. Mesela programlama dillerinde metinleri temsil eden “String” tipinin tam boyutu belli değildir. Niye? Çünkü bir stringin uzunluğu bellidir. Bir Int’in alabileceği maksimum ve minimum değer bellidir. Ama ya String? Kaç tane harf, boşluk veya noktalama işareti kullanabileceğimizi tahmin eden kahin bir derleyici çıkmadığı sürece stringlerin boyutu belirsiz kabul edilir.
Tabii, String tipinin boyutunun belli kabul edildiği durumlar da vardır. Bu stringlerin ne kadar uzunlukta olduğu program kodlanırken belirtilir ve derleyici de tıpkı alelade bir sayı belirtmişsiniz gibi onları istif alanına yerleştirir. Ancak, çoğu zaman bu gayet kullanışsızdır. Mesela ki kullanıcının ismini soyisimini sorduğumuz bir program yapacağız ve bunu “İsimSoyisim” adında bir değişkene aktaracağız. Bunun için bellekten yirmi baytlık yer ayırdık ama adamın adı “Ali Gel”. Toplasan yedi bayt ediyor. Geri kalan on üç bayt bellekte boş boşuna yer işgal etmiyor mu şimdi? Daha fenası, adam adıyla sanıyla “Lazaros Christodoulopoulos”. Siz bu isim için yirmi baytlık yer ayırırsanız, altı bayt açıkta kalıyor demek. Ve bu dayığındaki gelişigüzel bir veriyi de değiştirebileceği anlamına geliyor. Nasıl, evlere şenlik değil mi?
Elbette ki böyle bir riski göze almak veyahut lüzumsuz yere bellek tutmak yerine mis gibi yığın (Heap) alanımızı kullanacağız ve derleyiciye “Abi boyutun ne kadar tutacağını bilmiyorum, sen bildiğin gibi et hadi görüşürüz bay” demiş olacağız. Derleyici de “Bana ne yahu senin verinden?” diyerek topu programın çalışma atmış olacak ve işte tam bu sırada yığın alanı devreye girecek.
İstif her ne kadar düzenli bir yaşam sürdüren memur ruhlu verilerin yeriyse, yığın (Heap) da tam aksine asi bir anarşist hayatı yaşayan verilerin veridir. Buradaki veriler hizaya girmektense darmadağınık bir şekilde birikir ve kontrol edilmezlerse bilgisayarın hafızasının canına okurlar. Nasıl, ürktünüz değil mi? İşte burada yaşayan ipsiz sapsız verileri işi bittiği zaman bellekten kovmak için kabaca üç farklı bellek yönetimi yaklaşımı vardır:
Elle yönetim: Daha fazlasını isteyen genç ve şehvetli programcıların tercihi. Çoğu durumda en performanslı ancak güvensiz bir çözümdür.
Mülkiyet (Ownership) prensibi: Programlama dillerini tasarlayan kişilere “Biz bunu daha önce niye düşünemedik?” diye saç baş yolduran efsanevi çözümdür. Neredeyse elle yönetim kadar performanslı olmasına rağmen en güvenli çözümdür. Not: Beyin yakar.
Çöp toplayıcıları iki başlıkta incelenir:
Referans sayımı (Reference Counting): Kulağa çok siyasi bir şeymiş gibi gelse de kesinlikle öyle bir şey yoktur. Bir de bunun otomatik referans sayımı isimli bir kardeşi var, fenadır.
İz Süren Çöp toplayıcısı: Programcının dostu, çöp verinin düşmanı, Fenerbahçe’nin futbolcu sanıp teklif götürdüğü bellek temizleme yaklaşımı
1 – Elle yönetim
Bilgisayar programcılarının sayıca az ama kalite olarak yüksek olduğu o tatlı zamanlardan kalma bellek yönetim stilidir. Yurdum gençlerinin “Dil dediğin C’dir, C++’dır heyt!” temalı koşuklarının ana unsurudur. Bu yöntemde, yığın alanı elle oluşturulur ve buna giden referanslar da elle sayılır.
C programlama dilinde bu işi idare etmek için şöyle cici fonksiyonlar vardır:
malloc(): Memory allocation – Bellek tahsisi. Bir eğlencesi yoktur, direkt bellek alanını ayırır.
calloc(): Clear allocation – Temiz tahsis. Belleği ayırmadan önce bir ortalığı temizler ki sakatlık çıkmasın. Malloctan daha güvenli ancak çok çok az bir farkla daha yavaş bir fonksiyondur.
realloc(): Daha önce tahsis edilmiş bellek alanını tekrardan tahsis eden, bu sayede başka amaçlara kullanılmasını izin veren bir fonksiyondur. Güvenli midir? Yaaaani…
free(): Tahsis edilen belleğin derhal temizlenmesini sağlayan fonksiyondur.
Bu yöntem hâlen daha yüksek donanım gerektiren oyunlar, işletim sistemleri gibi performans bakımından belli başlı kritik yerlerde kullanılsa da son dönem programlama anlayışı yazılımcıya bellek yönetimini bırakmaktan yana değildir. Görünüşte gayet kolay olsa da büyük projelerde kafa karışıklığını arttıracağı, üstelik referansların bağlantısının da gereken durumlardan tekrar temizlenmesini gerektireceği için hatayamüsaittir.
Başlıca programlama dilleri: C, C++ ve D (İkincil tercih olarak)
2- Mülkiyet prensibi
Zaman içerisinde programlama anlayışı değişti, artık yazılımcılar daha görür görmez kodu anlayan üstün analitik zihinli mahkulatlar olarak değil de normal senin benim gibi insanlar olarak anlaşılmaya başlandı ve hâliyle şöyle bir şekline şemaline baktığımız zaman bir şeye benzetebileceğimiz programlama dilleri ortaya çıktı. C/C++’tan çok daha öncesine dayanan elle bellek yönetimi mantığı da yerini “otomatik bellek yönetimi” yaklaşımlarına bıraktı.
Bunlardan birisi, yazılımcıyı kesin bir şekilde bellek yönetiminden ayıran çöp toplayıcılarıdır. Ancak çöp toplayıcısının her işte kullanılmasına engel olan yönleri vardır ve bu işler için yeni bir alternatif gerekmektedir. Tabii ki işletim sistemi çekirdeği tasarlamak veyahut teknolojinin bütün verdiği nimetleri kullanan oyunları yazmak gibi çok çok ileri düzey işlerden bahsediyorum. Yoksa çöp toplayıcılı dillere can kurban.
Mülkiyet prensibi, yığındaki kontrol etmekten ziyade yığındaki veriyi mümkün olduğunca kendi hâline bırakmayı amaçlar. Esas amaç, yazılımcıya belleği sınırsızca kontrol etmek yerine doğru ve kusursuz bir bellek yönetimi yaptırmaktır. Mülkiyet prensibinin tersine giden kodlar derleyici tarafından reddedilir.
Mülkiyet prensibinin ortaya çıktığı bir dil olarak Rust, yazılımcıya o durum içerisindeki “en iyi” kodu bir Ney hocası edasıyla döve döve yazdırır. Kimi zamanlar yazılımcı “en kolay iş için en kötü durumun senaryosunu yazıyoruz” diyerek isyan etse de acıdan zevk almaya alışmış nice Rust programcısı bu durumdan gayet memnundur.
Yazıyı uzatmamak için mülkiyet prensibinin ana hatlarını anlatan bir yazı bırakıyorum, okuyun okumayın artık size kalmış:
https://github.com/RustDili/Rust-Mulkiyet-Kavrami
Başlıca programlama dilleri: Rust, D (İkincil tercih), C++ (Kısmen, akıllı işaretçiler sayesinde biraz biraz)
3- Çöp Toplayıcılar
Çöp toplayıcısı gerek Python gibi oldukça yüksek seviyeli diller olsun gerekse D gibi sistem programlama dilleri olsun çoğu tarafta genel kabul görmüş bir bellek yönetim anlayışıdır. Bu bellek yönetimi biçiminde yazılımcıya ya çok az bellek yönetimi payı bırakır ya da hiç bırakmaz. Ki çoğu zaman gc.clear() gibi pek çok işi bir anda yapan fonksiyonlar çağrılırsa çağılır ancak pek tercih edildiğine denk gelmedim.
Çöp toplayıcısının esas amacı kolaylıkmış gibi görünse de Java ve C# programlama dillerinde tercih edilmesinin sebebi kolaylıktan ziyade kod güvenliğini sağlamaktır.
Bu arada söz etmeliyim ki kod güvenliğinden kasıt “kodların gizlenebilmesi” değildir. Öngörülemeyen davranışların ve bellek sızıntısının önlenmesidir. Bu iş “Aman ne olur ki, ben dikkatli kodlarım olur biter” mevzusu da değildir. Sabaha kod yetiştirmeye çalışan bir kişinin acelesinden kaynaklanan bir anlık dikkatsizlik, yazılımı kullanacak şirketin milyon liralar kaybetmesine sebep olabilir. Üstelik projenin büyüklüğüne göre buna sebep olan kod bulanamayabilir bile!
Programlama dillerini tasarlayan kişilerin yaklaşımı şudur; projeleri böyle adamlara teslim etmektense gidelim kendimiz detaylanmış güvenlik algoritmaları yazalım ve herkesin kafası rahat olsun.
Hâliyle bilgisayar teknolojileri matematiksel bir alan olmaktan çıkıp günlük hayatın merkezine yerleştiği anda bellek yönetimleri de “çöp toplayıcısı” ismiyle otomatikleştirilmişti. Henüz yeni bir anlayış olan “Mülkiyet prensibi”nin oraya çıkışına dek daha güvenli bir bellek yönetim yaklaşımı ortaya çıkmadı. Mülkiyet prensibini en iyi uygulayan Rust dilinin pratik zorluklarını düşünürsek, çöp toplayıcılarının programlama sahnesinden çıkmasının epey bir yolu var.
Çöp toplayıcısı, sadece tek tip bir yapıymış gibi tarif edilse de aslında dilden dile dolaşan çeşitli yapılar ve farklı özelleştirmeler vardır. Bizim de tabii ilgilenmemiz gereken başka başka işlerimiz olduğu için bunlardan en çok kullanılan iki yaklaşımı üstün körü geçeceğiz: Referans sayımı (Referance Counting) ve İz Süren Çöp Toplama (Tracing Garbage Collection).
3.1- Referans Sayımı (Referance Counting)
Aslında yığındaki pek çok veri farklı farklı alanlara kopyalanacağına bir alanda tutulur, pek çok değişken/fonksiyon da oraya erişir. Pek çok programlama dilinin merkezinde bu anlayış vardır. Rust’ın mülkiyet prensibi belleğin bir alanını mümkün olduğunca kullanmak üzerine kuruluyken C gibi dillerde işaretçiler (pointerlar) aracılığıyla bellek verisi kopyalanmak yerine doğrudan erişilir.
Hiç bilmeyen birisi için kulağa Fransızca gibi geldiğini biliyorum, hani böyle tanıdık ama “Sütü seven kamyoncu” tadında bir çağrışım yaratıyor. Yine de örneklendirmeyi deneyelim; Siz bir Whatsapp grubunda alışveriş notlarınızı saklıyorsunuz diyelim. Bir dünya şey, vazelinden patlıcana kadar epey geniş bir liste hazırlamışsınız. Ayaküstü saymak zor. Birisi gelip size soruyor, abi/abla alınacakların listesi nerede? İşiniz yoksa yeni bir liste yazabilirsiniz. Peki ya alışveriş listesine on kişi falan bakacak olursa? Hepsine tek tek liste çıkartmaktansa “Git gruptaki listeye bak” da diyebilirsiniz.
Referans sayımı da, bellekteki ilgili alana erişen referansların sayımıdır. Eğer bellekteki alana erişen hiçbir referans kalmazsa, yığındaki veri de derhal imha edilir. Az önceki örneğimizi şöyle sürdürebiliriz; alınacaklar alındı ve bir kişi bile listeye bakmıyor; daha da bakmayacak. O zaman neden grupta boşu boşuna yer tutuyor ki bu liste? Kaldırın gitsin.
Başlıca dilleri: PHP, Python, Perl, C++ (Akıllı işaretçiler aracılığıyla), Rust (“RC” işaretçisi aracılığıyla)
3.1.1- Otomatik Referans Sayımı (Automatic Referance Counting ya da kısaca Arc)
Referans sayımı dediğimiz şey zaten otomatik. Yani korkmanıza gerek yok. Peki neden buna otomatik demişler ki?
Otomatik Referans Sayımı aslında bir Apple icadı. Adamlar oturmuşlar kafa yormuşlar, “Yav biz Objective-C gibi kafa seven bir dilde oturup neden elle referans sayıyoruz ki? Bellek ihtiyacını azaltalım, mümkün olduğunca az bellekle iş görelim ama çekilecek dert mi bu? Bunu bir algoritmaya bağlayalım, tertemiz işimizi görsün biz kodumuza bakalım. Olmaz mı?” Adamlar zaten uyguladıkları pratikleri sistematikleştirerek mümkün olan en az bellek kullanımını gerçekleştiren bir yaklaşım geliştirmiş, kişisel olarak takdir etmiyor değilim.
Evet ne diyorsun? Neden otomatik? Çünkü referans sayımı işleminde “Hadi bununla işimiz bitti, kaldıralım atalım” gibi bir yaklaşım yok. Bütün referansların aradan çıkması beklenir ki kimi zaman bu işlem bellekten temizlenmesi gereken verinin işi bittiğ ianda temizlenmemesine sebebiyet verebilir. Yani çöp geç temizlenir, belki de hiç temizlenmez.
Otomatik referans sayımı neden sonuç ilişkisine bağlı olarak (deterministik bir biçimde) belleği kontrol eder. Bu sayede, işi bitmiş olan bellek tam gerektiği anda aradan çıkar. Bu da iOS gibi çok az bir bellek ile çalışması gereken işletim sistemleri için epey fayda getirir (Tabii Android’in bu kadar çok bellek tüketmesinin bellek yönetimi dışında başka sebepleri var, bu başka bir şeyin konusu)
Ancak Otomatik Referans Sayımı, bu kadar iyi bir bellek yönetimine karşın performans konusunda muadillerine göre biraz vasattır çünkü sürekli “Çöp var mı? Çöp geldi mi abi?” diyerek belleğe salça olur. Yine de diğer çöp toplayıcı yaklaşımlarına karşı epey bir artıları vardır. Öyle ki, Otomatik Referans Sayımı çoğu yerde “çöp toplayıcısıymış” gibi anlaşılmaz.
Otomatik referans sayımını sunan başlıca diller: Objective-C, Swift ve Clang derleyicisi
4- İz Süren Çöp toplayıcısı
Çoğu kaynakta çöp toplayıcısı diye anlatılan ve bahsedilen yaklaşım budur. Referans sayımına “Fakirin çöp toplayıcısı” dendiğini de duydum, çünkü veriyi detaylıca yönetmekten çok veriyi yüzeysel olarak “Hadi işi bitti bunun, atalım” anlayışıyla çalışır. Ancak İz Süren Çöp Toplayıcısı, yok edilmiş veriyi tekrardan üretmek gibi hatalara düşmek yerine “Gerekebilecek veriyi her an elde tutmak” prensibini benimser.
Bu yaklaşım çok derinlemesine incelenmesi gereken, birden farklı şekilde yorumlanan bir anlayış olduğu için detaylarını anlatmaya kalksak valla yazıyı bitiremeyiz. Bundan dolayı, en temel çalışma prensibini anlatayım. Zaten sizin de başınız şişmiştir.
İz Süren çöp toplayıcısı veriye gerektiği an ulaşabilmeyi amaçladığından “main” olarak tarif edilen, program kodlarının varolduğu en üst katmanda olan verileri korur. Bu yüzden, bu yaklaşımı kullanan bir dilde mümkün olduğunca fonksiyonları kullanmak bellek yönetimi açısından epey faydalıdır.
“Main” dışında, o an işleyen ortam her neyse onun verileri de bellekte saklanır. Örneğin “falanca” fonksiyonu çalıştı, bu falanca fonksiyonu altındaki bütün sınıf örnekleri de ilklendi. Bu ilklenen örnekler fonksiyonun kapsamından çıkana kadar bellekten silinmeyecektir. Eğer “main” içerisinde veyahut çalışan fonksiyon içerisindeki gereksiz bulunan değerler varsa “Nil”/”Null”/”None” gibi “hiçbir şey” olmayı ifade eden bir değere eşitlenir.
Eğer bellekteki veri erişilemez hâle gelirse işte o zaman çöp toplayıcısı devreye girer ve bu erişilemeyen bellek alanı hemen temizlenir. Böylece siz mutlusunuz, bellek de mutlu ama yığındaki çöp veriler mezarda.
Başlıca dilleri: Ruby, Go, JavaScript, D, C# gibi pek çok ana akım dili.