JavaScript'te belleği siz ayırmaz ve siz serbest bırakmazsınız: motor bir değer oluşturduğunuzda bellek ayırır, o değere artık hiçbir yoldan ulaşılamadığında çöp toplayıcı (garbage collector, GC) onu geri alır. Modern motorların hepsi "işaretle ve süpür" (mark-and-sweep) yaklaşımını kullanır. Bu yüzden bellek sızıntısı, aslında artık ihtiyacınız olmayan bir nesneye hâlâ ulaşılabilir bir referans bırakmak demektir.

Otomatik yönetim, bellek konusunu düşünmeye gerek olmadığı izlenimi verebilir. Uzun süre çalışan bir Node.js sunucusunda ya da tek sayfalık bir uygulamada ise küçük bir unutkanlık, saatler içinde büyüyen bir bellek kullanımına dönüşebilir. Bu yazıda yaşam döngüsünü, erişilebilirlik kavramını, WeakMap, WeakRef ve FinalizationRegistry araçlarını ve dil düzeyindeki tipik sızıntı kalıplarını görüyorsunuz.

Belleğin yaşam döngüsü

Belleğin yaşam döngüsü

Hangi dil olursa olsun döngü aynıdır: belleği ayır, kullan (oku ve yaz), işin bitince serbest bırak. C gibi dillerde birinci ve üçüncü adım açıkça yazılır. JavaScript'te ise ayırma değer oluşturulurken kendiliğinden olur; sayı, metin, nesne, dizi ve fonksiyon tanımlamak bellek ayırır. new Date() veya concat() gibi yeni değer döndüren çağrılar da yeni bellek ister.

Zor olan üçüncü adımdır: bir bellek parçasına artık gerçekten ihtiyaç olup olmadığını genel olarak bilmek mümkün değildir. MDN bunu karar verilemez (undecidable) bir problem olarak tanımlar. Çöp toplayıcılar bu yüzden daha dar ama hesaplanabilir bir tanım kullanır.

Referans sayma ve mark-and-sweep

Eski ve basit yaklaşım referans saymadır: bir nesneyi gösteren hiçbir referans kalmadığında nesne toplanabilir sayılır. Sorun döngüsel referanslardır. İki nesne birbirini gösterirse sayıları hiç sıfıra inmez. MDN'nin notuna göre artık hiçbir modern JavaScript motoru referans sayma kullanmıyor.

javascript
function olustur() {
  const a = {};
  const b = {};
  a.diger = b;
  b.diger = a; // döngüsel referans
  return "bitti";
}
console.log(olustur()); // bitti
// Fonksiyon bittikten sonra a ve b köklerden erişilemez, toplanabilir.

Mark-and-sweep yaklaşımında çöp toplayıcı kök nesnelerden (global nesne gibi) başlar, buradan ulaşılabilen her nesneyi işaretler ve işaretlenmeyenleri toplar. "Gerekli değil" tanımı "erişilemez" olarak daraltılmıştır. Döngüsel referanslar sorun olmaktan çıkar, çünkü a ve b birbirini gösterse de köklerden onlara giden bir yol kalmamıştır. Kuşaksal (generational), artımlı ve eşzamanlı toplama gibi iyileştirmeler bu algoritmanın uygulama düzeyindeki geliştirmeleridir.

Erişilebilirlik tablosu

Erişilebilirlik tablosu: Değişken, dizi, Map, Set, WeakMap, WeakSet, WeakRef, FinalizationRegistry
YapıNesneyi nasıl tutarNe zaman kullanılır
Değişken, dizi, Map, SetGüçlü referans; nesne yaşamaya devam ederNormal veri saklama
WeakMap, WeakSetAnahtarı zayıf tutar; anahtar başka yerde yoksa kayıt toplanabilirNesneye ek bilgi iliştirmek
WeakRefHedefi zayıf tutar; deref() undefined dönebilirYeniden üretilebilen büyük verinin önbelleği
FinalizationRegistryHedefi zayıf tutar; toplandıktan sonra callback çağrılabilirKritik olmayan temizlik

WeakMap ile nesneye ek bilgi iliştirmek

WeakMap anahtarlarını zayıf tutar. Anahtar olarak kullandığınız nesneye programın başka hiçbir yerinden ulaşılamıyorsa kayıt da değeriyle birlikte toplanabilir hale gelir. Bu nedenle anahtar yalnızca nesne ya da kayıtsız bir symbol olabilir ve WeakMap üzerinde döngü kurulamaz. Ayrıntılı API için Map, Set ve WeakMap yazısına bakabilirsiniz.

javascript
const ekBilgi = new WeakMap();
let kullanici = { ad: "Deniz" };
ekBilgi.set(kullanici, { sonGiris: "2026-11-05" });
console.log(ekBilgi.get(kullanici)); // { sonGiris: '2026-11-05' }

kullanici = null;
// Nesneye başka referans yoksa kayıt da toplanabilir.

new WeakMap().set("metin", 1);
// TypeError: Invalid value used as weak map key

WeakRef ve FinalizationRegistry: dikkatli kullanın

WeakRef ve FinalizationRegistry: dikkatli kullanın: Yeni oluşturduğunuz ya da deref() ile aldığınız hedef, o anki iş (job) bitene..., deref() hiç undefined döndürmeyebilir; motor...

WeakRef, hedef nesneyi toplanmaktan alıkoymayan bir referanstır. deref() hedef hâlâ yaşıyorsa onu, toplanmışsa undefined döndürür. FinalizationRegistry ise kaydettiğiniz bir değer toplandıktan sonra bir temizlik callback'i çağrılmasını istemenizi sağlar.

javascript
const hedef = { veri: "büyük nesne" };
const zayif = new WeakRef(hedef);
console.log(zayif.deref() === hedef); // true
console.log(zayif.deref()?.veri); // büyük nesne

new WeakRef(Symbol.for("kayitli"));
// TypeError: WeakRef: invalid target  (kayıtlı symbol toplanamaz)

MDN bu iki API için açık bir uyarı yapar: mümkünse kullanmayın. Çöp toplamanın ne zaman, nasıl ve hatta olup olmayacağı motora bağlıdır. Aynı anda erişilemez hale gelen iki nesneden biri çok daha geç toplanabilir; motor, kapanışlar (closure) veya iç önbellekler yüzünden erişilemez görünen bir şeyi tutmaya devam edebilir. Şu kurallar sayfada açıkça yer alır:

  • Yeni oluşturduğunuz ya da deref() ile aldığınız hedef, o anki iş (job) bitene kadar toplanmaz; toplanmayı ancak olay döngüsünün turları arasında görebilirsiniz.
  • deref() hiç undefined döndürmeyebilir; motor nesneyi hiç toplamamaya karar verebilir.
  • Temizlik callback'i hiçbir zaman eşzamanlı çağrılmaz; geç çağrılabilir ya da hiç çağrılmayabilir. Program kapanırken veya registry nesnesinin kendisi erişilemez olduğunda çağrılması beklenmez.
  • Kesin bir temizlik gerekiyorsa try...finally gibi deterministik yapılar kullanın.
javascript
const kayit = new FinalizationRegistry((etiket) => {
  console.log("toplandı:", etiket);
});
let nesne = { ad: "geçici" };
kayit.register(nesne, "geçici nesne");
nesne = null;
console.log("ana iş bitti"); // ana iş bitti
// "toplandı: ..." satırı geç gelebilir ya da hiç gelmeyebilir.

Bu örneği Node.js 24'te --expose-gc ile çalıştırıp gc() çağırdığımızda bile callback mesajı yazılmadı. Bu, belgedeki "garanti yok" ifadesinin pratikteki karşılığıdır: uygulama mantığınızı bu callback'e bağlamayın.

Dil düzeyinde tipik sızıntı kalıpları

Sızıntıların büyük kısmı tek bir cümleyle özetlenebilir: işi biten bir nesneye hâlâ köklerden ulaşılabiliyor. Sık görülen kalıplar şunlardır.

1. Sınırsız büyüyen önbellek

Modül düzeyinde bir Map ya da dizi, her isteğin sonucunu sonsuza kadar tutarsa bellek yalnızca büyür. Bir üst sınır koyun. Map ekleme sırasını koruduğu için en eski kaydı keys().next() ile bulabilirsiniz:

javascript
const LIMIT = 3;
const onbellek = new Map();
function kaydet(anahtar, deger) {
  if (onbellek.has(anahtar)) onbellek.delete(anahtar);
  onbellek.set(anahtar, deger);
  if (onbellek.size > LIMIT) {
    const enEski = onbellek.keys().next().value;
    onbellek.delete(enEski);
  }
}
["a", "b", "c", "d"].forEach((h, i) => kaydet(h, i));
console.log([...onbellek.keys()]); // [ 'b', 'c', 'd' ]

2. Gereğinden fazlasını yakalayan closure

Bir closure dış kapsamdaki değişkenlere ulaşabildiği için onları yaşatabilir. Uzun ömürlü bir callback büyük bir veriye ihtiyaç duymuyorsa yalnızca gereken küçük değeri ayrı bir değişkene alın. MDN, motorun closure içindeki şeyleri beklenenden uzun tutabileceğini de not eder. Kavramın kendisi closure yazısında anlatılıyor.

javascript
function isleyiciYap() {
  const buyukDizi = new Array(1_000_000).fill(0);
  const uzunluk = buyukDizi.length; // yalnızca gereken değer
  return () => uzunluk;
}
const f = isleyiciYap();
console.log(f()); // 1000000

3. Durdurulmayan zamanlayıcılar ve unutulan aboneler

setInterval ile kurulan bir zamanlayıcı, callback'inin eriştiği her şeyi yaşatır. İşi bittiğinde clearInterval çağırın. Aynı mantık abone listeleri için de geçerlidir: eklediğiniz dinleyiciyi kaldırmanın bir yolunu her zaman döndürün.

javascript
const aboneler = new Set();
function aboneOl(fn) {
  aboneler.add(fn);
  return () => aboneler.delete(fn); // abonelikten çıkma fonksiyonu
}
const birak = aboneOl((v) => console.log("yeni değer:", v));
aboneler.forEach((fn) => fn(42)); // yeni değer: 42
birak();
console.log(aboneler.size); // 0

4. Yanlışlıkla oluşan global değişkenler

Strict mode dışında, tanımlamadan atama yaptığınız bir isim global nesneye özellik olarak eklenir ve program boyunca yaşar. Modüller ve sınıflar otomatik olarak strict olduğu için bu hata orada ReferenceError verir. Ayrıntı strict mode yazısında.

Node.js tarafında bellek

Node.js, V8'in bellek mekanizmasını yapılandırmak için bayraklar sunar. MDN'de örnek olarak verilen --max-old-space-size heap üst sınırını değiştirir, --expose-gc --inspect ise hata ayıklama oturumunda GC'ye erişim açar. Sunucu tarafında bellek profili çıkarmak ve performans ayarı için Node.js performans optimizasyonu yazısına bakabilirsiniz.

bash
node --max-old-space-size=6000 index.js
node --expose-gc --inspect index.js

Sık Sorulan Sorular

Değişkene null atamak belleği hemen boşaltır mı?

Hayır. null atamak yalnızca o referansı kaldırır. Nesneye başka bir yoldan ulaşılamıyorsa toplanabilir hale gelir, ama ne zaman toplanacağına motor karar verir.

Döngüsel referanslar sızıntı yapar mı?

Mark-and-sweep kullanan modern motorlarda, döngüdeki nesnelere köklerden ulaşılamıyorsa hayır. Sorun, döngünün bir parçası hâlâ global bir yapıdan erişilebilir olduğunda başlar.

WeakRef ile önbellek kurmak iyi bir fikir mi?

MDN'nin örneklerinden biri tam olarak budur, ama aynı sayfa API'nin davranışının neredeyse hiç garanti edilmediğini vurgular. Sınırlı boyutlu normal bir Map çoğu durumda daha öngörülebilirdir.

WeakMap ile Map arasındaki temel fark nedir?

Map anahtarlarını güçlü tutar ve üzerinde döngü kurulabilir. WeakMap anahtarlarını zayıf tutar, yalnızca nesne ve kayıtsız symbol kabul eder, döngü ve size sunmaz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: JavaScript öğrenme rehberi · Map, Set ve WeakMap · Closure nedir · Node.js performans optimizasyonu · Event loop