İçeriğe geç
academia.sh

Ders 13 / 17

Bellek Yaşam Döngüsü

Ayırma, kullanım ve serbest bırakma aşamaları; erişilebilirliğin kök kümesi; eşzamansız kodda zamanlayıcı, dinleyici ve sonuçlanmamış sözlerin bellekte tuttukları.

İçindekiler

Önceki konuda birkaç kez “bellekte kalır” denildi: yarışı kaybeden sözler, iptal edilmeyen istekler, tüketilmeyen tamponlar. Neyin, neden ve ne kadar süreyle kaldığı ise açıklanmadı.

Bu konu onu açıklar. Başlangıç noktası, her programlama dilinde ortak olan üç aşamalı döngüdür; JavaScript’i ayıran, üçüncü aşamanın kime ait olduğudur.

Üç Aşama

Ayırma. Bir değer için yer ayrılır. JavaScript’te bu, açıkça istenmez; bir nesne, dizi, dizgi ya da fonksiyon yazıldığında kendiliğinden olur.

Kullanım. Ayrılan yer okunur ve yazılır. Programın asıl işi budur.

Serbest bırakma. Yer geri verilir. C gibi dillerde bunu program yapar; JavaScript’te çalışma zamanı yapar.

Üçüncü aşamanın otomatik olması, sorunun ortadan kalktığı anlamına gelmez; sorunu dönüştürür. Elle yönetilen bellekte hata “erken serbest bırakmak”tır — Bilgisayarlar Nasıl Çalışır kursundaki sarkan işaretçi. Otomatik yönetimde hata “gereğinden uzun süre başvurmak”tır. İlkinde program çöker, ikincisinde şişer.

Nesneler, aynı kursta tanıtılan bellek düzenindeki öbek (heap) alanında yaşar; yerel değişkenler ise çağrı yığıtındaki çerçevede tutulur. Çerçeve, fonksiyon döndüğünde kaybolur; öbekteki nesne ise ona başvuran kimse kalmadığında toplanabilir hale gelir.

Erişilebilirlik ve Kökler

Çalışma zamanı, bir nesnenin “gerekli olup olmadığını” bilemez. Bunun yerine ölçülebilir bir ölçüt kullanır: erişilebilirlik (reachability). Bir kök kümesinden başlayıp başvuruları izleyerek ulaşılabilen her nesne yaşar; ulaşılamayan her nesne toplanabilir.

Kök kümesi şunlardan oluşur:

  • Yürüyen çağrı yığıtındaki yerel değişkenler ve parametreler
  • Genel kapsamdaki bağlamalar
  • Modül düzeyindeki bağlamalar
  • Bekleyen zamanlayıcıların geri çağrıları
  • Kayıtlı olay dinleyicileri
  • Sonuçlanmamış sözlerin geri çağrıları

Son üçü bu kursun konusudur ve çoğu bellek sorununun kaynağıdır. Bir zamanlayıcı kurulduğunda, geri çağrısı ve o geri çağrının kapattığı bütün değişkenler bir kök haline gelir. Kapanış kavramı JavaScript’te Nesneler ve Fonksiyonlar kursunda tanımlanmıştı; buradaki eklenti, kapanışın yaşam süresini eşzamansız bir olayın belirlemesidir.

Bir Nesne Ne Zaman Toplanabilir

Kural, çalıştırılabilir bir örnekle gösterilebilir. Aşağıdaki program iki nesne üretir: biri yalnız bir fonksiyonun içinde kullanılır, diğeri bir zamanlayıcı geri çağrısı tarafından kapatılır.

Toplama anını gözlemek için iki araç kullanılır. FinalizationRegistry, kaydedilen bir nesne toplandığında bir geri çağrı çalıştırır. Toplamayı elle tetiklemek için de çalışma zamanı ilgili seçenekle başlatılır; aksi halde toplayıcı kendi zamanlamasına göre çalışır ve çıktı belirlenimci olmaz.

const kayitDefteri = new FinalizationRegistry((ad) => {
  console.log("toplandı:", ad);
});

function geciciOlcum() {
  const olcum = { istasyon: "A1", deger: 21.4 };
  kayitDefteri.register(olcum, "geçici ölçüm");
  return olcum.deger;
}

function zamanlayicininTuttugu() {
  const olcum = { istasyon: "B2", deger: 19.8 };
  kayitDefteri.register(olcum, "zamanlayıcının tuttuğu ölçüm");
  return setTimeout(() => console.log("zamanlayıcı çalıştı:", olcum.istasyon), 60);
}

console.log("geçici ölçümün değeri:", geciciOlcum());
const kimlik = zamanlayicininTuttugu();

global.gc();
await new Promise((karar) => setTimeout(karar, 10));
global.gc();
console.log("ilk toplama turu bitti");

clearTimeout(kimlik);
global.gc();
await new Promise((karar) => setTimeout(karar, 10));
global.gc();
await new Promise((karar) => setTimeout(karar, 10));
console.log("zamanlayıcı iptal edildikten sonraki tur bitti");
$ node --expose-gc bellek.mjs
geçici ölçümün değeri: 21.4
toplandı: geçici ölçüm
ilk toplama turu bitti
toplandı: zamanlayıcının tuttuğu ölçüm
zamanlayıcı iptal edildikten sonraki tur bitti

Sonuç iki kuralı birden gösterir. Birinci nesne, fonksiyon döndüğü anda erişilemez hale geldi ve ilk toplama turunda toplandı — döndürülen yalnız bir sayıydı, nesnenin kendisi değil. İkinci nesne, zamanlayıcı beklediği sürece toplanamadı; ancak clearTimeout ile kök ortadan kalktıktan sonra toplandı.

Buradaki --expose-gc seçeneği yalnız gözlem içindir. Uygulama kodunda toplayıcıyı elle çağırmak ne gereklidir ne de doğrudur; kararı çalışma zamanı verir.

Eşzamansız Kökler ve Ölçüm Akışı

Aynı kural, kursun örneğindeki her yapıya uygulanır.

Kendi kendini zamanlayan akış. Zamanlayıcılar konusundaki akisBasla işlevi, her turda yeni bir zamanlayıcı kurar. Akış durdurulmadığı sürece geri çağrı ve kapattığı sayaçlar, tampon, istasyon listesi bellekte kalır. Akışı durduran işlev aynı zamanda bir bellek işlevidir.

İptal edilmeyen istek. Yarış tabanlı zaman aşımında kaybeden söz sonuçlanmaz; sonuçlanmayan sözün geri çağrıları da kök olduğu için, isteğin tuttuğu tampon ve yanıt gövdesi serbest bırakılamaz.

Kaldırılmayan dinleyici. İptal işareti dersinde dinleyici once seçeneğiyle kaydedilmişti. Seçenek olmasaydı dinleyici işaret nesnesine bağlı kalır, o da geri çağrının kapattığı her şeyi tutardı.

Üç örneğin ortak dersi şudur: eşzamansız bir işi başlatmak, aynı zamanda bir kök oluşturmaktır. İşi sonlandıran ya da iptal eden kod, o kökü kaldıran koddur.

Kapanış Neyi Tutar

Bir kapanışın kendi kapsamındaki her değeri tuttuğu sanılır. Çalışma zamanları böyle davranmak zorunda değildir: yalnız iç fonksiyonun gerçekten başvurduğu bağlamalar saklanır, diğerleri sıradan yerel değişken gibi ele alınır.

const toplananlar = [];
const kayitDefteri = new FinalizationRegistry((ad) => {
  toplananlar.push(ad);
});

function kapanisKur() {
  const hamTampon = { ad: "ham ölçüm dizisi" };
  const ozet = { istasyon: "A1", deger: 21.4 };
  kayitDefteri.register(hamTampon, "ham tampon");
  kayitDefteri.register(ozet, "özet");

  return function ozetiVer() {
    return ozet.deger;
  };
}

const ozetiVer = kapanisKur();

global.gc();
await new Promise((karar) => setTimeout(karar, 10));
global.gc();
await new Promise((karar) => setTimeout(karar, 10));

console.log("kapanışın verdiği değer:", ozetiVer());
console.log("toplananlar:", toplananlar.sort().join(", ") || "(yok)");
console.log("ölçüm alanları:", Object.keys(process.memoryUsage()).sort().join(", "));
$ node --expose-gc kapanis.mjs
kapanışın verdiği değer: 21.4
toplananlar: ham tampon
ölçüm alanları: arrayBuffers, external, heapTotal, heapUsed, rss

İki nesne de aynı fonksiyonun yerelleriydi; dönen kapanış yalnız birine başvuruyordu. Başvurulmayan tampon toplandı, başvurulan özet kapanış yaşadığı sürece kaldı.

Buna karşın bu davranış bir güvence değildir; kapanışın hangi bağlamaları sakladığı çalışma zamanının kararıdır. Taşınabilir kural şudur: uzun yaşayacak bir kapanışta yalnız gerekli olanı kapatın. Büyük bir yapıdan tek bir alan gerekiyorsa, kapanışı yapının tamamı yerine o alanla kurmak hem açık hem güvenlidir.

Son satır, bellek kullanımının hangi eksenlerde raporlandığını gösterir. Değerleri makineye ve yüke göre değiştiği için burada yalnız alan adları yazdırıldı: rss sürecin işletim sisteminden aldığı toplam bellek, heapTotal ayrılmış öbek boyutu, heapUsed öbekte gerçekten kullanılan bölüm, external ile arrayBuffers ise JavaScript öbeği dışında tutulan ikili veridir. Sızıntı takibinde izlenen alan genellikle heapUsed değeridir.

Serbest Bırakma Ne Zaman Görünür

Bir nesnenin erişilemez hale gelmesi ile belleğin gerçekten geri verilmesi aynı an değildir. Toplayıcı kendi zamanlamasına göre çalışır; ölçülen bellek kullanımı erişilemezlikten hemen sonra düşmeyebilir.

Bunun iki pratik sonucu vardır. Birincisi, “bellek düşmedi” gözlemi tek başına sızıntı kanıtı değildir; ölçüm birden çok toplama turu boyunca tekrarlanmalıdır. İkincisi, FinalizationRegistry geri çağrısının ne zaman — hatta çalışıp çalışmayacağı — güvence altında değildir; program sonlanırken hiç çalışmayabilir. Bu nedenle kaynak temizliği oraya yazılmaz; temizlik açık bir kapatma çağrısıyla ya da finally bloğuyla yapılır.

Özet

  • Bellek yaşam döngüsü ayırma, kullanım ve serbest bırakma aşamalarından oluşur; JavaScript’te üçüncüsünü çalışma zamanı yapar.
  • Otomatik yönetim sarkan işaretçi hatasını ortadan kaldırır, yerine “gereğinden uzun başvurma” hatasını koyar.
  • Bir nesne, kök kümesinden erişilemez hale geldiğinde toplanabilir; kökler arasında bekleyen zamanlayıcılar, kayıtlı dinleyiciler ve sonuçlanmamış sözler de vardır.
  • Eşzamansız bir iş başlatmak bir kök oluşturur; iptal ya da sonlandırma o kökü kaldırır.
  • Kapanış yalnız başvurduğu bağlamaları saklayabilir; uzun yaşayan kapanışlarda gereksiz değer kapatmamak taşınabilir kuraldır.
  • Erişilemezlik ile belleğin geri verilmesi aynı an değildir; sonlandırıcı geri çağrılar temizlik için kullanılamaz.

Sonraki Adım

Bu derste toplayıcı bir kara kutu olarak kullanıldı: “erişilemeyeni toplar” denildi ve elle tetiklendi. Sonraki ders kutuyu açar. Erişilebilirliğin nasıl hesaplandığı, başvuru sayma yönteminin çevrimlerde neden başarısız olduğu, kuşak varsayımının toplayıcıyı nasıl hızlandırdığı ve zayıf başvuru yapılarının bu düzenekte hangi boşluğu doldurduğu ele alınacak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat